diff --git a/docs/Getting-Started/Migrating/0X-to-1X.md b/docs/Getting-Started/Migrating/0X-to-1X.md index 8760d6d78..be6423609 100644 --- a/docs/Getting-Started/Migrating/0X-to-1X.md +++ b/docs/Getting-Started/Migrating/0X-to-1X.md @@ -154,7 +154,7 @@ Request and Error logging are inbuilt logging types that can be enabled using [` | [`Disable-PodeRequestLogging`](../../../Functions/Logging/Disable-PodeRequestLogging) | | [`Disable-PodeErrorLogging`](../../../Functions/Logging/Disable-PodeErrorLogging) | | [`Remove-PodeLogger`](../../../Functions/Logging/Remove-PodeLogger) | -| [`Clear-PodeLoggers`](../../../Functions/Logging/Clear-PodeLoggers) | +| [`Clear-PodeLogger`](../../../Functions/Logging/Clear-PodeLogger) | ### Writing Logs diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index f6a8720fa..b50149717 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -92,6 +92,8 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: | Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | | Server.ReceiveTimeout | Define the amount of time a Receive method call will block waiting for data | [link](../Endpoints/Basic/StaticContent/#server-timeout) | | Server.DefaultFolders | Set the Default Folders paths | [link](../Routes/Utilities/StaticContent/#changing-the-default-folders) | +| Server.Logging.QueueLimit | Set the maximum number of logs allowed in the queue | [link](../Logging/Overview) | +| Server.Logging.Masking.Patterns | Regular expressions congiguration to mask sensitive logs information | [link](../Logging/Overview) | | Server.Console | Set the Console settings | [link](../Getting-Started/Console) | | Web.OpenApi.DefaultDefinitionTag | Define the primary tag name for OpenAPI ( `default` is the default) | [link](../OpenAPI/Overview) | | Web.OpenApi.UsePodeYamlInternal | Force the use of the internal YAML converter (`False` is the default) | | diff --git a/docs/Tutorials/Logging/Methods/Custom.md b/docs/Tutorials/Logging/Methods/Custom.md index 8fe03a7b1..a6cde3cc4 100644 --- a/docs/Tutorials/Logging/Methods/Custom.md +++ b/docs/Tutorials/Logging/Methods/Custom.md @@ -1,35 +1,76 @@ # Custom -Sometimes you don't want to log to a file, or the terminal; instead you want to log to something better, like LogStash, Splunk, Athena, or any other central logging platform. Although Pode doesn't have these inbuilt (yet!) it is possible to create a custom logging method, where you define a ScriptBlock with logic to send logs to these platforms. +Sometimes you may want to log to platforms other than a file or the terminal, such as LogStash, Splunk, Athena, or other central logging platforms. Although Pode doesn't have these integrations built-in (yet!), it is possible to create a custom logging method by defining a ScriptBlock with the logic to send logs to these platforms. -These custom method can be used for any log type - Requests, Error, or Custom. +Custom methods can be used for any log type: Requests, Error, or Custom. -The ScriptBlock you create will be supplied two arguments: +The ScriptBlock you create will receive two arguments: -1. The item to be logged. This could be a string (from Requests/Errors), or any custom type. -2. The options you supplied on [`New-PodeLoggingMethod`](../../../../Functions/Logging/New-PodeLoggingMethod). +1. The item to be logged. This could be a string (from Requests/Errors) or any custom type. + +2. The options you supplied to [`New-PodeLoggingMethod`](../../../../Functions/Logging/New-PodeLoggingMethod). + +Additionally, custom logging methods can be run in their own runspace by using the `-UseRunspace` parameter, ensuring isolation and efficiency. ## Examples ### Send to S3 Bucket -This example will take whatever item is supplied to it, convert it to a string, and then send it off to some S3 bucket in AWS. In this case, it will be logging Requests: +This example takes the supplied item, converts it to a string, and sends it to an S3 bucket in AWS. In this case, it will log Requests: +#### Legacy (No Runspace) ```powershell $s3_options = @{ AccessKey = $AccessKey SecretKey = $SecretKey } -$s3_logging = New-PodeLoggingType -Custom -ArgumentList $s3_options -ScriptBlock { +$s3_logging = New-PodeLoggingMethod -Custom -ArgumentList $s3_options -ScriptBlock { param($item, $s3_opts) - Write-S3Object ` - -BucketName '' ` - -Content $item.ToString() ` - -AccessKey $s3_opts.AccessKey ` + Write-S3Object \` + -BucketName '' \` + -Content $item.ToString() \` + -AccessKey $s3_opts.AccessKey \` -SecretKey $s3_opts.SecretKey } +$s3_logging | Enable-PodeRequestLogging +``` + +#### With Runspace + +```powershell +$s3_options = @{ + AccessKey = $AccessKey + SecretKey = $SecretKey +} + +$s3_logging = New-PodeLoggingMethod -Custom -UseRunspace -CustomOptions $s3_options -ScriptBlock { + # No param() allowed here + Write-S3Object \` + -BucketName '' \` + -Content $Item.ToString() \` + -AccessKey $Options.AccessKey \` + -SecretKey $Options.SecretKey +} $s3_logging | Enable-PodeRequestLogging ``` + + +In this example, the `-UseRunspace` parameter ensures that the custom logging method runs in its own runspace, providing better isolation and performance. + +##### Variable available inside the ScriptBlock + +| Variable | Type | Description | +| ----------------------- | ----------------------------- | ------------------------------------------------ | +| Item | string | Log message content | +| Options | hashtable | The options supplied to the logging method | +| Options.FailureAction | string (Ignore, Report, Halt) | Defines the behavior in case of failure. | +| Options.DataFormat | string | The date format to use for the log entries. | +| Options.AsUTC | boolean | the time is logged in UTC instead of local time. | +| Options. | PSObject | Any key passed using `-CustomOptions` parameter | +| RawItem | hashtable | Log message in raw format | + + +By leveraging custom logging methods, you can extend Pode's logging capabilities to integrate with a wide range of external platforms, providing flexibility and control over your logging strategy. \ No newline at end of file diff --git a/docs/Tutorials/Logging/Methods/File.md b/docs/Tutorials/Logging/Methods/File.md index 84b28df21..946923a79 100644 --- a/docs/Tutorials/Logging/Methods/File.md +++ b/docs/Tutorials/Logging/Methods/File.md @@ -39,3 +39,42 @@ By default Pode puts all logs in the `./logs` directory. You can use a custom pa ```powershell New-PodeLoggingMethod -File -Name 'requests' -Path 'E:/logs' | Enable-PodeRequestLogging ``` + +### Format + +The Format parameter allows you to specify the format of the log entries. Available options are: + +- RFC3164 +- RFC5424 +- Simple +- Default (default option) + +The Simple format uses the following structure: timestamp level source message. The Default format uses the legacy Pode format. + +```powershell +New-PodeLoggingMethod -File -Name 'requests' -Format 'Simple' | Enable-PodeRequestLogging +``` +A log entry using the Simple format might look like this: + +```arduino +2024-08-01T12:00:00Z INFO MyApp "Request received" +``` + +### Custom Separator +When using the Simple format, you can specify a custom separator for log entries: + +```powershell +New-PodeLoggingMethod -File -Name 'requests' -Format 'Simple' -Separator ',' | Enable-PodeRequestLogging +``` + +A log entry using the Simple format with a comma separator might look like this: +```arduino +2024-08-01T12:00:00Z,INFO,MyApp,"Request received" +``` + +### Maximum Log Entry Length +The MaxLength parameter sets the maximum length of log entries. The default value is -1, which means no limit. + +```powershell +New-PodeLoggingMethod -File -Name 'requests' -MaxLength 500 | Enable-PodeRequestLogging +``` \ No newline at end of file diff --git a/docs/Tutorials/Logging/Methods/Syslog.md b/docs/Tutorials/Logging/Methods/Syslog.md new file mode 100644 index 000000000..eba6db8d2 --- /dev/null +++ b/docs/Tutorials/Logging/Methods/Syslog.md @@ -0,0 +1,56 @@ + +# Syslog + +Pode supports logging items to a Syslog server using the inbuilt Syslog logging method. This method allows you to define various parameters such as the Syslog server address, port, transport protocol, and more. The logging method will convert any item to a string and send it to the configured Syslog server. + +By default, Pode will use UDP as the transport protocol and RFC5424 as the Syslog protocol. You can customize these settings based on your Syslog server requirements. + +## Examples + +### Basic + +The following example will setup the Syslog logging method for logging requests: + +```powershell +New-PodeLoggingMethod -Syslog -Server '192.168.1.1' | Enable-PodeRequestLogging +``` + +### Custom Port + +The following example will configure Syslog logging to use a custom port. The default port is 514, but you can specify a different port if needed: + +```powershell +New-PodeLoggingMethod -Syslog -Server '192.168.1.1' -Port 1514 | Enable-PodeRequestLogging +``` + +### Secure Connection with TLS + +The following example will configure Syslog logging to use TLS for a secure connection. You can also specify the TLS protocol version to use: + +```powershell +New-PodeLoggingMethod -Syslog -Server '192.168.1.1' -Transport 'TLS' -TlsProtocol 'TLS1.2' | Enable-PodeRequestLogging +``` + +### Custom Syslog Protocol + +The following example will configure Syslog logging to use a different Syslog protocol. The default protocol is RFC5424, but you can specify RFC3164 if needed: + +```powershell +New-PodeLoggingMethod -Syslog -Server '192.168.1.1' -SyslogProtocol 'RFC3164' | Enable-PodeRequestLogging +``` + +### Skip Certificate Validation + +The following example will configure Syslog logging to skip certificate validation for TLS connections. This is useful for testing purposes but not recommended for production environments: + +```powershell +New-PodeLoggingMethod -Syslog -Server '192.168.1.1' -Transport 'TLS' -SkipCertificateCheck | Enable-PodeRequestLogging +``` + +### Custom Encoding + +The following example will configure Syslog logging to use a different encoding for the Syslog messages. The default encoding is UTF8: + +```powershell +New-PodeLoggingMethod -Syslog -Server '192.168.1.1' -Encoding 'ASCII' | Enable-PodeRequestLogging +``` \ No newline at end of file diff --git a/docs/Tutorials/Logging/Overview.md b/docs/Tutorials/Logging/Overview.md index ac6527c05..36fb528dd 100644 --- a/docs/Tutorials/Logging/Overview.md +++ b/docs/Tutorials/Logging/Overview.md @@ -1,15 +1,17 @@ # Overview -There are two aspects to logging in Pode: Methods and Types. +Logging in Pode consists of two main components: Methods and Types. -* Methods define how log items should be recorded, such as to a file, terminal, or event viewer. -* Types define how items to log are transformed, and what should be supplied to the Method. +- **Methods**: Define how log items should be recorded, such as to a file, terminal, or event viewer. Each logging method operates in its own runspace, providing isolation and efficiency. The exception to this is the Custom method, which by default runs in the same runspace as the log dispatcher unless the `-UseRunspace` parameter is specified. -For example when you supply an Exception to [`Write-PodeErrorLog`](../../../Functions/Logging/Write-PodeErrorLog), this Exception is first supplied to Pode's inbuilt Error logging type. This type transforms any Exception (or Error Record) into a string which can then be supplied to the File logging method. +- **Types**: Define how log items are transformed and what data should be supplied to the Method. -In Pode you can use File, Terminal, Event Viewer, or a Custom method. As well as Request, Error, or a Custom type. +When you supply an Exception to [`Write-PodeErrorLog`](../../../Functions/Logging/Write-PodeErrorLog), the Exception is first processed by Pode's built-in Error logging type. This type transforms the Exception (or Error Record) into a string format, which can then be recorded by the logging method (e.g., File). + +Pode supports various logging methods, including File, Terminal, Event Viewer, Syslog, Restful, or Custom methods. Additionally, you can utilize different logging types such as Request, Error, or Custom types. + +This flexibility allows you to create a custom logging method that can output logs to various platforms, such as an S3 bucket, Splunk, or any other logging service. -This means you could write a logging method to output to an S3 bucket, Splunk, or any other logging platform. ## Masking Values @@ -109,3 +111,50 @@ Instead of writing logs one-by-one, the above will keep transformed log items in This means that the method's scriptblock will receive an array of items, rather than a single item. You can also sent a `-BatchTimeout` value, in seconds, so that if your batch size it 10 but only 5 log items are added, then after the timeout value the logs items will be sent to your method. + + + +## Configuring Failure Actions for Log Writing + +Defines the behavior in case of failure to write a log. This can happen if the disk is full, the Syslog server is offline, or if the number of logs in the queue reaches the maximum allowed. The options are: +- **Ignore** : Does nothing and continues execution. **(Default)** +- **Report** : Writes a message to the console for any failure. +- **Halt** : Writes a message to the console and shuts down the Pode server. + +```powershell +New-PodeLoggingMethod -File -Path './logs' -Name 'errors' -FailureAction 'Report' | Enable-PodeRequestLogging +``` + +## QueueLimit +Defines the maximum number of logs allowed in the queue before throwing an event. +The default value is 500. The exception is handled based on the `-FailureAction` parameter. + +```powershell +@{ + Server = @{ + Logging = @{ + QueueLimit = 1000 + } + } +} +``` + +## DataFormat +The date format to use for the log entries. The default format is `'dd/MMM/yyyy:HH:mm:ss zzz'`. + +```powershell +New-PodeLoggingMethod -File -Path './logs' -Name 'access' -DataFormat 'yyyy-MM-dd HH:mm:ss' | Enable-PodeErrorLogging +``` + +## ISO8601 +If set, the date format will be ISO 8601 compliant (equivalent to `-DataFormat 'yyyy-MM-ddTHH:mm:ssK'`). This parameter is mutually exclusive with DataFormat. + +```powershell +New-PodeLoggingMethod -File -Path './logs' -Name 'access' -ISO8601 | Enable-PodeErrorLogging +``` + +## AsUTC +If set, the time will be logged in UTC instead of local time. + +```powershell +New-PodeLoggingMethod -File -Path './logs' -Name 'access' -AsUTC -ISO8601 | Enable-PodeErrorLogging \ No newline at end of file diff --git a/docs/Tutorials/Logging/Types/General.md b/docs/Tutorials/Logging/Types/General.md new file mode 100644 index 000000000..1fc2daee2 --- /dev/null +++ b/docs/Tutorials/Logging/Types/General.md @@ -0,0 +1,70 @@ + +# General Logging + +Pode supports general logging, allowing you to define custom logging methods and log levels. This feature enables you to write logs based on specified methods, ensuring flexibility and control over logging outputs. + +To enable general logging, use the `Add-PodeLoggingMethod` function. This function takes a hashtable defining the logging method, including a ScriptBlock for log output. You can specify various log levels to be enabled, such as Error, Emergency, Alert, Critical, Warning, Notice, Informational, Info, Verbose, and Debug. + +## Enabling General Logging + +To enable general logging, use the `Add-PodeLoggingMethod` function, supplying the necessary parameters: + +- `Method`: The hashtable defining the logging method, including the ScriptBlock for log output. +- `Levels`: An array of log levels to be enabled for the logging method (default includes Error, Emergency, Alert, Critical, Warning, Notice, Informational, Info, Verbose, Debug). +- `Name`: The name of the logging method to be enabled. +- `Raw`: If set, the raw log data will be included in the logging output. + +### Example + +```powershell +$method = New-PodeLoggingMethod -syslog -Server 127.0.0.1 -Transport UDP +$method | Add-PodeLoggingMethod -Name "mysyslog" +``` + +## Disabling General Logging + +To disable a general logging method, use the `Remove-PodeLoggingMethod` function with the `Name` parameter: + +### Example + +```powershell +Remove-PodeLoggingMethod -Name 'mysyslog' +``` + +With these functions, Pode ensures robust and customizable logging capabilities, allowing you to manage logs effectively based on your specific requirements. + +## Writing to General Logs + +Pode allows you to write logs to configured custom or inbuilt logging methods using the `Write-PodeLog` function. This function supports both custom and inbuilt logging methods, enabling structured logging with various log levels and messages. + +### Writing to General Logs + +To write logs, you can use the `Write-PodeLog` function with different parameters to specify the logging method, log level, message, and other details. + +#### Example Usage + +##### Logging an Object + +To write an object to a configured logging method: + +```powershell +$logItem = @{ + Date = [datetime]::Now + Level = 'Informational' + Server = 'MyServer' + Category = 'General' + Message = 'This is a log message' + StackTrace = '' +} +$logItem | Write-PodeLog -Name 'mysyslog' +``` + +##### Logging with Custom Levels and Messages + +To log a custom message with a specific log level: + +```powershell +Write-PodeLog -Name 'mysyslog' -Level 'Error' -Message 'An error occurred.' -Tag 'MyApp' +``` + +In these examples, `Write-PodeLog` is used to write structured log items or custom messages to the specified logging methods, helping you maintain organized and detailed logs. \ No newline at end of file diff --git a/docs/Tutorials/Logging/Types/Requests.md b/docs/Tutorials/Logging/Types/Requests.md index 60a812d62..96ba2d805 100644 --- a/docs/Tutorials/Logging/Types/Requests.md +++ b/docs/Tutorials/Logging/Types/Requests.md @@ -6,7 +6,10 @@ Pode has inbuilt Request logging logic, that will parse and return a valid log i To enable and use the Request logging you use the [`Enable-PodeRequestLogging`](../../../../Functions/Logging/Enable-PodeRequestLogging) function, supplying a logging method from [`New-PodeLoggingMethod`](../../../../Functions/Logging/New-PodeLoggingMethod). -The Request type logic will format a string using [Combined Log Format](https://httpd.apache.org/docs/1.3/logs.html#combined). This string is then supplied to the logging method's scriptblock. If you're using a Custom logging method and want the raw hashtable instead, you can supply `-Raw` to [`Enable-PodeRequestLogging`](../../../../Functions/Logging/Enable-PodeRequestLogging). +The Request type logic will format a string using [Combined Log Format](https://httpd.apache.org/docs/1.3/logs.html#combined). +This string is then supplied to the logging method's scriptblock. You can customize the log format using the `-LogFormat` parameter with options like `Extended`, `Common`, `Combined`, and `JSON`. + +If you're using a Custom logging method and want the raw hashtable instead, you can supply `-Raw` to [`Enable-PodeRequestLogging`](../../../../Functions/Logging/Enable-PodeRequestLogging). ## Examples @@ -18,6 +21,23 @@ The following example simply enables Request logging, and will output all items New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging ``` +### Log Format + +#### Extended Log Format +The following example enables Request logging using the Extended Log Format: + +```powershell +New-PodeLoggingMethod -File -Path './logs' -Name 'requests' | Enable-PodeRequestLogging -LogFormat 'Extended' +``` + +#### JSON Format +The following example enables Request logging using JSON Format: + +```powershell +New-PodeLoggingMethod -File -Path './logs' -Name 'requests' | Enable-PodeRequestLogging -LogFormat 'Json' +``` + + ### Using Raw Item The following example uses a Custom logging method, and sets Request logging to return and supply the raw hashtable to the Custom method's scriptblock. The Custom method simply logs the Host an StatusCode to the terminal (but could be to something like an S3 bucket): @@ -70,4 +90,4 @@ The raw Request hashtable that will be supplied to any Custom logging methods wi Size = '9001' } } -``` +``` \ No newline at end of file diff --git a/examples/Logging.ps1 b/examples/Logging.ps1 index 2625991bf..fb7f0cf9a 100644 --- a/examples/Logging.ps1 +++ b/examples/Logging.ps1 @@ -22,11 +22,20 @@ License: MIT License #> + +param( + [ValidateSet('Terminal', 'File', 'mylog', 'Syslog', 'EventViewer', 'Custom')] + [string[]] + $LoggingType = @( 'file', 'Custom', 'Syslog'), + + [switch] + $Raw +) + try { - # Determine the script path and Pode module path + #Determine the script path and Pode module path $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) $podePath = Split-Path -Parent -Path $ScriptPath - # Import the Pode module from the source path if it exists, otherwise from installed modules if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop @@ -36,39 +45,70 @@ try { } } catch { throw } - # or just: # Import-Module Pode -$LOGGING_TYPE = 'terminal' # Terminal, File, Custom - # create a server, and start listening on port 8081 -Start-PodeServer { +Start-PodeServer -browse { Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http Set-PodeViewEngine -Type Pode + $logging = @() - switch ($LOGGING_TYPE.ToLowerInvariant()) { - 'terminal' { - New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + if ( $LoggingType -icontains 'terminal') { + $logging += New-PodeLoggingMethod -Terminal + } + + if ( $LoggingType -icontains 'file') { + $logging += New-PodeFileLoggingMethod -Name 'file' -MaxDays 4 -Format Simple -ISO8601 + $requestLogging = New-PodeLoggingMethod -File -Name 'requests' -MaxDays 4 + } + + if ( $LoggingType -icontains 'custom') { + $logging += New-PodeLoggingMethod -Custom -ArgumentList 'arg1', 'arg2', 'arg3' -ScriptBlock { + param($item, $arg1 , $arg2, $arg3, $rawItem) + $item | Out-File './examples/logs/customLegacy.log' -Append + $arg1 , $arg2, $arg3 -join ',' | Out-File './examples/logs/customLegacy_argumentList.log' -Append + $rawItem | Out-File './examples/logs/customLegacy_rawItem.log' -Append } - 'file' { - New-PodeLoggingMethod -File -Name 'requests' -MaxDays 4 | Enable-PodeRequestLogging + $logging += New-PodeCustomLoggingMethod -CustomOptions @{ 'opt1' = 'something'; 'opt2' = 'else' } -ScriptBlock { + $item | Out-File './examples/logs/customWithRunspace.log' -Append + $options | Out-File './examples/logs/customWithRunspace_options.log' -Append + $rawItem | Out-File './examples/logs/customWithRunspace_rawItem.log' -Append } + } - 'custom' { - $type = New-PodeLoggingMethod -Custom -ScriptBlock { - param($item) - # send request row to S3 - } + if ( $LoggingType -icontains 'eventviewer') { + $logging += New-PodeLoggingMethod -EventViewer + } - $type | Enable-PodeRequestLogging - } + if ( $LoggingType -icontains 'syslog') { + $logging += New-PodeSyslogLoggingMethod -Server 127.0.0.1 -Transport UDP -AsUTC -ISO8601 -FailureAction Report + } + + if ($logging.Count -eq 0) { + throw 'No logging selected' } + if ( $requestLogging) { + $requestLogging | Enable-PodeRequestLogging -LogFormat Extended + } + + New-PodeFileLoggingMethod -Name 'error' -MaxDays 4 -Format RFC5424 -ISO8601 | Enable-PodeErrorLogging -Raw -Levels Error + @( + (New-PodeFileLoggingMethod -Name 'default' -MaxDays 4 -Format Simple -ISO8601 -DefaultTag 'filetest') + (New-PodeFileLoggingMethod -Name 'defaultRFC5424' -MaxDays 4 -Format RFC5424 -ISO8601 -DefaultTag 'filetestRFC5424') + (New-PodeSyslogLoggingMethod -Server 127.0.0.1 -Transport UDP -AsUTC -ISO8601 -SyslogProtocol RFC3164 -FailureAction Report -DefaultTag 'test') + ) | Enable-PodeDefaultLogging -Raw + $logging | Add-PodeLoggingMethod -Name 'mylog' -Raw:$Raw + Write-PodeLog -Name 'mylog' -Message 'just started' -Level 'Info' # GET request for web page on "localhost:8081/" Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeLog -Name 'mylog' -Message 'My custom log' -Level 'Info' + Write-PodeLog -Message 'This is for the deafult log.' + Start-Sleep -Seconds 2 + Write-PodeLog -Message 'An allert with a new tag.' -Tag 'newTag' -Level Alert Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } } @@ -77,9 +117,19 @@ Start-PodeServer { Set-PodeResponseStatus -Code 500 } + Add-PodeRoute -Method Get -Path '/exception' -ScriptBlock { + try { + throw 4 / 0 + } + catch { + $_ | Write-PodeErrorLog + } + Set-PodeResponseStatus -Code 500 + } + # GET request to download a file Add-PodeRoute -Method Get -Path '/download' -ScriptBlock { Set-PodeResponseAttachment -Path 'Anger.jpg' } -} +} \ No newline at end of file diff --git a/examples/PetStore/Petstore-OpenApi.ps1 b/examples/PetStore/Petstore-OpenApi.ps1 index 983df0ff8..417911ca8 100644 --- a/examples/PetStore/Petstore-OpenApi.ps1 +++ b/examples/PetStore/Petstore-OpenApi.ps1 @@ -115,7 +115,9 @@ Start-PodeServer -Threads 1 -ScriptBlock { Add-PodeFavicon -Default # Enable error logging - New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + New-PodeFileLoggingMethod -Name 'error' -MaxDays 4 -Format RFC5424 -ISO8601 | Enable-PodeErrorLogging + New-PodeFileLoggingMethod -Name 'petstore' -MaxDays 4 -Format RFC5424 -ISO8601 | Enable-PodeDefaultLogging -Levels Alert,Critical,Emergency,Error,Informational,Warning + New-PodeFileLoggingMethod -Name 'access' -MaxDays 4 -ISO8601 | Enable-PodeRequestLogging -LogFormat Extended # Configure CORS Set-PodeSecurityAccessControl -Origin '*' -Duration 7200 -WithOptions -AuthorizationHeader -autoMethods -AutoHeader -Credentials -CrossDomainXhrRequests diff --git a/examples/Session-Data.ps1 b/examples/Session-Data.ps1 new file mode 100644 index 000000000..b2ce0bd3a --- /dev/null +++ b/examples/Session-Data.ps1 @@ -0,0 +1,84 @@ +<# +.SYNOPSIS + Demonstrates session management using Pode with basic authentication. + +.DESCRIPTION + This script sets up a Pode web server with a basic authentication endpoint. It tracks user sessions and + increments a session counter each time the endpoint is accessed. + +.EXAMPLE + To run the sample: ./SessionData.ps1 + $result=Invoke-WebRequest -Uri "http://localhost:8081/auth/basic" -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } + $session = ($result.Headers['pode.sid'] | Select-Object -First 1) + + $result = Invoke-WebRequest -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = $session } + $content = ($result.Content | ConvertFrom-Json) + $content.Result #should be 2 + + $result = Invoke-WebRequest -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = $session } + $content = ($result.Content | ConvertFrom-Json) + $content.Result #should be 3 and so on... + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/SessionData.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script directory path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Check if Pode is available from source; otherwise, load it from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } # Stop execution if Pode module fails to load + +# Start the Pode web server +Start-PodeServer -ScriptBlock { + + # Define an HTTP endpoint on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # Add a route to gracefully stop the server + Add-PodeRoute -Method Get -Path '/close' -ScriptBlock { + Close-PodeServer + } + + # Enable session middleware with secret-based authentication and session persistence + Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 5 -Extend -UseHeaders + + # Define a basic authentication scheme + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Auth' -ScriptBlock { + param($username, $password) + + # Authenticate user based on predefined credentials + if (($username -eq 'morty') -and ($password -eq 'pickle')) { + return @{ User = @{ ID = 'M0R7Y302' } } # Return user ID if authentication is successful + } + + return @{ Message = 'Invalid details supplied' } # Return error message for failed authentication + } + + # Define a route that requires authentication and maintains session state + Add-PodeRoute -Method Post -Path '/auth/basic' -Authentication Auth -ScriptBlock { + + # Increment session view count for the authenticated user + $WebEvent.Session.Data.Views++ + + # Return JSON response with session details + Write-PodeJsonResponse -Value @{ + Result = 'OK' + Username = $WebEvent.Auth.User.ID + Views = $WebEvent.Session.Data.Views + } + } +} diff --git a/examples/server.psd1 b/examples/server.psd1 index ec5b15edf..86a66de67 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -40,6 +40,7 @@ '(?AppleWebKit\/)\d+\.\d+(?( @@ -348,7 +355,7 @@ public void StartReceive() /// Thrown if the request cannot be upgraded to a WebSocket. public async Task UpgradeWebSocket(string clientId = null) { - PodeHelpers.WriteErrorMessage($"Upgrading Websocket", Listener, PodeLoggingLevel.Verbose, this); + PodeLogger.LogMessage($"Upgrading Websocket", Listener, PodeLoggingLevel.Verbose, this); if (!IsWebSocket) { @@ -393,7 +400,7 @@ public async Task UpgradeWebSocket(string clientId = null) var signal = new PodeSignal(this, HttpRequest.Url.AbsolutePath, clientId); Request = new PodeSignalRequest(HttpRequest, signal); Listener.AddSignal(SignalRequest.Signal); - PodeHelpers.WriteErrorMessage($"Websocket upgraded", Listener, PodeLoggingLevel.Verbose, this); + PodeLogger.LogMessage($"Websocket upgraded", Listener, PodeLoggingLevel.Verbose, this); } /// @@ -413,7 +420,7 @@ public void Dispose(bool force) { lock (_lockable) { - PodeHelpers.WriteErrorMessage($"Disposing Context", Listener, PodeLoggingLevel.Verbose, this); + PodeLogger.LogMessage($"Disposing Context", Listener, PodeLoggingLevel.Verbose, this); Listener.RemoveProcessingContext(this); if (IsClosed) @@ -483,14 +490,14 @@ public void Dispose(bool force) } catch (Exception ex) { - PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Error); + PodeLogger.LogException(ex, Listener, PodeLoggingLevel.Error); } finally { // Handle re-receiving or socket clean-up. if ((_awaitingBody || (IsKeepAlive && !IsErrored && !IsTimeout && !Response.SseEnabled)) && !force) { - PodeHelpers.WriteErrorMessage($"Re-receiving Request", Listener, PodeLoggingLevel.Verbose, this); + PodeLogger.LogMessage($"Re-receiving Request", Listener, PodeLoggingLevel.Verbose, this); StartReceive(); } else diff --git a/src/Listener/PodeFileWatcher.cs b/src/Listener/PodeFileWatcher.cs index 941b78be6..932e143c5 100644 --- a/src/Listener/PodeFileWatcher.cs +++ b/src/Listener/PodeFileWatcher.cs @@ -87,7 +87,7 @@ private void FileEventHandler(object _, FileSystemEventArgs e) private void FileErrorEventHandler(object _, FileWatcherErrorEventArgs e) { - PodeHelpers.WriteException(e.Error, Watcher); + PodeLogger.LogException(e.Error, Watcher); } } } \ No newline at end of file diff --git a/src/Listener/PodeFormat.cs b/src/Listener/PodeFormat.cs new file mode 100644 index 000000000..f363d880b --- /dev/null +++ b/src/Listener/PodeFormat.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections; +using System.Text; +using System.Text.RegularExpressions; + +namespace Pode +{ + public static class PodeFormat + { + /// + /// Sanitizes input by returning a default value if the input is null or whitespace. + /// + /// The object value to be sanitized. + /// A sanitized string, or "-" if the input is null or whitespace. + private static string Sanitize(object value) + { + return value == null || string.IsNullOrWhiteSpace(value.ToString()) ? "-" : value.ToString(); + } + + /// + /// Formats error log entries based on the provided options, including Date, Level, ThreadId, Server, Category, Message, and StackTrace. + /// + /// A hashtable containing log details. + /// A hashtable containing format options such as Levels, Raw, and DataFormat. + /// A formatted log string, or the original item if Raw is specified, or null if Level is not in options.Levels. + public static object ErrorsLog(Hashtable item, Hashtable options) + { + if (item == null || options == null) return null; + + // Check for required keys in the log item + if (!item.ContainsKey("Level") || !item.ContainsKey("Date") || !item.ContainsKey("ThreadId") || + !item.ContainsKey("Server") || !item.ContainsKey("Category") || !item.ContainsKey("Message") || !item.ContainsKey("StackTrace")) + { + return null; + } + + // Ensure the log level is present in the allowed levels + if (options.ContainsKey("Levels") && !((IList)options["Levels"]).Contains(item["Level"])) + { + return null; + } + + // Return raw item if Raw option is set + if (options.ContainsKey("Raw") && (bool)options["Raw"]) + { + return item; + } + + // Set the date format or use a default + string dataFormat = options.ContainsKey("DataFormat") ? options["DataFormat"].ToString() : "yyyy-MM-dd HH:mm:ss"; + + // Build the log entry string + StringBuilder sb = new StringBuilder(); + return sb.Append("Date: ").Append(((DateTime)item["Date"]).ToString(dataFormat)).Append(" Level: ").Append(Sanitize(item["Level"])) + .Append(" ThreadId: ").Append(Sanitize(item["ThreadId"])).Append(" Server: ").Append(Sanitize(item["Server"])).Append(" Category: ") + .Append(Sanitize(item["Category"])).Append(" Message: ").Append(Sanitize(item["Message"])).Append(" StackTrace: ") + .Append(Sanitize(item["StackTrace"])).ToString(); + } + + /// + /// Formats request log entries based on the specified format in options, supporting formats like "extended", "common", "json", and "combined". + /// + /// A hashtable containing request log details. + /// A hashtable containing format options such as LogFormat and Raw. + /// A formatted request log string, or the original item if Raw is specified. + public static object RequestLog(Hashtable item, Hashtable options) + { + if (item == null || options == null) return null; + + // Return raw item if Raw option is set + if (options.ContainsKey("Raw") && (bool)options["Raw"]) + { + return item; + } + + // Retrieve the log format, defaulting to "combined" + string logFormat = options.ContainsKey("LogFormat") ? options["LogFormat"].ToString().ToLowerInvariant() : "combined"; + + StringBuilder sb = new StringBuilder(); + + switch (logFormat) + { + case "extended": + if (item.ContainsKey("Host") && item.ContainsKey("User") && item.ContainsKey("Request") && item.ContainsKey("Response") && + item["Request"] is Hashtable requestExtended && item["Response"] is Hashtable responseExtended) + { + return sb.Append(((DateTime)item["Date"]).ToString("yyyy-MM-dd")).Append(' ').Append(((DateTime)item["Date"]).ToString("HH:mm:ss")).Append(' ') + .Append(Sanitize(item["Host"])).Append(' ').Append(Sanitize(item["RfcUserIdentity"])).Append(' ') + .Append(Sanitize(item["User"])).Append(' ').Append(Sanitize(requestExtended["Method"])).Append(' ') + .Append(Sanitize(requestExtended["Resource"])).Append(' ').Append("- ").Append(Sanitize(responseExtended["StatusCode"])).Append(' ') + .Append(Sanitize(responseExtended["Size"])).Append(' ').Append("\"").Append(Sanitize(requestExtended["Agent"])).Append("\"") + .ToString(); + } + break; + + case "common": + if (item.ContainsKey("Host") && item.ContainsKey("RfcUserIdentity") && item.ContainsKey("User") && item.ContainsKey("Request") && item.ContainsKey("Response") && + item["Request"] is Hashtable requestCommon && item["Response"] is Hashtable responseCommon) + { + return sb.Append(Sanitize(item["Host"])).Append(' ').Append(Sanitize(item["RfcUserIdentity"])).Append(' ').Append(Sanitize(item["User"])).Append(" [") + .Append(Regex.Replace(((DateTime)item["Date"]).ToString("dd/MMM/yyyy:HH:mm:ss zzz"), @"([+-]\d{2}):(\d{2})", "$1$2")).Append("] \"") + .Append(Sanitize(requestCommon["Method"])).Append(' ').Append(Sanitize(requestCommon["Resource"])).Append(' ') + .Append(Sanitize(requestCommon["Protocol"])).Append("\" ").Append(Sanitize(responseCommon["StatusCode"])) + .Append(' ').Append(Sanitize(responseCommon["Size"])).ToString(); + } + break; + + case "json": + if (item.ContainsKey("Host") && item.ContainsKey("User") && item.ContainsKey("Request") && item.ContainsKey("Response") && + item["Request"] is Hashtable requestJson && item["Response"] is Hashtable responseJson) + { + return sb.Append("{\"time\": \"").Append(((DateTime)item["Date"]).ToString("yyyy-MM-ddTHH:mm:ssK")).Append("\",\"remote_ip\": \"") + .Append(Sanitize(item["Host"])).Append("\",\"user\": \"").Append(Sanitize(item["User"])).Append("\",\"method\": \"") + .Append(Sanitize(requestJson["Method"])).Append("\",\"uri\": \"").Append(Sanitize(requestJson["Resource"])) + .Append("\",\"query\": \"").Append(Sanitize(requestJson["Query"])).Append("\",\"status\": ") + .Append(Sanitize(responseJson["StatusCode"])).Append(",\"response_size\": ") + .Append(Sanitize(responseJson["Size"])).Append(",\"user_agent\": \"").Append(Sanitize(requestJson["Agent"])) + .Append("\"}").ToString(); + } + break; + + default: + if (item.ContainsKey("Host") && item.ContainsKey("RfcUserIdentity") && item.ContainsKey("User") && item.ContainsKey("Request") && item.ContainsKey("Response") && + item["Request"] is Hashtable requestCombined && item["Response"] is Hashtable responseCombined) + { + return sb.Append(Sanitize(item["Host"])).Append(' ').Append(Sanitize(item["RfcUserIdentity"])).Append(' ').Append(Sanitize(item["User"])).Append(" [") + .Append(Regex.Replace(((DateTime)item["Date"]).ToString("dd/MMM/yyyy:HH:mm:ss zzz"), @"([+-]\d{2}):(\d{2})", "$1$2")).Append("] \"") + .Append(Sanitize(requestCombined["Method"])).Append(' ').Append(Sanitize(requestCombined["Resource"])).Append(' ') + .Append(Sanitize(requestCombined["Protocol"])).Append("\" ").Append(Sanitize(responseCombined["StatusCode"])) + .Append(' ').Append(Sanitize(responseCombined["Size"])).Append(" \"") + .Append(Sanitize(requestCombined["Referrer"])).Append("\" \"").Append(Sanitize(requestCombined["Agent"])).Append("\"") + .ToString(); + } + break; + } + return null; + } + + /// + /// Formats general log entries by checking for level filtering and required fields. + /// + /// A hashtable containing general log details. + /// A hashtable containing format options such as Levels, Raw, and DataFormat. + /// A formatted general log string, or the original item if Raw is specified, or null if Level is not in options.Levels. + public static object GeneralLog(Hashtable item, Hashtable options) + { + if (item == null || options == null) return null; + + // Ensure the log level is present in the allowed levels + if (options.ContainsKey("Levels") && !((IList)options["Levels"]).Contains(item["Level"])) + { + return null; + } + + // Return raw item if Raw option is set + if (options.ContainsKey("Raw") && (bool)options["Raw"]) + { + return item; + } + + // Set the date format or use a default + string dataFormat = options.ContainsKey("DataFormat") ? options["DataFormat"].ToString() : "yyyy-MM-dd HH:mm:ss"; + + // Build the log entry string + StringBuilder sb = new StringBuilder(); + return sb.Append('[').Append(((DateTime)item["Date"]).ToString(dataFormat)).Append("] ") + .Append(Sanitize(item["Level"])).Append(' ').Append(Sanitize(item["Tag"])).Append(' ').Append(Sanitize(item["ThreadId"])).Append(' ').Append(Sanitize(item["Message"])) + .ToString(); + } + + /// + /// Formats a syslog message from raw data and applies masking where necessary. + /// + /// A hashtable representing raw log data. + /// A hashtable containing options for message formatting, such as Format, DataFormat, and MaxLength. + /// A hashtable with masking patterns to obfuscate sensitive information in the message. + /// A formatted syslog message string. + public static string Syslog(Hashtable rawItem, Hashtable options, Hashtable masking) + { + int maxLength = -1; + string message = string.Empty; + + // Process message and stack trace, applying masking if available + if (rawItem.ContainsKey("Message")) + { + if (rawItem.ContainsKey("StackTrace") && !string.IsNullOrEmpty(rawItem["StackTrace"] as string)) + { + message = $"{rawItem["Level"].ToString().ToUpperInvariant()}: {PodeLogger.ProtectLogItem(rawItem["Message"].ToString(), masking)}. Exception Type: {rawItem["Category"]}. Stack Trace: {rawItem["StackTrace"]}"; + } + else + { + message = PodeLogger.ProtectLogItem(rawItem["Message"].ToString(), masking); + } + } + + // Map log level to syslog severity + int severity; + string level = rawItem["Level"].ToString().ToLowerInvariant(); + switch (level) + { + case "emergency": severity = 0; break; + case "alert": severity = 1; break; + case "critical": severity = 2; break; + case "error": severity = 3; break; + case "warning": severity = 4; break; + case "notice": severity = 5; break; + case "info": + case "informational": severity = 6; break; + case "debug": severity = 7; break; + default: severity = 6; break; + } + + // Set tag and priority + string tag = string.IsNullOrEmpty(rawItem["Tag"] as string) + ? (options != null && options.ContainsKey("DefaultTag") ? options["DefaultTag"].ToString() : "DefaultTag") + : rawItem["Tag"].ToString(); + + int facility = 1; // User-level messages + int priority = (facility * 8) + severity; + int processId = System.Diagnostics.Process.GetCurrentProcess().Id; + string fullSyslogMessage; + string timestamp; + + // Determine syslog format and format accordingly + switch (options["Format"].ToString().ToUpper()) + { + case "RFC3164": + maxLength = 1024; + timestamp = ((DateTime)rawItem["Date"]).ToString("MMM dd HH:mm:ss"); + fullSyslogMessage = string.Format("<{0}>{1} {2} {3}: {4}", priority, timestamp, Environment.MachineName, tag, message); + break; + + case "RFC5424": + maxLength = 2048; + timestamp = ((DateTime)rawItem["Date"]).ToString("yyyy-MM-ddTHH:mm:ss.ffffffK"); + fullSyslogMessage = string.Format("<{0}>1 {1} {2} {3} {4} - - {5}", priority, timestamp, Environment.MachineName, tag, processId, message); + break; + + default: + maxLength = options != null && options.ContainsKey("MaxLength") ? Convert.ToInt32(options["MaxLength"]) : -1; + string dataFormat = options != null && options.ContainsKey("DataFormat") ? options["DataFormat"].ToString() : null; + string separator = options != null && options.ContainsKey("Separator") ? options["Separator"].ToString() : " "; + timestamp = !string.IsNullOrEmpty(dataFormat) ? ((DateTime)rawItem["Date"]).ToString(dataFormat) : string.Empty; + fullSyslogMessage = string.Format("{0}{1}{2}{3}{4}{5}{6}", timestamp, separator, rawItem["Level"], separator, tag, separator, message); + break; + } + + // Truncate message if it exceeds maxLength + if (maxLength > 0 && fullSyslogMessage.Length > maxLength) + { + return fullSyslogMessage.Substring(0, maxLength); + } + + return fullSyslogMessage; + } + } +} diff --git a/src/Listener/PodeHelpers.cs b/src/Listener/PodeHelpers.cs index 50adc84c6..1069ca8ee 100644 --- a/src/Listener/PodeHelpers.cs +++ b/src/Listener/PodeHelpers.cs @@ -5,14 +5,16 @@ using System.Security.Cryptography; using System.Reflection; using System.Runtime.Versioning; -using System.Threading.Tasks; +using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.Threading; +using System.Threading.Tasks; using System.Text; using System.IO.Compression; namespace Pode { - public class PodeHelpers + public static class PodeHelpers { public static readonly string[] HTTP_METHODS = new string[] { "CONNECT", "DELETE", "GET", "HEAD", "MERGE", "OPTIONS", "PATCH", "POST", "PUT", "TRACE" }; public const string WEB_SOCKET_MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; @@ -42,30 +44,6 @@ public static bool IsNetFramework } } - public static void WriteException(Exception ex, PodeConnector connector = default, PodeLoggingLevel level = PodeLoggingLevel.Error) - { - if (ex == default(Exception)) - { - return; - } - - // return if logging disabled, or if level isn't being logged - if (connector != default(PodeConnector) && (!connector.ErrorLoggingEnabled || !connector.ErrorLoggingLevels.Contains(level.ToString(), StringComparer.InvariantCultureIgnoreCase))) - { - return; - } - - // write the exception to terminal - Console.WriteLine($"[{level}] {ex.GetType().Name}: {ex.Message}"); - Console.WriteLine(string.IsNullOrEmpty(ex.StackTrace) ? " [No Stack Trace]" : ex.StackTrace); - - if (ex.InnerException != null) - { - Console.WriteLine($"[{level}] {ex.InnerException.GetType().Name}: {ex.InnerException.Message}"); - Console.WriteLine(string.IsNullOrEmpty(ex.InnerException.StackTrace) ? " [No Stack Trace]" : ex.InnerException.StackTrace); - } - } - public static void HandleAggregateException(AggregateException aex, PodeConnector connector = default, PodeLoggingLevel level = PodeLoggingLevel.Error, bool handled = false) { try @@ -77,7 +55,7 @@ public static void HandleAggregateException(AggregateException aex, PodeConnecto return true; } - WriteException(ex, connector, level); + PodeLogger.LogException(ex, connector, level); return false; }); } @@ -90,29 +68,7 @@ public static void HandleAggregateException(AggregateException aex, PodeConnecto } } - public static void WriteErrorMessage(string message, PodeConnector connector = default, PodeLoggingLevel level = PodeLoggingLevel.Error, PodeContext context = default) - { - // do nothing if no message - if (string.IsNullOrWhiteSpace(message)) - { - return; - } - - // return if logging disabled, or if level isn't being logged - if (connector != default(PodeConnector) && (!connector.ErrorLoggingEnabled || !connector.ErrorLoggingLevels.Contains(level.ToString(), StringComparer.InvariantCultureIgnoreCase))) - { - return; - } - if (context == default(PodeContext)) - { - Console.WriteLine($"[{level}]: {message}"); - } - else - { - Console.WriteLine($"[{level}]: [ContextId: {context.ID}] {message}"); - } - } public static string NewGuid(int length = 16) { diff --git a/src/Listener/PodeListener.cs b/src/Listener/PodeListener.cs index c0443c319..bb7271912 100644 --- a/src/Listener/PodeListener.cs +++ b/src/Listener/PodeListener.cs @@ -265,27 +265,27 @@ public override void Start() protected override void Close() { // close existing contexts - PodeHelpers.WriteErrorMessage($"Closing contexts", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing contexts", this, PodeLoggingLevel.Verbose); foreach (var _context in Contexts.ToArray()) { _context.Dispose(true); } Contexts.Clear(); - PodeHelpers.WriteErrorMessage($"Closed contexts", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed contexts", this, PodeLoggingLevel.Verbose); // close connected signals - PodeHelpers.WriteErrorMessage($"Closing signals", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing signals", this, PodeLoggingLevel.Verbose); foreach (var _signal in Signals.Values.ToArray()) { _signal.Dispose(); } Signals.Clear(); - PodeHelpers.WriteErrorMessage($"Closed signals", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed signals", this, PodeLoggingLevel.Verbose); // close connected server events - PodeHelpers.WriteErrorMessage($"Closing server events", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing server events", this, PodeLoggingLevel.Verbose); foreach (var _sseName in ServerEvents.Values.ToArray()) { foreach (var _sse in _sseName.Values.ToArray()) @@ -297,17 +297,17 @@ protected override void Close() } ServerEvents.Clear(); - PodeHelpers.WriteErrorMessage($"Closed server events", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed server events", this, PodeLoggingLevel.Verbose); // shutdown the sockets - PodeHelpers.WriteErrorMessage($"Closing sockets", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing sockets", this, PodeLoggingLevel.Verbose); for (var i = Sockets.Count - 1; i >= 0; i--) { Sockets[i].Dispose(); } Sockets.Clear(); - PodeHelpers.WriteErrorMessage($"Closed sockets", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed sockets", this, PodeLoggingLevel.Verbose); } } } \ No newline at end of file diff --git a/src/Listener/PodeLogger.cs b/src/Listener/PodeLogger.cs new file mode 100644 index 000000000..09483cf4b --- /dev/null +++ b/src/Listener/PodeLogger.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Concurrent; +using System.Collections; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Pode +{ + public static class PodeLogger + { + /// + /// The name used for the request logger. + /// + public const string RequestLogName = "__pode_log_requests__"; + + /// + /// The name used for the default logger. + /// + public const string DefaultLogName = "__pode_log_Defaults__"; + + /// + /// The name used for the error logger. + /// + public const string ErrorLogName = "__pode_log_errors__"; + + /// + /// The name used for the error logger. + /// + public const string ListenerLogName = "__pode_log_Listener__"; + + + // Static fields to control logging and store log entries in a thread-safe queue + private static bool _enabled; + private static ConcurrentQueue _queue; + + /// + /// Enables or disables writing logs to the console. + /// + public static bool Terminal { get; set; } + + /// + /// Enables or disables logging. Initializes or clears the queue based on the value. + /// + public static bool Enabled + { + get => _enabled; + set + { + _enabled = value; + if (_enabled) + { + // Initializes the queue for logging + _queue = new ConcurrentQueue(); + } + else + { + // Clears the queue if logging is disabled + _queue = null; + } + } + } + + /// + /// Gets the count of items in the log queue. + /// + public static int Count => _queue != null ? _queue.Count : 0; + + /// + /// Adds a log entry to the queue. + /// + /// The log entry as a Hashtable. + public static void Enqueue(Hashtable table) + { + if (_queue != null) + { + _queue.Enqueue(table); + } + } + + /// + /// Attempts to dequeue a log entry from the queue. + /// + /// The dequeued log entry. + /// True if a log entry was dequeued, false otherwise. + public static bool TryDequeue(out Hashtable table) + { + if (_queue != null) + { + return _queue.TryDequeue(out table); + } + table = null; + return false; + } + + /// + /// Dequeues a log entry from the queue. Returns null if the queue is empty. + /// + /// The dequeued log entry as a Hashtable. + public static Hashtable Dequeue() + { + if (_queue != null && _queue.TryDequeue(out Hashtable table)) + { + return table; + } + return null; + } + + /// + /// Clears all entries from the log queue. + /// + public static void Clear() + { + if (_queue != null) + { + while (_queue.TryDequeue(out _)) { } + } + } + + /// + /// Logs an exception by adding it to the queue and optionally writing it to the console. + /// + /// The exception to log. + /// Optional PodeConnector to control logging based on settings. + /// The logging level (default is Error). + public static void LogException(Exception ex, PodeConnector connector = default(PodeConnector), PodeLoggingLevel level = PodeLoggingLevel.Error) + { + if (ex == default(Exception)) + { + return; + } + + // Exit if logging is disabled or the logging level isn’t configured in the connector + if (connector != default(PodeConnector) && (!connector.ErrorLoggingEnabled || !connector.ErrorLoggingLevels.Contains(level.ToString(), StringComparer.InvariantCultureIgnoreCase))) + { + return; + } + + // If Terminal logging is enabled, output exception details to the console + if (Terminal) + { + Console.WriteLine($"[{level}] {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine(string.IsNullOrEmpty(ex.StackTrace) ? " [No Stack Trace]" : ex.StackTrace); + + if (ex.InnerException != null) + { + Console.WriteLine($"[{level}] {ex.InnerException.GetType().Name}: {ex.InnerException.Message}"); + Console.WriteLine(string.IsNullOrEmpty(ex.InnerException.StackTrace) ? " [No Stack Trace]" : ex.InnerException.StackTrace); + } + } + + // Add the exception to the log queue if logging is enabled + if (Enabled) + { + Hashtable logEntry = new Hashtable + { + ["Name"] = ListenerLogName, + ["Item"] = ex + }; + + Enqueue(logEntry); + } + } + + /// + /// Logs a message by adding it to the queue and optionally writing it to the console. + /// + /// The message to log. + /// Optional PodeConnector to control logging based on settings. + /// The logging level (default is Error). + /// Optional PodeContext to include context ID in the log entry. + public static void LogMessage(string message, PodeConnector connector = default(PodeConnector), PodeLoggingLevel level = PodeLoggingLevel.Error, PodeContext context = default(PodeContext)) + { + // Exit if message is empty or whitespace + if (string.IsNullOrWhiteSpace(message)) + { + return; + } + + // Exit if logging is disabled or the level isn’t configured in the connector + if (connector != default(PodeConnector) && (!connector.ErrorLoggingEnabled || !connector.ErrorLoggingLevels.Contains(level.ToString(), StringComparer.InvariantCultureIgnoreCase))) + { + return; + } + + // If Terminal logging is enabled, output message to the console, including context ID if provided + if (Terminal) + { + if (context == default(PodeContext)) + { + Console.WriteLine($"[{level}]: {message}"); + } + else + { + Console.WriteLine($"[{level}]: [ContextId: {context.ID}] {message}"); + } + } + + // Add the log message to the log queue if logging is enabled + if (Enabled) + { + Hashtable logEntry = new Hashtable + { + ["Name"] = ListenerLogName, + ["Item"] = new Hashtable + { + ["Message"] = message, + ["Level"] = level, + ["ThreadId"] = Environment.CurrentManagedThreadId + } + }; + + // Add the context ID to the log entry if a context is provided + if (context != null) + { + ((Hashtable)logEntry["Item"])["TargetObject"] = context.ID; + } + + Enqueue(logEntry); + } + } + + /// + /// Masks sensitive information in a log item based on specified regex patterns. + /// + /// The log item to mask. + /// A Hashtable containing masking patterns and a mask character. + /// The masked log item as a string. + public static string ProtectLogItem(string item, Hashtable masking) + { + // Exit if there are no masking patterns or if the mask value is null, empty, or missing + if (masking == null || masking.Count == 0 || !masking.ContainsKey("Mask") || string.IsNullOrEmpty(masking["Mask"]?.ToString())) + { + return item; + } + + // Retrieve the mask character and patterns from the masking hashtable + string mask = masking["Mask"].ToString(); + object[] patterns = (object[])masking["Patterns"]; + + // Apply each regex pattern to the log item + foreach (string regexPattern in patterns.Cast()) + { + Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase); + Match match = regex.Match(item); + + if (match.Success) + { + // Check for keep_before and keep_after groups in the match to retain surrounding text + if (match.Groups["keep_before"].Success && match.Groups["keep_after"].Success) + { + item = regex.Replace(item, $"{match.Groups["keep_before"].Value}{mask}{match.Groups["keep_after"].Value}"); + } + else if (match.Groups["keep_before"].Success) + { + item = regex.Replace(item, $"{match.Groups["keep_before"].Value}{mask}"); + } + else if (match.Groups["keep_after"].Success) + { + item = regex.Replace(item, $"{mask}{match.Groups["keep_after"].Value}"); + } + else + { + item = regex.Replace(item, mask); + } + } + } + + return item; + } + } +} diff --git a/src/Listener/PodeReceiver.cs b/src/Listener/PodeReceiver.cs index 21cff8f2c..95372e640 100644 --- a/src/Listener/PodeReceiver.cs +++ b/src/Listener/PodeReceiver.cs @@ -93,7 +93,7 @@ public void RemoveProcessingWebSocketRequest(PodeWebSocketRequest request) protected override void Close() { // disconnect websockets - PodeHelpers.WriteErrorMessage($"Closing client web sockets", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing client web sockets", this, PodeLoggingLevel.Verbose); foreach (var _webSocket in WebSockets.Values.ToArray()) { @@ -101,10 +101,10 @@ protected override void Close() } WebSockets.Clear(); - PodeHelpers.WriteErrorMessage($"Closed client web sockets", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed client web sockets", this, PodeLoggingLevel.Verbose); // close existing websocket requests - PodeHelpers.WriteErrorMessage($"Closing client web sockets requests", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing client web sockets requests", this, PodeLoggingLevel.Verbose); foreach (var _req in Requests.ToArray()) { @@ -112,7 +112,7 @@ protected override void Close() } Requests.Clear(); - PodeHelpers.WriteErrorMessage($"Closed client web requests", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed client web requests", this, PodeLoggingLevel.Verbose); } } } \ No newline at end of file diff --git a/src/Listener/PodeRequest.cs b/src/Listener/PodeRequest.cs index f8625735e..6cc815b5c 100644 --- a/src/Listener/PodeRequest.cs +++ b/src/Listener/PodeRequest.cs @@ -138,7 +138,7 @@ public async Task Open(CancellationToken cancellationToken) } else { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Debug); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Debug); } State = PodeStreamState.Error; @@ -187,7 +187,7 @@ await ssl.AuthenticateAsServerAsync(Certificate, AllowClientCertificate, Protoco } catch (Exception ex) when (ex is OperationCanceledException || ex is IOException || ex is ObjectDisposedException) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Verbose); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Verbose); ssl?.Dispose(); State = PodeStreamState.Error; Error = new PodeRequestException(ex, 500); @@ -195,14 +195,14 @@ await ssl.AuthenticateAsServerAsync(Certificate, AllowClientCertificate, Protoco catch (AuthenticationException ex) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Debug); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Debug); ssl?.Dispose(); State = PodeStreamState.Error; Error = new PodeRequestException(ex, 400); } catch (Exception ex) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Error); ssl?.Dispose(); State = PodeStreamState.Error; Error = new PodeRequestException(ex, 502); @@ -287,7 +287,7 @@ public async Task Receive(CancellationToken cancellationToken) { if (Context.Listener.IsConnected) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Debug); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Debug); } break; } @@ -324,20 +324,20 @@ public async Task Receive(CancellationToken cancellationToken) } catch (OperationCanceledException ex) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Verbose); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Verbose); } catch (IOException ex) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Verbose); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Verbose); } catch (PodeRequestException ex) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Error); Error = ex; } catch (Exception ex) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Error); Error = new PodeRequestException(ex, 500); } finally @@ -481,7 +481,7 @@ public virtual void PartialDispose() } catch (Exception ex) { - PodeHelpers.WriteException(ex, Context.Listener, PodeLoggingLevel.Error); + PodeLogger.LogException(ex, Context.Listener, PodeLoggingLevel.Error); } } @@ -511,6 +511,7 @@ protected virtual void Dispose(bool disposing) } PartialDispose(); + PodeLogger.LogMessage($"Request disposed", Context.Listener, PodeLoggingLevel.Verbose, Context); } } diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs index bd4e54e13..9cfe66758 100644 --- a/src/Listener/PodeResponse.cs +++ b/src/Listener/PodeResponse.cs @@ -94,13 +94,13 @@ public async Task Send() return; } - PodeHelpers.WriteErrorMessage($"Sending response", Context.Listener, PodeLoggingLevel.Verbose, Context); + PodeLogger.LogMessage($"Sending response", Context.Listener, PodeLoggingLevel.Verbose, Context); try { await SendHeaders(Context.IsTimeout).ConfigureAwait(false); await SendBody(Context.IsTimeout).ConfigureAwait(false); - PodeHelpers.WriteErrorMessage($"Response sent", Context.Listener, PodeLoggingLevel.Verbose, Context); + PodeLogger.LogMessage($"Response sent", Context.Listener, PodeLoggingLevel.Verbose, Context); } catch (OperationCanceledException) { } catch (IOException) { } @@ -110,7 +110,7 @@ public async Task Send() } catch (Exception ex) { - PodeHelpers.WriteException(ex, Context.Listener); + PodeLogger.LogException(ex, Context.Listener); throw; } finally @@ -126,13 +126,13 @@ public async Task SendTimeout() return; } - PodeHelpers.WriteErrorMessage($"Sending response timed-out", Context.Listener, PodeLoggingLevel.Verbose, Context); + PodeLogger.LogMessage($"Sending response timed-out", Context.Listener, PodeLoggingLevel.Verbose, Context); StatusCode = 408; try { await SendHeaders(true).ConfigureAwait(false); - PodeHelpers.WriteErrorMessage($"Response timed-out sent", Context.Listener, PodeLoggingLevel.Verbose, Context); + PodeLogger.LogMessage($"Response timed-out sent", Context.Listener, PodeLoggingLevel.Verbose, Context); } catch (OperationCanceledException) { } catch (IOException) { } @@ -142,7 +142,7 @@ public async Task SendTimeout() } catch (Exception ex) { - PodeHelpers.WriteException(ex, Context.Listener); + PodeLogger.LogException(ex, Context.Listener); throw; } finally @@ -395,7 +395,7 @@ public async Task Write(byte[] buffer, bool flush = false) } catch (Exception ex) { - PodeHelpers.WriteException(ex, Context.Listener); + PodeLogger.LogException(ex, Context.Listener); throw; } } @@ -515,7 +515,7 @@ public void Dispose() OutputStream = default; } - PodeHelpers.WriteErrorMessage($"Response disposed", Context.Listener, PodeLoggingLevel.Verbose, Context); + PodeLogger.LogMessage($"Response disposed", Context.Listener, PodeLoggingLevel.Verbose, Context); } } } \ No newline at end of file diff --git a/src/Listener/PodeResponseHeaders.cs b/src/Listener/PodeResponseHeaders.cs index e619a5a86..f45eb259b 100644 --- a/src/Listener/PodeResponseHeaders.cs +++ b/src/Listener/PodeResponseHeaders.cs @@ -3,15 +3,29 @@ namespace Pode { + /// + /// Represents a collection of response headers that supports multiple values per header. + /// public class PodeResponseHeaders { + /// + /// Gets or sets the first value of the specified header. + /// + /// The name of the header. public object this[string name] { - get => (Headers.ContainsKey(name) ? Headers[name][0] : string.Empty); + get => Headers.TryGetValue(name, out IList value) ? value[0] : string.Empty; set => Set(name, value); } + /// + /// Gets the number of headers. + /// public int Count => Headers.Count; + + /// + /// Gets the collection of header names. + /// public ICollection Keys => Headers.Keys; private IDictionary> Headers; @@ -21,37 +35,54 @@ public PodeResponseHeaders() Headers = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); } + /// + /// Determines whether the collection contains the specified header. + /// public bool ContainsKey(string name) { return Headers.ContainsKey(name); } + /// + /// Gets the list of values associated with the specified header. + /// public IList Get(string name) { - return Headers.ContainsKey(name) ? Headers[name] : default(IList); + return Headers.TryGetValue(name, out IList value) ? value : default(IList); } + /// + /// Sets the specified header to the provided value, replacing any existing values. + /// public void Set(string name, object value) { - if (!Headers.ContainsKey(name)) + if (!Headers.TryGetValue(name, out var list)) { - Headers.Add(name, new List()); + list = new List(); + Headers[name] = list; } Headers[name].Clear(); Headers[name].Add(value); } + /// + /// Adds a value to the specified header, preserving any existing values. + /// public void Add(string name, object value) { - if (!Headers.ContainsKey(name)) + if (!Headers.TryGetValue(name, out var list)) { - Headers.Add(name, new List()); + list = new List(); + Headers[name] = list; } - Headers[name].Add(value); + list.Add(value); } + /// + /// Removes the specified header. + /// public void Remove(string name) { if (Headers.ContainsKey(name)) @@ -60,6 +91,9 @@ public void Remove(string name) } } + /// + /// Clears all headers. + /// public void Clear() { Headers.Clear(); diff --git a/src/Listener/PodeSignalRequest.cs b/src/Listener/PodeSignalRequest.cs index 2848b891e..8265d3c64 100644 --- a/src/Listener/PodeSignalRequest.cs +++ b/src/Listener/PodeSignalRequest.cs @@ -184,7 +184,7 @@ protected override async Task Parse(byte[] bytes, CancellationToken cancel catch (Exception ex) { // Log the error and return false to indicate failure. - PodeHelpers.WriteErrorMessage($"Error decoding WebSocket frame: {ex.Message}", Context.Listener, PodeLoggingLevel.Error, Context); + PodeLogger.LogMessage($"Error decoding WebSocket frame: {ex.Message}", Context.Listener, PodeLoggingLevel.Error, Context); throw; } finally @@ -252,7 +252,7 @@ protected override void Dispose(bool disposing) if (disposing) { // Log a message indicating the WebSocket is being closed. - PodeHelpers.WriteErrorMessage($"Closing Websocket", Context.Listener, PodeLoggingLevel.Verbose, Context); + PodeLogger.LogMessage($"Closing Websocket", Context.Listener, PodeLoggingLevel.Verbose, Context); // Send a Close frame to the client and wait for the operation to complete. Context.Response.WriteFrame(string.Empty, PodeWsOpCode.Close).Wait(); diff --git a/src/Listener/PodeSmtpRequest.cs b/src/Listener/PodeSmtpRequest.cs index 1c3bfca08..b4f458edc 100644 --- a/src/Listener/PodeSmtpRequest.cs +++ b/src/Listener/PodeSmtpRequest.cs @@ -265,7 +265,7 @@ protected override async Task Parse(byte[] bytes, CancellationToken cancel public void Reset() { - PodeHelpers.WriteErrorMessage($"Request reset", Context.Listener, PodeLoggingLevel.Verbose, Context); + PodeLogger.LogMessage($"Request reset", Context.Listener, PodeLoggingLevel.Verbose, Context); _canProcess = false; Headers = new Hashtable(StringComparer.InvariantCultureIgnoreCase); diff --git a/src/Listener/PodeSocket.cs b/src/Listener/PodeSocket.cs index 335166558..ea52bd24e 100644 --- a/src/Listener/PodeSocket.cs +++ b/src/Listener/PodeSocket.cs @@ -180,7 +180,7 @@ private async Task StartReceive(Socket acceptedSocket) // Create the context for the connection. var context = new PodeContext(acceptedSocket, this, Listener); - PodeHelpers.WriteErrorMessage($"Opening Receive", Listener, PodeLoggingLevel.Verbose, context); + PodeLogger.LogMessage($"Opening Receive", Listener, PodeLoggingLevel.Verbose, context); // Initialize the context. await context.Initialise().ConfigureAwait(false); @@ -200,7 +200,7 @@ private async Task StartReceive(Socket acceptedSocket) /// The context to start receiving for. public void StartReceive(PodeContext context) { - PodeHelpers.WriteErrorMessage($"Starting Receive", Listener, PodeLoggingLevel.Verbose, context); + PodeLogger.LogMessage($"Starting Receive", Listener, PodeLoggingLevel.Verbose, context); try { @@ -210,12 +210,12 @@ public void StartReceive(PodeContext context) catch (OperationCanceledException ex) { // Handle cancellation. - PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Verbose); + PodeLogger.LogException(ex, Listener, PodeLoggingLevel.Verbose); } catch (IOException ex) { // Handle I/O exceptions. - PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Verbose); + PodeLogger.LogException(ex, Listener, PodeLoggingLevel.Verbose); } catch (AggregateException aex) { @@ -226,7 +226,7 @@ public void StartReceive(PodeContext context) catch (Exception ex) { // Handle any other exceptions. - PodeHelpers.WriteException(ex, Listener); + PodeLogger.LogException(ex, Listener); context.Socket.Close(); } } @@ -250,7 +250,7 @@ private void ProcessAccept(SocketAsyncEventArgs args) { if (error != SocketError.Success) { - PodeHelpers.WriteErrorMessage($"Closing accepting socket: {error}", Listener, PodeLoggingLevel.Debug); + PodeLogger.LogMessage($"Closing accepting socket: {error}", Listener, PodeLoggingLevel.Debug); } // Close socket if it was accepted but there's an error. @@ -269,12 +269,12 @@ private void ProcessAccept(SocketAsyncEventArgs args) catch (OperationCanceledException ex) { // Handle cancellation. - PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Verbose); + PodeLogger.LogException(ex, Listener, PodeLoggingLevel.Verbose); } catch (IOException ex) { // Handle I/O exceptions. - PodeHelpers.WriteException(ex, Listener, PodeLoggingLevel.Verbose); + PodeLogger.LogException(ex, Listener, PodeLoggingLevel.Verbose); } catch (AggregateException aex) { @@ -284,7 +284,7 @@ private void ProcessAccept(SocketAsyncEventArgs args) catch (Exception ex) { // Handle any other exceptions. - PodeHelpers.WriteException(ex, Listener); + PodeLogger.LogException(ex, Listener); } } @@ -310,7 +310,7 @@ public async Task HandleContext(PodeContext context) // Check if the request is aborted with a non-StatusCode of 408 (Request Timeout). if (context.Request.IsAborted) { - PodeHelpers.WriteException(context.Request.Error, Listener, context.Request.Error.LoggingLevel); + PodeLogger.LogException(context.Request.Error, Listener, context.Request.Error.LoggingLevel); } context.Dispose(true); @@ -352,13 +352,13 @@ public async Task HandleContext(PodeContext context) { if (context.IsWebSocket) { - PodeHelpers.WriteErrorMessage($"Received client signal", Listener, PodeLoggingLevel.Verbose, context); + PodeLogger.LogMessage($"Received client signal", Listener, PodeLoggingLevel.Verbose, context); Listener.AddClientSignal(context.SignalRequest.NewClientSignal()); context.Dispose(); } else { - PodeHelpers.WriteErrorMessage($"Received request", Listener, PodeLoggingLevel.Verbose, context); + PodeLogger.LogMessage($"Received request", Listener, PodeLoggingLevel.Verbose, context); Listener.AddContext(context); } } @@ -366,7 +366,7 @@ public async Task HandleContext(PodeContext context) catch (Exception ex) { // Log any exceptions that occur while handling the context. - PodeHelpers.WriteException(ex, Listener); + PodeLogger.LogException(ex, Listener); } } @@ -470,7 +470,7 @@ public void Dispose() } catch (Exception ex) { - PodeHelpers.WriteException(ex, Listener); + PodeLogger.LogException(ex, Listener); } } finally diff --git a/src/Listener/PodeTcpRequest.cs b/src/Listener/PodeTcpRequest.cs index debd03b94..749ce6925 100644 --- a/src/Listener/PodeTcpRequest.cs +++ b/src/Listener/PodeTcpRequest.cs @@ -66,7 +66,7 @@ protected override Task Parse(byte[] bytes, CancellationToken cancellation public void Reset() { - PodeHelpers.WriteErrorMessage($"Request reset", Context.Listener, PodeLoggingLevel.Verbose, Context); + PodeLogger.LogMessage($"Request reset", Context.Listener, PodeLoggingLevel.Verbose, Context); _body = string.Empty; RawBody = default; } diff --git a/src/Listener/PodeWatcher.cs b/src/Listener/PodeWatcher.cs index ed3134cb6..2307ba638 100644 --- a/src/Listener/PodeWatcher.cs +++ b/src/Listener/PodeWatcher.cs @@ -52,7 +52,7 @@ public override void Start() protected override void Close() { // dispose watchers - PodeHelpers.WriteErrorMessage($"Closing file watchers", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing file watchers", this, PodeLoggingLevel.Verbose); foreach (var _watcher in FileWatchers.ToArray()) { @@ -60,10 +60,10 @@ protected override void Close() } FileWatchers.Clear(); - PodeHelpers.WriteErrorMessage($"Closed file watchers", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed file watchers", this, PodeLoggingLevel.Verbose); // dispose existing file events - PodeHelpers.WriteErrorMessage($"Closing file events", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing file events", this, PodeLoggingLevel.Verbose); foreach (var _evt in FileEvents.ToArray()) { @@ -71,7 +71,7 @@ protected override void Close() } FileEvents.Clear(); - PodeHelpers.WriteErrorMessage($"Closed file events", this, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed file events", this, PodeLoggingLevel.Verbose); } } } \ No newline at end of file diff --git a/src/Listener/PodeWebSocket.cs b/src/Listener/PodeWebSocket.cs index 87cf4ed79..71f91ba99 100644 --- a/src/Listener/PodeWebSocket.cs +++ b/src/Listener/PodeWebSocket.cs @@ -105,7 +105,7 @@ public async Task Receive() catch (IOException) { } catch (WebSocketException ex) { - PodeHelpers.WriteException(ex, Receiver, PodeLoggingLevel.Debug); + PodeLogger.LogException(ex, Receiver, PodeLoggingLevel.Debug); Dispose(); } finally @@ -139,7 +139,7 @@ public async Task Disconnect(PodeWebSocketCloseFrom closeFrom) if (IsConnected) { - PodeHelpers.WriteErrorMessage($"Closing client web socket: {Name}", Receiver, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closing client web socket: {Name}", Receiver, PodeLoggingLevel.Verbose); // only close output in client closing if (closeFrom == PodeWebSocketCloseFrom.Client) @@ -153,12 +153,12 @@ public async Task Disconnect(PodeWebSocketCloseFrom closeFrom) await WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); } - PodeHelpers.WriteErrorMessage($"Closed client web socket: {Name}", Receiver, PodeLoggingLevel.Verbose); + PodeLogger.LogMessage($"Closed client web socket: {Name}", Receiver, PodeLoggingLevel.Verbose); } WebSocket.Dispose(); - WebSocket = default; - PodeHelpers.WriteErrorMessage($"Disconnected client web socket: {Name}", Receiver, PodeLoggingLevel.Verbose); + WebSocket = default(ClientWebSocket); + PodeLogger.LogMessage($"Disconnected client web socket: {Name}", Receiver, PodeLoggingLevel.Verbose); } public void Dispose() diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index d9617f469..2810e39d3 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "لم يتم توفير معلمة باسم '{0}' في الطلب أو لا توجد بيانات متاحة." cacheStorageNotFoundForSetExceptionMessage = "لم يتم العثور على مخزن ذاكرة التخزين المؤقت بالاسم '{0}' عند محاولة تعيين العنصر المخزن مؤقتًا '{1}'" methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: تم التعريف بالفعل.' - errorLoggingAlreadyEnabledExceptionMessage = 'تم تمكين تسجيل الأخطاء بالفعل.' valueForUsingVariableNotFoundExceptionMessage = "لم يتم العثور على قيمة لـ '`$using:{0}'." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'أداة الوثائق RapidPdf لا تدعم OpenAPI 3.1' oauth2ClientSecretRequiredExceptionMessage = 'تتطلب OAuth2 سر العميل عند عدم استخدام PKCE.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "تتطلب الخطة '{0}' المقدمة لمحقق المصادقة '{1}' ScriptBlock صالح." sseFailedToBroadcastExceptionMessage = 'فشل بث SSE بسبب مستوى البث SSE المحدد لـ {0}: {1}' adModuleWindowsOnlyExceptionMessage = 'وحدة Active Directory متاحة فقط على نظام Windows.' - requestLoggingAlreadyEnabledExceptionMessage = 'تم تمكين تسجيل الطلبات بالفعل.' invalidAccessControlMaxAgeDurationExceptionMessage = 'مدة Access-Control-Max-Age غير صالحة المقدمة: {0}. يجب أن تكون أكبر من 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'تعريف OpenAPI باسم {0} موجود بالفعل.' renamePodeOADefinitionTagExceptionMessage = "لا يمكن استخدام Rename-PodeOADefinitionTag داخل Select-PodeOADefinition 'ScriptBlock'." + loggingAlreadyEnabledExceptionMessage = "تم تمكين تسجيل '{0}' بالفعل." + invalidEncodingExceptionMessage = 'ترميز غير صالح: {0}' + syslogProtocolExceptionMessage = 'يمكن لبروتوكول Syslog استخدام RFC3164 أو RFC5424 فقط.' taskProcessDoesNotExistExceptionMessage = 'عملية المهمة غير موجودة: {0}' scheduleProcessDoesNotExistExceptionMessage = 'عملية الجدول الزمني غير موجودة: {0}' definitionTagChangeNotAllowedExceptionMessage = 'لا يمكن تغيير علامة التعريف لمسار.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات." unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}' localEndpointConflictExceptionMessage = "تم تعريف كل من '{0}' و '{1}' كنقاط نهاية محلية لـ OpenAPI، لكن يُسمح فقط بنقطة نهاية محلية واحدة لكل تعريف API." + deprecatedFunctionWarningMessage = "تحذير: الدالة '{0}' مهملة وستتم إزالتها في الإصدارات المستقبلية. يُرجى استخدام الدالة '{1}' بدلاً منها." suspendingMessage = 'تعليق' resumingMessage = 'استئناف' serverControlCommandsTitle = 'أوامر التحكم بالخادم:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = 'قاعدة الحد الأقصى للمعدل غير موجودة: {0}' accessLimitRuleAlreadyExistsExceptionMessage = 'تم تعريف قاعدة الحد الأقصى للوصول بالفعل: {0}' accessLimitRuleDoesNotExistExceptionMessage = 'قاعدة الحد الأقصى للوصول غير موجودة: {0}' + loggerDoesNotExistExceptionMessage = "المسجل '{0}' غير موجود." } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index d85929f70..7db360d19 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "Ein Parameter namens '{0}' wurde in der Anfrage nicht angegeben oder es sind keine Daten verfügbar." cacheStorageNotFoundForSetExceptionMessage = "Der Cache-Speicher mit dem Namen '{0}' wurde nicht gefunden, als versucht wurde, das zwischengespeicherte Element '{1}' zu setzen." methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Bereits definiert.' - errorLoggingAlreadyEnabledExceptionMessage = 'Die Fehlerprotokollierung wurde bereits aktiviert.' valueForUsingVariableNotFoundExceptionMessage = "Der Wert für '`$using:{0}' konnte nicht gefunden werden." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'Das Dokumentationstool RapidPdf unterstützt OpenAPI 3.1 nicht.' oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 erfordert ein Client Secret, wenn PKCE nicht verwendet wird.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "Das bereitgestellte '{0}'-Schema für den Authentifizierungsvalidator '{1}' erfordert einen gültigen ScriptBlock." sseFailedToBroadcastExceptionMessage = 'SSE konnte aufgrund des definierten SSE-Broadcast-Levels für {0}: {1} nicht übertragen werden.' adModuleWindowsOnlyExceptionMessage = 'Active Directory-Modul nur unter Windows verfügbar.' - requestLoggingAlreadyEnabledExceptionMessage = 'Die Anforderungsprotokollierung wurde bereits aktiviert.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Ungültige Access-Control-Max-Age-Dauer angegeben: {0}. Sollte größer als 0 sein.' openApiDefinitionAlreadyExistsExceptionMessage = 'Die OpenAPI-Definition mit dem Namen {0} existiert bereits.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag kann nicht innerhalb eines 'ScriptBlock' von Select-PodeOADefinition verwendet werden." + loggingAlreadyEnabledExceptionMessage = "Das Logging '{0}' wurde bereits aktiviert." + invalidEncodingExceptionMessage = 'Ungültige Codierung: {0}' + syslogProtocolExceptionMessage = 'Das Syslog-Protokoll kann nur RFC3164 oder RFC5424 verwenden.' taskProcessDoesNotExistExceptionMessage = "Der Aufgabenprozess '{0}' existiert nicht." scheduleProcessDoesNotExistExceptionMessage = "Der Aufgabenplanerprozess '{0}' existiert nicht." definitionTagChangeNotAllowedExceptionMessage = 'Definitionstag für eine Route kann nicht geändert werden.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Die Funktion '{0}' akzeptiert kein Array als Pipeline-Eingabe." unsupportedStreamCompressionEncodingExceptionMessage = 'Die Stream-Komprimierungskodierung wird nicht unterstützt: {0}' localEndpointConflictExceptionMessage = "Sowohl '{0}' als auch '{1}' sind als lokale OpenAPI-Endpunkte definiert, aber es ist nur ein lokaler Endpunkt pro API-Definition erlaubt." + deprecatedFunctionWarningMessage = "WARNUNG: Die Funktion '{0}' ist veraltet und wird in zukünftigen Versionen entfernt. Bitte verwenden Sie stattdessen die Funktion '{1}'." suspendingMessage = 'Anhalten' resumingMessage = 'Fortsetzen' serverControlCommandsTitle = 'Serversteuerbefehle:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "Die Rate-Limit-Regel mit dem Namen '{0}' existiert nicht." accessLimitRuleAlreadyExistsExceptionMessage = "Die Zugriffsbeschränkungsregel mit dem Namen '{0}' existiert bereits." accessLimitRuleDoesNotExistExceptionMessage = "Die Zugriffsbeschränkungsregel mit dem Namen '{0}' existiert nicht." + loggerDoesNotExistExceptionMessage = "Logger '{0}' existiert nicht." } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index c2ac6d2b0..d45f4a654 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "A parameter called '{0}' was not supplied in the request or has no data available." cacheStorageNotFoundForSetExceptionMessage = "Cache storage with name '{0}' not found when attempting to set cached item '{1}'" methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Already defined.' - errorLoggingAlreadyEnabledExceptionMessage = 'Error Logging has already been enabled.' valueForUsingVariableNotFoundExceptionMessage = "Value for '`$using:{0}' could not be found." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = "The Document tool RapidPdf doesn't support OpenAPI 3.1" oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 requires a Client Secret when not using PKCE.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "The supplied '{0}' Scheme for the '{1}' authentication validator requires a valid ScriptBlock." sseFailedToBroadcastExceptionMessage = 'SSE failed to broadcast due to defined SSE broadcast level for {0}: {1}' adModuleWindowsOnlyExceptionMessage = 'Active Directory module only available on Windows OS.' - requestLoggingAlreadyEnabledExceptionMessage = 'Request Logging has already been enabled.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Invalid Access-Control-Max-Age duration supplied: {0}. Should be greater than 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." + loggingAlreadyEnabledExceptionMessage = "Logging '{0}' has already been enabled." + invalidEncodingExceptionMessage = 'Invalid encoding: {0}' + syslogProtocolExceptionMessage = 'The Syslog protocol can use only RFC3164 or RFC5424.' taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}' scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.' @@ -326,4 +327,6 @@ rateLimitRuleDoesNotExistExceptionMessage = "A rate limit rule with the name '{0}' does not exist." accessLimitRuleAlreadyExistsExceptionMessage = "An access limit rule with the name '{0}' already exists." accessLimitRuleDoesNotExistExceptionMessage = "An access limit rule with the name '{0}' does not exist." + deprecatedFunctionWarningMessage = "WARNING: The function '{0}' is deprecated and will be removed in future releases. Please use the '{1}' function instead." + loggerDoesNotExistExceptionMessage = "Logger '{0}' does not exist." } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index fc04db0d2..eb2e503d5 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -154,7 +154,6 @@ parameterNotSuppliedInRequestExceptionMessage = "A parameter called '{0}' was not supplied in the request or has no data available." cacheStorageNotFoundForSetExceptionMessage = "Cache storage with name '{0}' not found when attempting to set cached item '{1}'" methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Already defined.' - errorLoggingAlreadyEnabledExceptionMessage = 'Error Logging has already been enabled.' valueForUsingVariableNotFoundExceptionMessage = "Value for '`$using:{0}' could not be found." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = "The Document tool RapidPdf doesn't support OpenAPI 3.1" oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 requires a Client Secret when not using PKCE.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "The supplied '{0}' Scheme for the '{1}' authentication validator requires a valid ScriptBlock." sseFailedToBroadcastExceptionMessage = 'SSE failed to broadcast due to defined SSE broadcast level for {0}: {1}' adModuleWindowsOnlyExceptionMessage = 'Active Directory module only available on Windows OS.' - requestLoggingAlreadyEnabledExceptionMessage = 'Request Logging has already been enabled.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Invalid Access-Control-Max-Age duration supplied: {0}. Should be greater than 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." + loggingAlreadyEnabledExceptionMessage = "Logging '{0}' has already been enabled." + invalidEncodingExceptionMessage = 'Invalid encoding: {0}' + syslogProtocolExceptionMessage = 'The Syslog protocol can use only RFC3164 or RFC5424.' taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}' scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' localEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition." + deprecatedFunctionWarningMessage = "WARNING: The function '{0}' is deprecated and will be removed in future releases. Please use the '{1}' function instead." suspendingMessage = 'Suspending' resumingMessage = 'Resuming' serverControlCommandsTitle = 'Server Control Commands:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "A Rate Limit Rule with the name '{0}' does not exist." accessLimitRuleAlreadyExistsExceptionMessage = "An Access Limit Rule with the name '{0}' already exists." accessLimitRuleDoesNotExistExceptionMessage = "An Access Limit Rule with the name '{0}' does not exist." + loggerDoesNotExistExceptionMessage = "Logger '{0}' does not exist." } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index d7db64e6c..f86926a2f 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "No se ha proporcionado un parámetro llamado '{0}' en la solicitud o no hay datos disponibles." cacheStorageNotFoundForSetExceptionMessage = "No se encontró el almacenamiento en caché con el nombre '{0}' al intentar establecer el elemento en caché '{1}'." methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Ya está definido.' - errorLoggingAlreadyEnabledExceptionMessage = 'El registro de errores ya está habilitado.' valueForUsingVariableNotFoundExceptionMessage = "No se pudo encontrar el valor para '`$using:{0}'." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'La herramienta de documentación RapidPdf no admite OpenAPI 3.1' oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 requiere un Client Secret cuando no se usa PKCE.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "El esquema '{0}' proporcionado para el validador de autenticación '{1}' requiere un ScriptBlock válido." sseFailedToBroadcastExceptionMessage = 'SSE no pudo transmitir debido al nivel de transmisión SSE definido para {0}: {1}.' adModuleWindowsOnlyExceptionMessage = 'El módulo de Active Directory solo está disponible en Windows.' - requestLoggingAlreadyEnabledExceptionMessage = 'El registro de solicitudes ya está habilitado.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Duración inválida para Access-Control-Max-Age proporcionada: {0}. Debe ser mayor que 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La definición de OpenAPI con el nombre {0} ya existe.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag no se puede usar dentro de un 'ScriptBlock' de Select-PodeOADefinition." + loggingAlreadyEnabledExceptionMessage = "El registro '{0}' ya ha sido habilitado." + invalidEncodingExceptionMessage = 'Codificación inválida: {0}' + syslogProtocolExceptionMessage = 'El protocolo Syslog solo puede usar RFC3164 o RFC5424.' taskProcessDoesNotExistExceptionMessage = "El proceso de la tarea '{0}' no existe." scheduleProcessDoesNotExistExceptionMessage = "El proceso del programación '{0}' no existe." definitionTagChangeNotAllowedExceptionMessage = 'La etiqueta de definición para una Route no se puede cambiar.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La función '{0}' no acepta una matriz como entrada de canalización." unsupportedStreamCompressionEncodingExceptionMessage = 'La codificación de compresión de transmisión no es compatible: {0}' localEndpointConflictExceptionMessage = "Tanto '{0}' como '{1}' están definidos como puntos finales locales de OpenAPI, pero solo se permite un punto final local por definición de API." + deprecatedFunctionWarningMessage = "ADVERTENCIA: La función '{0}' está obsoleta y será eliminada en futuras versiones. Por favor, use la función '{1}' en su lugar." suspendingMessage = 'Suspendiendo' resumingMessage = 'Reanudando' serverControlCommandsTitle = 'Comandos de control del servidor:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "La regla de límite de velocidad con el nombre '{0}' no existe." accessLimitRuleAlreadyExistsExceptionMessage = "La regla de límite de acceso con el nombre '{0}' ya existe." accessLimitRuleDoesNotExistExceptionMessage = "La regla de límite de acceso con el nombre '{0}' no existe." + loggerDoesNotExistExceptionMessage = "El registrador '{0}' no existe." } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index a0d769eab..313ee151c 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "Un paramètre nommé '{0}' n'a pas été fourni dans la demande ou aucune donnée n'est disponible." cacheStorageNotFoundForSetExceptionMessage = "Le stockage de cache nommé '{0}' est introuvable lors de la tentative de définition de l'élément mis en cache '{1}'." methodPathAlreadyDefinedExceptionMessage = '[{0}] {1} : Déjà défini.' - errorLoggingAlreadyEnabledExceptionMessage = 'La journalisation des erreurs est déjà activée.' valueForUsingVariableNotFoundExceptionMessage = "Valeur pour '`$using:{0}' introuvable." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = "L'outil de documentation RapidPdf ne prend pas en charge OpenAPI 3.1" oauth2ClientSecretRequiredExceptionMessage = "OAuth2 nécessite un Client Secret lorsque PKCE n'est pas utilisé." @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "Le schéma '{0}' fourni pour le validateur d'authentification '{1}' nécessite un ScriptBlock valide." sseFailedToBroadcastExceptionMessage = 'SSE a échoué à diffuser en raison du niveau de diffusion SSE défini pour {0} : {1}.' adModuleWindowsOnlyExceptionMessage = 'Le module Active Directory est uniquement disponible sur Windows.' - requestLoggingAlreadyEnabledExceptionMessage = 'La journalisation des requêtes est déjà activée.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Durée Access-Control-Max-Age invalide fournie : {0}. Doit être supérieure à 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La définition OpenAPI nommée {0} existe déjà.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag ne peut pas être utilisé à l'intérieur d'un 'ScriptBlock' de Select-PodeOADefinition." + loggingAlreadyEnabledExceptionMessage = "La journalisation '{0}' a déjà été activée." + invalidEncodingExceptionMessage = 'Encodage invalide : {0}' + syslogProtocolExceptionMessage = 'Le protocole Syslog ne peut utiliser que RFC3164 ou RFC5424.' taskProcessDoesNotExistExceptionMessage = "Le processus de la tâche '{0}' n'existe pas." scheduleProcessDoesNotExistExceptionMessage = "Le processus de l'horaire '{0}' n'existe pas." definitionTagChangeNotAllowedExceptionMessage = 'Le tag de définition pour une Route ne peut pas être modifié.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La fonction '{0}' n'accepte pas un tableau en tant qu'entrée de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = "La compression de flux {0} n'est pas prise en charge." localEndpointConflictExceptionMessage = "Les deux '{0}' et '{1}' sont définis comme des points de terminaison locaux pour OpenAPI, mais un seul point de terminaison local est autorisé par définition d'API." + deprecatedFunctionWarningMessage = "AVERTISSEMENT : La fonction '{0}' est obsolète et sera supprimée dans les versions futures. Veuillez utiliser la fonction '{1}' à la place." suspendingMessage = 'Suspension' resumingMessage = 'Reprise' serverControlCommandsTitle = 'Commandes de contrôle du serveur :' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "La règle de limite de taux '{0}' n'existe pas." accessLimitRuleAlreadyExistsExceptionMessage = "Une règle de limite d'accès nommée '{0}' existe déjà." accessLimitRuleDoesNotExistExceptionMessage = "La règle de limite d'accès '{0}' n'existe pas." + loggerDoesNotExistExceptionMessage = "Le journaliseur '{0}' n'existe pas." } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 5dd3c47f4..30b7ce678 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "Un parametro chiamato '{0}' non è stato fornito nella richiesta o non ci sono dati disponibili." cacheStorageNotFoundForSetExceptionMessage = "Memoria cache con nome '{0}' non trovata durante il tentativo di impostare l'elemento memorizzato nella cache '{1}'." methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Già definito.' - errorLoggingAlreadyEnabledExceptionMessage = 'La registrazione degli errori è già abilitata.' valueForUsingVariableNotFoundExceptionMessage = "Impossibile trovare il valore per '`$using:{0}'." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'Lo strumento di documentazione RapidPdf non supporta OpenAPI 3.1' oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 richiede un Client Secret quando non si utilizza PKCE.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "Lo schema '{0}' fornito per il validatore di autenticazione '{1}' richiede uno ScriptBlock valido." sseFailedToBroadcastExceptionMessage = 'SSE non è riuscito a trasmettere a causa del livello di trasmissione SSE definito per {0}: {1}.' adModuleWindowsOnlyExceptionMessage = 'Il modulo Active Directory è disponibile solo su Windows OS.' - requestLoggingAlreadyEnabledExceptionMessage = 'La registrazione delle richieste è già abilitata.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Durata non valida fornita per Access-Control-Max-Age: {0}. Deve essere maggiore di 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La definizione OpenAPI denominata {0} esiste già.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag non può essere utilizzato all'interno di un 'ScriptBlock' di Select-PodeOADefinition." + loggingAlreadyEnabledExceptionMessage = "Il logging '{0}' è già stato abilitato." + invalidEncodingExceptionMessage = 'Codifica non valida: {0}' + syslogProtocolExceptionMessage = 'Il protocollo Syslog può utilizzare solo RFC3164 o RFC5424.' taskProcessDoesNotExistExceptionMessage = "Il processo dell'attività '{0}' non esiste." scheduleProcessDoesNotExistExceptionMessage = "Il processo della programma '{0}' non esiste." definitionTagChangeNotAllowedExceptionMessage = 'Il tag di definizione per una Route non può essere cambiato.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La funzione '{0}' non accetta una matrice come input della pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'La compressione dello stream non è supportata per la codifica {0}' localEndpointConflictExceptionMessage = "Sia '{0}' che '{1}' sono definiti come endpoint locali OpenAPI, ma è consentito solo un endpoint locale per definizione API." + deprecatedFunctionWarningMessage = "AVVISO: La funzione '{0}' è obsoleta e verrà rimossa nelle versioni future. Si prega di utilizzare la funzione '{1}' al suo posto." suspendingMessage = 'Sospensione' resumingMessage = 'Ripresa' serverControlCommandsTitle = 'Comandi di controllo del server:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "La regola di limitazione del tasso con il nome '{0}' non esiste." accessLimitRuleAlreadyExistsExceptionMessage = "Una regola di limitazione dell'accesso con il nome '{0}' esiste già." accessLimitRuleDoesNotExistExceptionMessage = "La regola di limitazione dell'accesso con il nome '{0}' non esiste." + loggerDoesNotExistExceptionMessage = "Il logger '{0}' non esiste." } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 43d8243bc..becba3aad 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "リクエストに '{0}' という名前のパラメータが提供されていないか、データがありません。" cacheStorageNotFoundForSetExceptionMessage = "キャッシュされたアイテム '{1}' を設定しようとしたときに、名前 '{0}' のキャッシュストレージが見つかりません。" methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: 既に定義されています。' - errorLoggingAlreadyEnabledExceptionMessage = 'エラーロギングは既に有効になっています。' valueForUsingVariableNotFoundExceptionMessage = "'`$using:{0}'の値が見つかりませんでした。" rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'ドキュメントツール RapidPdf は OpenAPI 3.1 をサポートしていません' oauth2ClientSecretRequiredExceptionMessage = 'PKCEを使用しない場合、OAuth2にはクライアントシークレットが必要です。' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "'{1}'認証バリデーターのために提供された'{0}'スキームには有効なScriptBlockが必要です。" sseFailedToBroadcastExceptionMessage = '{0}のSSEブロードキャストレベルが定義されているため、SSEのブロードキャストに失敗しました: {1}' adModuleWindowsOnlyExceptionMessage = 'Active DirectoryモジュールはWindowsでのみ利用可能です。' - requestLoggingAlreadyEnabledExceptionMessage = 'リクエストロギングは既に有効になっています。' invalidAccessControlMaxAgeDurationExceptionMessage = '無効な Access-Control-Max-Age 期間が提供されました:{0}。0 より大きくする必要があります。' openApiDefinitionAlreadyExistsExceptionMessage = '名前が {0} の OpenAPI 定義は既に存在します。' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag は Select-PodeOADefinition 'ScriptBlock' 内で使用できません。" + loggingAlreadyEnabledExceptionMessage = "ログ記録 '{0}' は既に有効になっています。" + invalidEncodingExceptionMessage = '無効なエンコーディング: {0}' + syslogProtocolExceptionMessage = 'SyslogプロトコルはRFC3164またはRFC5424のみを使用できます。' taskProcessDoesNotExistExceptionMessage = 'タスクプロセスが存在しません: {0}' scheduleProcessDoesNotExistExceptionMessage = 'スケジュールプロセスが存在しません: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Routeの定義タグは変更できません。' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' は配列をパイプライン入力として受け付けません。" unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}' localEndpointConflictExceptionMessage = "'{0}' と '{1}' は OpenAPI のローカルエンドポイントとして定義されていますが、API 定義ごとに 1 つのローカルエンドポイントのみ許可されます。" + deprecatedFunctionWarningMessage = "警告: 関数 '{0}' は廃止され、将来のリリースで削除されます。代わりに '{1}' 関数を使用してください。" suspendingMessage = '停止' resumingMessage = '再開' serverControlCommandsTitle = 'サーバーコントロールコマンド:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "名前が '{0}' のレート制限ルールは存在しません。" accessLimitRuleAlreadyExistsExceptionMessage = "名前が '{0}' のアクセス制限ルールは既に存在します。" accessLimitRuleDoesNotExistExceptionMessage = "名前が '{0}' のアクセス制限ルールは存在しません。" + loggerDoesNotExistExceptionMessage = "ロガー '{0}' は存在しません。" } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index a0d882588..29c82a1b1 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "요청에 '{0}'라는 이름의 매개변수가 제공되지 않았거나 데이터가 없습니다." cacheStorageNotFoundForSetExceptionMessage = "캐시된 항목 '{1}'을(를) 설정하려고 할 때 이름이 '{0}'인 캐시 스토리지를 찾을 수 없습니다." methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: 이미 정의되었습니다.' - errorLoggingAlreadyEnabledExceptionMessage = '오류 로깅이 이미 활성화되었습니다.' valueForUsingVariableNotFoundExceptionMessage = "'`$using:{0}'에 대한 값을 찾을 수 없습니다." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = '문서 도구 RapidPdf는 OpenAPI 3.1을 지원하지 않습니다.' oauth2ClientSecretRequiredExceptionMessage = 'PKCE를 사용하지 않을 때 OAuth2에는 클라이언트 비밀이 필요합니다.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "'{1}' 인증 검증기에 제공된 '{0}' 스킴에는 유효한 ScriptBlock이 필요합니다." sseFailedToBroadcastExceptionMessage = '{0}에 대해 정의된 SSE 브로드캐스트 수준으로 인해 SSE 브로드캐스트에 실패했습니다: {1}' adModuleWindowsOnlyExceptionMessage = 'Active Directory 모듈은 Windows에서만 사용할 수 있습니다.' - requestLoggingAlreadyEnabledExceptionMessage = '요청 로깅이 이미 활성화되었습니다.' invalidAccessControlMaxAgeDurationExceptionMessage = '잘못된 Access-Control-Max-Age 기간이 제공되었습니다: {0}. 0보다 커야 합니다.' openApiDefinitionAlreadyExistsExceptionMessage = '이름이 {0}인 OpenAPI 정의가 이미 존재합니다.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag은 Select-PodeOADefinition 'ScriptBlock' 내에서 사용할 수 없습니다." + loggingAlreadyEnabledExceptionMessage = "로그 '{0}'이(가) 이미 활성화되었습니다." + invalidEncodingExceptionMessage = '잘못된 인코딩: {0}' + syslogProtocolExceptionMessage = 'Syslog 프로토콜은 RFC3164 또는 RFC5424만 사용할 수 있습니다.' taskProcessDoesNotExistExceptionMessage = '작업 프로세스가 존재하지 않습니다: {0}' scheduleProcessDoesNotExistExceptionMessage = '스케줄 프로세스가 존재하지 않습니다: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Route에 대한 정의 태그는 변경할 수 없습니다.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다." unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' localEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다." + deprecatedFunctionWarningMessage = "경고: 함수 '{0}'는 더 이상 지원되지 않으며, 향후 릴리스에서 제거될 예정입니다. 대신 '{1}' 함수를 사용하세요." suspendingMessage = '중단' resumingMessage = '재개' serverControlCommandsTitle = '서버 제어 명령:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "이름이 '{0}'인 비율 제한 규칙이 존재하지 않습니다." accessLimitRuleAlreadyExistsExceptionMessage = "이름이 '{0}'인 액세스 제한 규칙이 이미 존재합니다." accessLimitRuleDoesNotExistExceptionMessage = "이름이 '{0}'인 액세스 제한 규칙이 존재하지 않습니다." + loggerDoesNotExistExceptionMessage = "로거 '{0}'가 존재하지 않습니다." } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 6aad6ebc4..487f391dc 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -154,7 +154,6 @@ parameterNotSuppliedInRequestExceptionMessage = "Een parameter genaamd '{0}' is niet opgegeven in het verzoek of heeft geen beschikbare gegevens." cacheStorageNotFoundForSetExceptionMessage = "Cache-opslag met naam '{0}' niet gevonden bij poging om gecachte item '{1}' in te stellen" methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Al gedefinieerd.' - errorLoggingAlreadyEnabledExceptionMessage = 'Foutlogboekregistratie is al ingeschakeld.' valueForUsingVariableNotFoundExceptionMessage = "Waarde voor '`$using:{0}' kon niet worden gevonden." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'Het Document-tool RapidPdf ondersteunt OpenAPI 3.1 niet' oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 vereist een Client Secret wanneer PKCE niet wordt gebruikt.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "Het opgegeven '{0}' schema voor de '{1}' authenticatievalidator vereist een geldige ScriptBlock." sseFailedToBroadcastExceptionMessage = 'SSE kon niet uitzenden vanwege het gedefinieerde SSE-uitzendniveau voor {0}: {1}' adModuleWindowsOnlyExceptionMessage = 'Active Directory-module alleen beschikbaar op Windows OS.' - requestLoggingAlreadyEnabledExceptionMessage = 'Verzoeklogboekregistratie is al ingeschakeld.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Ongeldige Access-Control-Max-Age duur opgegeven: {0}. Moet groter zijn dan 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI-definitie met de naam {0} bestaat al.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag kan niet worden gebruikt binnen een Select-PodeOADefinition 'ScriptBlock'." + loggingAlreadyEnabledExceptionMessage = "Logging '{0}' is al ingeschakeld." + invalidEncodingExceptionMessage = 'Ongeldige codering: {0}' + syslogProtocolExceptionMessage = 'Het Syslog-protocol kan alleen RFC3164 of RFC5424 gebruiken.' taskProcessDoesNotExistExceptionMessage = "Taakproces '{0}' bestaat niet." scheduleProcessDoesNotExistExceptionMessage = "Schema-proces '{0}' bestaat niet." definitionTagChangeNotAllowedExceptionMessage = 'Definitietag voor een route kan niet worden gewijzigd.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "De functie '{0}' accepteert geen array als pipeline-invoer." unsupportedStreamCompressionEncodingExceptionMessage = 'Niet-ondersteunde streamcompressie-encodering: {0}' localEndpointConflictExceptionMessage = "Zowel '{0}' als '{1}' zijn gedefinieerd als lokale OpenAPI-eindpunten, maar er is slechts één lokaal eindpunt per API-definitie toegestaan." + deprecatedFunctionWarningMessage = "WAARSCHUWING: De functie '{0}' is verouderd en zal in toekomstige versies worden verwijderd. Gebruik in plaats daarvan de functie '{1}'." suspendingMessage = 'Onderbreken' resumingMessage = 'Hervatten' serverControlCommandsTitle = "Serverbedieningscommando's:" @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "Rate Limit-regel met de naam '{0}' bestaat niet." accessLimitRuleAlreadyExistsExceptionMessage = "Toegangslimietregel met de naam '{0}' bestaat al." accessLimitRuleDoesNotExistExceptionMessage = "Toegangslimietregel met de naam '{0}' bestaat niet." + loggerDoesNotExistExceptionMessage = "Logger '{0}' bestaat niet." } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index cf5dd507b..16bf4f072 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "Parametr o nazwie '{0}' nie został dostarczony w żądaniu lub nie ma dostępnych danych." cacheStorageNotFoundForSetExceptionMessage = "Nie znaleziono magazynu pamięci podręcznej o nazwie '{0}' podczas próby ustawienia elementu w pamięci podręcznej '{1}'." methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Już zdefiniowane.' - errorLoggingAlreadyEnabledExceptionMessage = 'Rejestrowanie błędów jest już włączone.' valueForUsingVariableNotFoundExceptionMessage = "Nie można znaleźć wartości dla '`$using:{0}'." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'Narzędzie do dokumentów RapidPdf nie obsługuje OpenAPI 3.1' oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 wymaga tajemnicy klienta, gdy nie używa się PKCE.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "Dostarczony schemat '{0}' dla walidatora uwierzytelniania '{1}' wymaga ważnego ScriptBlock." sseFailedToBroadcastExceptionMessage = 'SSE nie udało się przesłać z powodu zdefiniowanego poziomu przesyłania SSE dla {0}: {1}' adModuleWindowsOnlyExceptionMessage = 'Moduł Active Directory jest dostępny tylko w systemie Windows.' - requestLoggingAlreadyEnabledExceptionMessage = 'Rejestrowanie żądań jest już włączone.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Podano nieprawidłowy czas trwania Access-Control-Max-Age: {0}. Powinien być większy niż 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'Definicja OpenAPI o nazwie {0} już istnieje.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag nie może być używany wewnątrz 'ScriptBlock' Select-PodeOADefinition." + loggingAlreadyEnabledExceptionMessage = "Rejestrowanie '{0}' jest już włączone." + invalidEncodingExceptionMessage = 'Nieprawidłowe kodowanie: {0}' + syslogProtocolExceptionMessage = 'Protokół Syslog może używać tylko RFC3164 lub RFC5424.' taskProcessDoesNotExistExceptionMessage = "Proces zadania '{0}' nie istnieje." scheduleProcessDoesNotExistExceptionMessage = "Proces harmonogramu '{0}' nie istnieje." definitionTagChangeNotAllowedExceptionMessage = 'Tag definicji dla Route nie może zostać zmieniony.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Funkcja '{0}' nie akceptuje tablicy jako wejścia potoku." unsupportedStreamCompressionEncodingExceptionMessage = 'Kodowanie kompresji strumienia nie jest obsługiwane: {0}' localEndpointConflictExceptionMessage = "Zarówno '{0}', jak i '{1}' są zdefiniowane jako lokalne punkty końcowe OpenAPI, ale na jedną definicję API dozwolony jest tylko jeden lokalny punkt końcowy." + deprecatedFunctionWarningMessage = "OSTRZEŻENIE: Funkcja '{0}' jest przestarzała i zostanie usunięta w przyszłych wersjach. Użyj funkcji '{1}' zamiast niej." suspendingMessage = 'Wstrzymywanie' resumingMessage = 'Wznawianie' serverControlCommandsTitle = 'Polecenia sterowania serwerem:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "Reguła limitu szybkości o nazwie '{0}' nie istnieje." accessLimitRuleAlreadyExistsExceptionMessage = "Reguła limitu dostępu o nazwie '{0}' już istnieje." accessLimitRuleDoesNotExistExceptionMessage = "Reguła limitu dostępu o nazwie '{0}' nie istnieje." + loggerDoesNotExistExceptionMessage = "Logger '{0}' nie istnieje." } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index b8e1e4cd1..ae88065d4 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "Um parâmetro chamado '{0}' não foi fornecido na solicitação ou não há dados disponíveis." cacheStorageNotFoundForSetExceptionMessage = "Armazenamento em cache com o nome '{0}' não encontrado ao tentar definir o item em cache '{1}'." methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: Já definido.' - errorLoggingAlreadyEnabledExceptionMessage = 'O registro de erros já está habilitado.' valueForUsingVariableNotFoundExceptionMessage = "Valor para '`$using:{0}' não pôde ser encontrado." rapidPdfDoesNotSupportOpenApi31ExceptionMessage = 'A ferramenta de documentos RapidPdf não suporta OpenAPI 3.1' oauth2ClientSecretRequiredExceptionMessage = 'OAuth2 requer um Client Secret quando não se usa PKCE.' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "O esquema '{0}' fornecido para o validador de autenticação '{1}' requer um ScriptBlock válido." sseFailedToBroadcastExceptionMessage = 'SSE falhou em transmitir devido ao nível de transmissão SSE definido para {0}: {1}.' adModuleWindowsOnlyExceptionMessage = 'O módulo Active Directory está disponível apenas no Windows.' - requestLoggingAlreadyEnabledExceptionMessage = 'O registro de solicitações já está habilitado.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Duração inválida fornecida para Access-Control-Max-Age: {0}. Deve ser maior que 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'A definição OpenAPI com o nome {0} já existe.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag não pode ser usado dentro de um 'ScriptBlock' Select-PodeOADefinition." + loggingAlreadyEnabledExceptionMessage = "O registro '{0}' já foi habilitado." + invalidEncodingExceptionMessage = 'Codificação inválida: {0}' + syslogProtocolExceptionMessage = 'O protocolo Syslog só pode usar RFC3164 ou RFC5424.' taskProcessDoesNotExistExceptionMessage = "O processo da tarefa '{0}' não existe." scheduleProcessDoesNotExistExceptionMessage = "O processo do cronograma '{0}' não existe." definitionTagChangeNotAllowedExceptionMessage = 'A Tag de definição para uma Route não pode ser alterada.' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "A função '{0}' não aceita uma matriz como entrada de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'A codificação de compressão de fluxo não é suportada.' localEndpointConflictExceptionMessage = "Tanto '{0}' quanto '{1}' estão definidos como endpoints locais do OpenAPI, mas apenas um endpoint local é permitido por definição de API." + deprecatedFunctionWarningMessage = "AVISO: A função '{0}' está obsoleta e será removida em versões futuras. Por favor, use a função '{1}' em seu lugar." suspendingMessage = 'Suspensão' resumingMessage = 'Retomada' serverControlCommandsTitle = 'Comandos de controle do servidor:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "A regra de limite de taxa com o nome '{0}' não existe." accessLimitRuleAlreadyExistsExceptionMessage = "A regra de limite de acesso com o nome '{0}' já existe." accessLimitRuleDoesNotExistExceptionMessage = "A regra de limite de acesso com o nome '{0}' não existe." + loggerDoesNotExistExceptionMessage = "O logger '{0}' não existe." } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 3653dc151..246d260f9 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -153,7 +153,6 @@ parameterNotSuppliedInRequestExceptionMessage = "请求中未提供名为 '{0}' 的参数或没有可用数据。" cacheStorageNotFoundForSetExceptionMessage = "尝试设置缓存项 '{1}' 时,找不到名为 '{0}' 的缓存存储。" methodPathAlreadyDefinedExceptionMessage = '[{0}] {1}: 已经定义。' - errorLoggingAlreadyEnabledExceptionMessage = '错误日志记录已启用。' valueForUsingVariableNotFoundExceptionMessage = "未找到 '`$using:{0}' 的值。" rapidPdfDoesNotSupportOpenApi31ExceptionMessage = '文档工具 RapidPdf 不支持 OpenAPI 3.1' oauth2ClientSecretRequiredExceptionMessage = '不使用 PKCE 时, OAuth2 需要一个客户端密钥。' @@ -281,10 +280,12 @@ invalidSchemeForAuthValidatorExceptionMessage = "提供的 '{0}' 方案用于 '{1}' 身份验证验证器,需要一个有效的 ScriptBlock。" sseFailedToBroadcastExceptionMessage = '由于为{0}定义的SSE广播级别, SSE广播失败: {1}' adModuleWindowsOnlyExceptionMessage = '仅支持 Windows 的 Active Directory 模块。' - requestLoggingAlreadyEnabledExceptionMessage = '请求日志记录已启用。' invalidAccessControlMaxAgeDurationExceptionMessage = '提供的 Access-Control-Max-Age 时长无效:{0}。应大于 0。' openApiDefinitionAlreadyExistsExceptionMessage = '名为 {0} 的 OpenAPI 定义已存在。' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag 不能在 Select-PodeOADefinition 'ScriptBlock' 内使用。" + loggingAlreadyEnabledExceptionMessage = "日志记录 '{0}' 已启用。" + invalidEncodingExceptionMessage = '无效的编码: {0}' + syslogProtocolExceptionMessage = 'Syslog 协议只能使用 RFC3164 或 RFC5424。' taskProcessDoesNotExistExceptionMessage = "任务进程 '{0}' 不存在。" scheduleProcessDoesNotExistExceptionMessage = "计划进程 '{0}' 不存在。" definitionTagChangeNotAllowedExceptionMessage = 'Route的定义标签无法更改。' @@ -292,6 +293,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。" unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' localEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。" + deprecatedFunctionWarningMessage = "警告:函数 '{0}' 已被弃用,并将在未来版本中移除。请改用 '{1}' 函数。" suspendingMessage = '暂停' resumingMessage = '恢复' serverControlCommandsTitle = '服务器控制命令:' @@ -326,4 +328,5 @@ rateLimitRuleDoesNotExistExceptionMessage = '速率限制规则不存在: {0}' accessLimitRuleAlreadyExistsExceptionMessage = '访问限制规则已存在: {0}' accessLimitRuleDoesNotExistExceptionMessage = '访问限制规则不存在: {0}' + loggerDoesNotExistExceptionMessage = "日志记录器 '{0}' 不存在。" } \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index aff2bfcb7..971c12f78 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -286,17 +286,40 @@ # logging 'New-PodeLoggingMethod', + 'New-PodeCustomLoggingMethod', + 'New-PodeEventViewerLoggingMethod', + 'New-PodeFileLoggingMethod', + 'New-PodeSyslogLoggingMethod', + 'New-PodeTerminalLoggingMethod', + 'New-PodeAwsLoggingMethod', + 'New-PodeAzureLoggingMethod', + 'New-PodeDatadogLoggingMethod', + 'New-PodeElasticsearchLoggingMethod', + 'New-PodeGoogleLoggingMethod', + 'New-PodeGraylogLoggingMethod', + 'New-PodeLogInsightLoggingMethod', + 'New-PodeSplunkLoggingMethod', + 'Enable-PodeRequestLogging', 'Enable-PodeErrorLogging', + 'Enable-PodeDefaultLogging', 'Disable-PodeRequestLogging', 'Disable-PodeErrorLogging', + 'Disable-PodeDefaultLogging', + 'Add-PodeLoggingMethod', + 'Remove-PodeLoggingMethod', 'Add-PodeLogger', 'Remove-PodeLogger', - 'Clear-PodeLoggers', + 'Clear-PodeLogger', 'Write-PodeErrorLog', 'Write-PodeLog', 'Protect-PodeLogItem', 'Use-PodeLogging', + 'Enable-PodeLog', + 'Disable-PodeLog', + 'Clear-PodeLogging', + 'Test-PodeLoggerEnabled', + 'Get-PodeLoggerLevel' # core 'Start-PodeServer', @@ -541,7 +564,8 @@ 'Enable-PodeOpenApiViewer', 'Enable-PodeOA', 'Get-PodeOpenApiDefinition', - 'New-PodeOASchemaProperty' + 'New-PodeOASchemaProperty', + 'Clear-PodeLoggers' ) # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. diff --git a/src/Private/Console.ps1 b/src/Private/Console.ps1 index 3cae177fb..01df4c364 100644 --- a/src/Private/Console.ps1 +++ b/src/Private/Console.ps1 @@ -1340,6 +1340,7 @@ function Set-PodeConsoleOverrideConfiguration { $PodeContext.Server.Console.Quiet = $true $PodeContext.Server.Console.DisableConsoleInput = $true $PodeContext.Server.Console.DisableTermination = $true + $PodeContext.Server.Console.Daemon = $true } # Apply IIS-specific overrides diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 19d4c1004..1c037456d 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -1,5 +1,58 @@ using namespace Pode +<# +.SYNOPSIS + Initializes a new Pode context with various server configurations. + +.DESCRIPTION + This function creates and initializes a new Pode context object with server configurations, including threading, schedules, tasks, logging, and more. + It ensures that essential configurations are set, and it can run in different environments such as serverless or IIS. + +.PARAMETER ScriptBlock + The script block to be executed within the Pode context. + +.PARAMETER FilePath + The file path to the script block. + +.PARAMETER Threads + The number of threads to be used. Default is 1. + +.PARAMETER Interval + The interval for server operations. Default is 0. + +.PARAMETER ServerRoot + The root path for the server. + +.PARAMETER Name + The name of the server. If not provided, a random name will be generated. + +.PARAMETER ServerlessType + Specifies if the server is running in a serverless context. + +.PARAMETER StatusPageExceptions + Configuration for displaying exceptions on the status page. + +.PARAMETER ListenerType + The type of listener to be used by the server. + +.PARAMETER EnablePool + An array of pools to enable, such as 'timers', 'tasks', 'schedules', and 'websockets'. + +.PARAMETER DisableTermination + A switch to disable server termination. + +.PARAMETER Quiet + A switch to enable quiet mode, suppressing certain outputs. + +.PARAMETER EnableBreakpoints + A switch to enable debugging breakpoints. + +.PARAMETER Daemon + Configures the server to run as a daemon with minimal console interaction and output. + +.EXAMPLE + $context = New-PodeContext -ScriptBlock $script -FilePath 'path/to/file' -Threads 4 -ServerRoot 'path/to/root' +#> function New-PodeContext { [CmdletBinding()] param( @@ -82,7 +135,6 @@ function New-PodeContext { Runspaces = $null RunspaceState = $null Tokens = @{} - LogsToProcess = $null Threading = @{} Server = @{} Metrics = @{} @@ -137,8 +189,11 @@ function New-PodeContext { # basic logging setup $ctx.Server.Logging = @{ - Enabled = $true - Types = @{} + Enabled = $true + Type = @{} + Masking = @{} + QueueLimit = 500 + Method = @{} } # set thread counts @@ -474,9 +529,6 @@ function New-PodeContext { # create new cancellation tokens $ctx.Tokens = Initialize-PodeCancellationToken - # requests that should be logged - $ctx.LogsToProcess = [System.Collections.ArrayList]::new() - # middleware that needs to run $ctx.Server.Middleware = @() $ctx.Server.BodyParsers = @{} @@ -500,6 +552,7 @@ function New-PodeContext { Gui = $null Tasks = $null Files = $null + Logs = $null Timers = $null } @@ -614,6 +667,13 @@ function New-PodeRunspacePool { LastId = 0 } + # logs runspace - any log is running here + $PodeContext.RunspacePools.Logs = @{ + Pool = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 + } + # web runspace - if we have any http/s endpoints if (Test-PodeEndpointByProtocolType -Type Http) { $PodeContext.RunspacePools.Web = @{ @@ -870,7 +930,6 @@ function New-PodeStateContext { RunspacePools = $Context.RunspacePools Tokens = $Context.Tokens Metrics = $Context.Metrics - LogsToProcess = $Context.LogsToProcess Threading = $Context.Threading Server = $Context.Server } @@ -967,14 +1026,18 @@ function Set-PodeServerConfiguration { Files = @() } - # logging - $Context.Server.Logging = @{ - Enabled = (($null -eq $Configuration.Logging.Enable) -or [bool]$Configuration.Logging.Enable) - Masking = @{ - Patterns = (Remove-PodeEmptyItemsFromArray -Array @($Configuration.Logging.Masking.Patterns)) - Mask = (Protect-PodeValue -Value $Configuration.Logging.Masking.Mask -Default '********') + if ($Configuration.ContainsKey('Logging')) { + # logging + if ($Configuration.Logging.ContainsKey('Enable')) { + $Context.Server.Logging.Enabled = ([bool]$Configuration.Logging.Enable) + } + if ($Configuration.Logging.ContainsKey('Masking')) { + $Context.Server.Logging.Masking = @{ + Patterns = (Remove-PodeEmptyItemsFromArray -Array @($Configuration.Logging.Masking.Patterns)) + Mask = (Protect-PodeValue -Value $Configuration.Logging.Masking.Mask -Default '********') + } } - Types = @{} + $Context.Server.Logging.QueueLimit = (Protect-PodeValue -Value $Configuration.Logging.QueueLimit $Context.Server.Logging.QueueLimit) } # sockets diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index 97c590ad4..3e1f1c0d1 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -12,8 +12,8 @@ function New-PodeFileWatcher { [OutputType([PodeWatcher])] param() $watcher = [PodeWatcher]::new($PodeContext.Tokens.Cancellation.Token) - $watcher.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $watcher.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $watcher.ErrorLoggingEnabled = (Test-PodeLoggerEnabled -Type Error) + $watcher.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeLoggerLevel -Type Error) } else { @() } return $watcher } @@ -168,4 +168,4 @@ function Start-PodeFileWatcherRunspace { } Add-PodeRunspace -Type Files -Name 'KeepAlive' -ScriptBlock $waitScript -Parameters @{ 'Watcher' = $watcher } -NoProfile -} +} \ No newline at end of file diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index beefebb89..4855e1238 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -622,6 +622,9 @@ function Close-PodeServerInternal { # PodeContext doesn't exist return if ($null -eq $PodeContext) { return } try { + #Disable Logging before closing + Disable-PodeLog + # ensure the token is cancelled Write-Verbose 'Cancelling main cancellation token' Close-PodeCancellationTokenRequest -Type Cancellation, Terminate @@ -4009,6 +4012,101 @@ function Test-PodeIsISEHost { return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name)) } + +<# +.SYNOPSIS + Converts a PSCustomObject to an ordered hashtable. + +.DESCRIPTION + This function recursively converts a given PSCustomObject into an ordered hashtable. + It ensures that properties maintain their order and that nested PSCustomObjects and + collections are also converted appropriately. + +.PARAMETER InputObject + Specifies the PSCustomObject to be converted into an ordered hashtable. + This parameter is mandatory and accepts pipeline input. + +.OUTPUTS + [System.Collections.Specialized.OrderedDictionary] + Returns an ordered hashtable representing the original PSCustomObject. + +.EXAMPLE + $object = [PSCustomObject]@{ + Name = "John" + Age = 30 + Address = [PSCustomObject]@{ + City = "New York" + State = "NY" + } + } + + Convert-PsCustomObjectToOrderedHashtable -InputObject $object + + This example converts the PSCustomObject `$object` into an ordered hashtable. + +.EXAMPLE + $object | Convert-PsCustomObjectToOrderedHashtable + + This example demonstrates using the function with pipeline input. + +.NOTES + Internal helper function `Convert-ObjectRecursively` is used to process nested objects + and collections recursively. +#> + +function Convert-PsCustomObjectToOrderedHashtable { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [PSCustomObject]$InputObject + ) + begin { + # Define a recursive function within the process block + function Convert-ObjectRecursively { + param ( + [Parameter(Mandatory = $true)] + [System.Object] + $InputObject + ) + + # Initialize an ordered dictionary + $orderedHashtable = [ordered]@{} + + # Loop through each property of the PSCustomObject + foreach ($property in $InputObject.PSObject.Properties) { + # Check if the property value is a PSCustomObject + if ($property.Value -is [PSCustomObject]) { + # Recursively convert the nested PSCustomObject + $orderedHashtable[$property.Name] = Convert-ObjectRecursively -InputObject $property.Value + } + elseif ($property.Value -is [System.Collections.IEnumerable] -and -not ($property.Value -is [string])) { + # If the value is a collection, check each element + $convertedCollection = @() + foreach ($item in $property.Value) { + if ($item -is [PSCustomObject]) { + $convertedCollection += Convert-ObjectRecursively -InputObject $item + } + else { + $convertedCollection += $item + } + } + $orderedHashtable[$property.Name] = $convertedCollection + } + else { + # Add the property name and value to the ordered hashtable + $orderedHashtable[$property.Name] = $property.Value + } + } + + # Return the resulting ordered hashtable + return $orderedHashtable + } + } + process { + # Call the recursive helper function for each input object + Convert-ObjectRecursively -InputObject $InputObject + } +} <# .SYNOPSIS Determines the MIME type of an image from its binary header. diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index 885133dad..aed6a0621 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -1,115 +1,378 @@ +using namespace Pode + +<# +.SYNOPSIS +Defines the method for writing log messages to the terminal. + +.DESCRIPTION +This internal function handles writing log messages to the terminal. +It checks if the server is in quiet mode and protects sensitive information before outputting the log messages. + +.PARAMETER item +The log item to be written to the terminal. + +.PARAMETER options +A hashtable containing options for the terminal logging method. + +.NOTES +This is an internal function and may change in future releases of Pode. +#> function Get-PodeLoggingTerminalMethod { return { - param($item, $options) + param($MethodId) if ($PodeContext.Server.Quiet) { return } - # check if it's an array from batching - if ($item -is [array]) { - $item = ($item -join [System.Environment]::NewLine) - } + $log = @{} + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + Start-Sleep -Milliseconds 100 + + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + if ($null -ne $log) { + $Item = $log.Item + # check if it's an array from batching + if ($Item -is [array]) { + $Item = ($Item -join [System.Environment]::NewLine) + } - # protect then write - $item = ($item | Protect-PodeLogItem) - $item.ToString() | Out-PodeHost + # protect then write + $Item = ([pode.PodeLogger]::ProtectLogItem($Item, $PodeContext.Server.Logging.Masking)) #($Item | Protect-PodeLogItem) + $Item.ToString() | Out-PodeHost + } + } + } } } +<# +.SYNOPSIS + Defines the method for writing log messages to a file. + +.DESCRIPTION + This internal function handles writing log messages to a file, managing file rotation based on size and date, and removing old log files beyond a specified retention period. + It includes error handling based on user-defined actions. + +.PARAMETER item + The log item to be written to the file. + +.PARAMETER options + A hashtable containing options for the file logging method including Path, Name, MaxDays, MaxSize, Date, FileId, and FailureAction. +.NOTES + This is an internal function and may change in future releases of Pode. +#> function Get-PodeLoggingFileMethod { return { - param($item, $options) + param($MethodId) - # check if it's an array from batching - if ($item -is [array]) { - $item = ($item -join [System.Environment]::NewLine) - } + $log = @{} + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + Start-Sleep -Milliseconds 100 - # mask values - $item = ($item | Protect-PodeLogItem) + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + if ($null -ne $log) { - # variables - $date = [DateTime]::Now.ToString('yyyy-MM-dd') + try { + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem - # do we need to reset the fileId? - if ($options.Date -ine $date) { - $options.Date = $date - $options.FileId = 0 - } + # Variables + $date = [DateTime]::Now.ToString('yyyy-MM-dd') - # get the fileId - if ($options.FileId -eq 0) { - $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_*.log") - $options.FileId = (@(Get-ChildItem -Path $path)).Length - if ($options.FileId -eq 0) { - $options.FileId = 1 - } - } + # Reset the fileId if the date has changed + if ($Options.Date -ine $date) { + $Options.Date = $date + $Options.FileId = 0 + } + + # Get the fileId if it hasn't been set + if ($Options.FileId -eq 0) { + $path = [System.IO.Path]::Combine($Options.Path, "$($Options.Name)_$($date)_*.log") + $Options.FileId = (@(Get-ChildItem -Path $path)).Length + if ($Options.FileId -eq 0) { + $Options.FileId = 1 + } + } - $id = "$($options.FileId)".PadLeft(3, '0') - if ($options.MaxSize -gt 0) { - $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_$($id).log") - if ((Get-Item -Path $path -Force).Length -ge $options.MaxSize) { - $options.FileId++ - $id = "$($options.FileId)".PadLeft(3, '0') + $id = "$($Options.FileId)".PadLeft(3, '0') + + # Check if file size exceeds MaxSize and increment fileId if necessary + if ($Options.MaxSize -gt 0) { + $path = [System.IO.Path]::Combine($Options.Path, "$($Options.Name)_$($date)_$($id).log") + if ((Get-Item -Path $path -Force).Length -ge $Options.MaxSize) { + $Options.FileId++ + $id = "$($Options.FileId)".PadLeft(3, '0') + } + } + + # Get the file to write to + $path = [System.IO.Path]::Combine($Options.Path, "$($Options.Name)_$($date)_$($id).log") + + if ($Options.Format -eq 'Default') { + # Check if the item is an array from batching + if ($Item -is [array]) { + $Item = ($Item -join [System.Environment]::NewLine) + } + + # Mask values + $outString = ([pode.PodeLogger]::ProtectLogItem($Item, $PodeContext.Server.Logging.Masking)).ToString() + } + else { + if ($RawItem -is [array]) { + $tmpStrings = @() + foreach ($ritem in $RawItem) { + $tmpStrings += [pode.PodeFormat]::Syslog($RawItem, $Options, $PodeContext.Server.Logging.Masking) + } + $outString = $tmpStrings -join [System.Environment]::NewLine + + } + else { + $outString = [pode.PodeFormat]::Syslog($RawItem, $Options, $PodeContext.Server.Logging.Masking) + } + + } + # Write the item to the file + $outString | Out-File -FilePath $path -Encoding $Options.Encoding -Append -Force + + # Remove log files beyond the MaxDays retention period, ensuring this runs once a day + if (($Options.MaxDays -gt 0) -and ($Options.NextClearDown -le [DateTime]::Now.Date)) { + $date = [DateTime]::Now.Date.AddDays(-$Options.MaxDays) + + $null = Get-ChildItem -Path $options.Path -Filter "$($options.Name)_*.log" -Force | + Where-Object { $_.CreationTime -lt $date } | + Remove-Item -Force + + $Options.NextClearDown = [DateTime]::Now.Date.AddDays(1) + } + } + catch { + Invoke-PodeHandleFailure -Message "Failed to log a message: $_" -FailureAction $Options.FailureAction + } + } } } + } +} - # get the file to write to - $path = [System.IO.Path]::Combine($options.Path, "$($options.Name)_$($date)_$($id).log") - # write the item to the file - $item.ToString() | Out-File -FilePath $path -Encoding utf8 -Append -Force +<# +.SYNOPSIS + Handles the sending of log messages to a Syslog server using various transport protocols. - # if set, remove log files beyond days set (ensure this is only run once a day) - if (($options.MaxDays -gt 0) -and ($options.NextClearDown -le [DateTime]::Now.Date)) { - $date = [DateTime]::Now.Date.AddDays(-$options.MaxDays) +.DESCRIPTION + This function defines the logic for sending log messages to a Syslog server using different transport protocols including UDP, TCP, TLS, Splunk, and VMware LogInsight. + It supports both RFC 3164 and RFC 5424 formats and includes error handling based on user-defined actions. - $null = Get-ChildItem -Path $options.Path -Filter "$($options.Name)_*.log" -Force | - Where-Object { $_.CreationTime -lt $date } | - Remove-Item -Force +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeLoggingSysLogMethod { + return { + param($MethodId) - $options.NextClearDown = [DateTime]::Now.Date.AddDays(1) + $log = @{} + $socketCreated = $false + try { + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + Start-Sleep -Milliseconds 100 + + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + + $Options = $log.Options + $RawItem = $log.RawItem + + if ($RawItem -isnot [array]) { + $RawItem = @($RawItem) + } + + # Create the socket if it hasn't been created already + if (!$socketCreated) { + switch ($Options.Transport.ToUpperInvariant()) { + 'UDP' { + $udpClient = [System.Net.Sockets.UdpClient]::new() + } + 'TCP' { + # Create a TCP client for non-secure communication + $tcpClient = [System.Net.Sockets.TcpClient]::new() + $tcpClient.Connect($Options.Server, $Options.Port) + $networkStream = $tcpClient.GetStream() + } + 'TLS' { + # Create a TCP client for secure communication + $tcpClient = [System.Net.Sockets.TcpClient]::new() + $tcpClient.Connect($Options.Server, $Options.Port) + + $sslStream = if ($Options.SkipCertificateCheck) { + [System.Net.Security.SslStream]::new($tcpClient.GetStream(), $false, { $true }) + } + else { + [System.Net.Security.SslStream]::new($tcpClient.GetStream(), $false) + } + + # Define the TLS protocol version + $tlsProtocol = if ($Options.TlsProtocols) { + $Options.TlsProtocols + } + else { + [System.Security.Authentication.SslProtocols]::Tls12 # Default to TLS 1.2 + } + + # Authenticate as client with specific TLS protocol + $sslStream.AuthenticateAsClient($Options.Server, $null, $tlsProtocol, $false) + } + default { + $udpClient = [System.Net.Sockets.UdpClient]::new() + } + } + $socketCreated = $true + } + + for ($i = 0; $i -lt $RawItem.Length; $i++) { + $fullSyslogMessage = [pode.PodeFormat]::Syslog($RawItem[$i], $Options, $PodeContext.Server.Logging.Masking) + + # Convert the message to a byte array + $byteMessage = $($Options.Encoding).GetBytes($fullSyslogMessage) + + # Determine the transport protocol and send the message + switch ($Options.Transport.ToUpperInvariant()) { + 'UDP' { + try { + # Send the message to the syslog server + $udpClient.Send($byteMessage, $byteMessage.Length, $Options.Server, $Options.Port) + } + catch { + Invoke-PodeHandleFailure -Message "Failed to send UDP message: $_" -FailureAction $Options.FailureAction + } + } + 'TCP' { + try { + # Send the message + $networkStream.Write($byteMessage, 0, $byteMessage.Length) + $networkStream.Flush() + } + catch { + Invoke-PodeHandleFailure -Message "Failed to send TCP message: $_" -FailureAction $Options.FailureAction + } + } + 'TLS' { + try { + # Send the message + $sslStream.Write($byteMessage) + $sslStream.Flush() + } + catch { + Invoke-PodeHandleFailure -Message "Failed to send secure TLS message: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + } + } + finally { + # Close the sockets and cleanup + switch ($Options.Transport.ToUpperInvariant()) { + 'UDP' { + # Close the UDP client + if ($udpClient) { + $udpClient.Close() + } + } + 'TCP' { + # Close the TCP client + if ($networkStream) { $networkStream.Close() } + if ($tcpClient) { $tcpClient.Close() } + } + 'TLS' { + # Close the TCP client + if ($sslStream) { $sslStream.Close() } + if ($tcpClient) { $tcpClient.Close() } + } + } + $socketCreated = $false } } } +<# +.SYNOPSIS +Defines the method for sending log messages to the Windows Event Viewer. + +.DESCRIPTION +This internal function handles the sending of log messages to the Windows Event Viewer, converting log levels and creating event log entries. It includes error handling based on user-defined actions. + +.NOTES +This is an internal function and may change in future releases of Pode. +#> function Get-PodeLoggingEventViewerMethod { return { - param($item, $options, $rawItem) + param($MethodId) - if ($item -isnot [array]) { - $item = @($item) - } + $log = @{} + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + Start-Sleep -Milliseconds 100 - if ($rawItem -isnot [array]) { - $rawItem = @($rawItem) - } + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + if ($null -ne $log) { + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem - for ($i = 0; $i -lt $item.Length; $i++) { - # convert log level - info if no level present - $entryType = ConvertTo-PodeEventViewerLevel -Level $rawItem[$i].Level + # Ensure item and rawItem are arrays + if ($Item -isnot [array]) { + $Item = @($Item) + } - # create log instance - $entryInstance = [System.Diagnostics.EventInstance]::new($options.ID, 0, $entryType) + if ($RawItem -isnot [array]) { + $RawItem = @($RawItem) + } - # create event log - $entryLog = [System.Diagnostics.EventLog]::new() - $entryLog.Log = $options.LogName - $entryLog.Source = $options.Source + for ($i = 0; $i -lt $RawItem.Length; $i++) { + # Convert log level to Event Viewer entry type - default to 'Information' if no level present + $entryType = ConvertTo-PodeEventViewerLevel -Level $RawItem[$i].Level - try { - $message = ($item[$i] | Protect-PodeLogItem) - $entryLog.WriteEvent($entryInstance, $message) - } - catch { - $_ | Write-PodeErrorLog -Level Debug + # Create EventInstance for the log entry + $entryInstance = [System.Diagnostics.EventInstance]::new($Options.ID, 0, $entryType) + + # Create EventLog object and set the log name and source + $entryLog = [System.Diagnostics.EventLog]::new() + $entryLog.Log = $Options.LogName + $entryLog.Source = $Options.Source + + try { + # Mask values and write the event to the Event Viewer + $message = ([pode.PodeLogger]::ProtectLogItem($Item[$i], $PodeContext.Server.Logging.Masking)) + $entryLog.WriteEvent($entryInstance, $message) + } + catch { + Invoke-PodeHandleFailure -Message "Failed to write an Event Viewer message: $_" -FailureAction $Options.FailureAction + } + } + } } } } } +<# +.SYNOPSIS +Converts a log level string to a corresponding EventLogEntryType. + +.DESCRIPTION +This internal function converts a provided log level string to the corresponding `System.Diagnostics.EventLogEntryType` enumeration value. +It defaults to `Information` if the level is empty or unrecognized. + +.PARAMETER Level +The log level string to be converted (e.g., 'error', 'warning'). + +.RETURNS +Returns a `System.Diagnostics.EventLogEntryType` enumeration value corresponding to the provided log level. + +.NOTES +This is an internal function and may change in future releases of Pode. +#> function ConvertTo-PodeEventViewerLevel { param( [Parameter()] @@ -132,10 +395,26 @@ function ConvertTo-PodeEventViewerLevel { return [System.Diagnostics.EventLogEntryType]::Information } +<# +.SYNOPSIS +Gets the script block for a specified inbuilt logging type. + +.DESCRIPTION +This function returns a script block that formats log entries for a specified inbuilt logging type in Pode. The supported types are 'Errors', 'Requests', 'General', and 'Default'. Each type has its own formatting logic. + +.PARAMETER Type +The type of logging to get the script block for. Must be one of 'Errors', 'Requests', 'General', or 'Default'. + +.EXAMPLE +$script = Get-PodeLoggingInbuiltType -Type 'Requests' + +.EXAMPLE +$script = Get-PodeLoggingInbuiltType -Type 'Errors' +#> function Get-PodeLoggingInbuiltType { param( [Parameter(Mandatory = $true)] - [ValidateSet('Errors', 'Requests')] + [ValidateSet('Errors', 'Requests', 'General', 'Default', 'Listener')] [string] $Type ) @@ -144,55 +423,27 @@ function Get-PodeLoggingInbuiltType { 'requests' { $script = { param($item, $options) - - # just return the item if Raw is set - if ($options.Raw) { - return $item - } - - function sg($value) { - if ([string]::IsNullOrWhiteSpace($value)) { - return '-' - } - - return $value - } - - # build the url with http method - $url = "$(sg $item.Request.Method) $(sg $item.Request.Resource) $(sg $item.Request.Protocol)" - - # build and return the request row - return "$(sg $item.Host) $(sg $item.RfcUserIdentity) $(sg $item.User) [$(sg $item.Date)] `"$($url)`" $(sg $item.Response.StatusCode) $(sg $item.Response.Size) `"$(sg $item.Request.Referrer)`" `"$(sg $item.Request.Agent)`"" + return [Pode.PodeFormat]::RequestLog($item, $options) } } 'errors' { $script = { param($item, $options) + return [Pode.PodeFormat]::ErrorsLog($item, $options) + } + } + 'general' { + $script = { + param($item, $options) + return [Pode.PodeFormat]::GeneralLog($item, $options) + } + } - # do nothing if the error level isn't present - if (@($options.Levels) -inotcontains $item.Level) { - return - } - - # just return the item if Raw is set - if ($options.Raw) { - return $item - } - - # build the exception details - $row = @( - "Date: $($item.Date.ToString('yyyy-MM-dd HH:mm:ss'))", - "Level: $($item.Level)", - "ThreadId: $($item.ThreadId)", - "Server: $($item.Server)", - "Category: $($item.Category)", - "Message: $($item.Message)", - "StackTrace: $($item.StackTrace)" - ) - - # join the details and return - return "$($row -join "`n")`n" + 'Default' { + $script = { + param($item, $options) + return [Pode.PodeFormat]::GeneralLog($item, $options) } } } @@ -200,14 +451,6 @@ function Get-PodeLoggingInbuiltType { return $script } -function Get-PodeRequestLoggingName { - return '__pode_log_requests__' -} - -function Get-PodeErrorLoggingName { - return '__pode_log_errors__' -} - <# .SYNOPSIS Retrieves a Pode logger by name. @@ -232,73 +475,66 @@ function Get-PodeLogger { [string] $Name ) - - return $PodeContext.Server.Logging.Types[$Name] -} - -function Test-PodeLoggerEnabled { - param( - [Parameter(Mandatory = $true)] - [string] - $Name - ) - - return ($PodeContext.Server.Logging.Enabled -and $PodeContext.Server.Logging.Types.ContainsKey($Name)) + if (!$PodeContext.Server.Logging.Type.ContainsKey($Name)) { + throw $PodeLocale.loggerDoesNotExistExceptionMessage + } + return $PodeContext.Server.Logging.Type[$Name] } + <# .SYNOPSIS - Gets the error logging levels for Pode. +Writes a log entry for a Pode web request. .DESCRIPTION - This function retrieves the error logging levels configured for Pode. It returns an array of available error levels. - -.PARAMETER Name - The name of the Pode logger to retrieve. - -.OUTPUTS - An array of error logging levels. +This function writes a log entry for a Pode web request. It logs details about the request and response, including method, resource, status code, and user information. The log entry is enqueued for processing if logging is enabled. -.NOTES - This is an internal function and may change in future releases of Pode. -#> -function Get-PodeErrorLoggingLevel { - return (Get-PodeLogger -Name (Get-PodeErrorLoggingName)).Arguments.Levels -} +.PARAMETER Request +The Pode web request object. -function Test-PodeErrorLoggingEnabled { - return (Test-PodeLoggerEnabled -Name (Get-PodeErrorLoggingName)) -} +.PARAMETER Response +The Pode web response object. -function Test-PodeRequestLoggingEnabled { - return (Test-PodeLoggerEnabled -Name (Get-PodeRequestLoggingName)) -} +.PARAMETER Path +The path of the request. +.EXAMPLE +Write-PodeRequestLog -Request $webEvent.Request -Response $webEvent.Response -Path $webEvent.Path +#> function Write-PodeRequestLog { param( [Parameter(Mandatory = $true)] + [PodeHttpRequest] $Request, [Parameter(Mandatory = $true)] + [PodeResponse] $Response, [Parameter()] [string] $Path ) - - # do nothing if logging is disabled, or request logging isn't setup - $name = Get-PodeRequestLoggingName + # Do nothing if logging is disabled, or request logging isn't set up + $name = [Pode.PodeLogger]::RequestLogName if (!(Test-PodeLoggerEnabled -Name $name)) { return } - # build a request object + # Determine the current date and time, respecting the AsUTC setting + if ($PodeContext.Server.Logging.Type[$Name].Method.Arguments.AsUTC) { + $date = [datetime]::UtcNow + } + else { + $date = [datetime]::Now + } + + # Build a request object $item = @{ Host = $Request.RemoteEndPoint.Address.IPAddressToString RfcUserIdentity = '-' User = '-' - Date = [DateTime]::Now.ToString('dd/MMM/yyyy:HH:mm:ss zzz') + Date = $date UtcDate = [DateTime]::UtcNow Request = @{ Method = $Request.HttpMethod.ToUpperInvariant() @@ -308,21 +544,22 @@ function Write-PodeRequestLog { Query = (Protect-PodeValue -Value $Request.Url.Query -Default '-').TrimStart('?') Protocol = "HTTP/$($Request.ProtocolVersion)" Referrer = $Request.UrlReferrer - Agent = $Request.UserAgent + Agent = $Request.UserAgent } Response = @{ StatusCode = $Response.StatusCode StatusDescription = $Response.StatusDescription Size = '-' } + Level = 'info' } - - # set size if >0 + + # Set size if >0 if ($Response.ContentLength64 -gt 0) { $item.Response.Size = $Response.ContentLength64 } - # set username - dot spaces + # Set username - dot spaces if (Test-PodeAuthUser -IgnoreSession) { $userProps = (Get-PodeLogger -Name $name).Properties.Username.Split('.') @@ -336,13 +573,27 @@ function Write-PodeRequestLog { } } - # add the item to be processed - $null = $PodeContext.LogsToProcess.Add(@{ + # Add the item to be processed + $null = [Pode.PodeLogger]::Enqueue(@{ Name = $name Item = $item }) } + +<# +.SYNOPSIS +Adds request logging endware to a Pode web event. + +.DESCRIPTION +This function adds endware to a Pode web event for logging request and response details. It checks if request logging is enabled and configured before attaching the logging logic to the web event's end handler. + +.PARAMETER WebEvent +The Pode web event to which the logging endware will be added. + +.EXAMPLE +Add-PodeRequestLogEndware -WebEvent $webEvent +#> function Add-PodeRequestLogEndware { param( [Parameter(Mandatory = $true)] @@ -350,13 +601,13 @@ function Add-PodeRequestLogEndware { $WebEvent ) - # do nothing if logging is disabled, or request logging isn't setup - $name = Get-PodeRequestLoggingName + # Do nothing if logging is disabled, or request logging isn't set up + $name = [Pode.PodeLogger]::RequestLogName if (!(Test-PodeLoggerEnabled -Name $name)) { return } - # add the request logging endware + # Add the request logging endware $WebEvent.OnEnd += @{ Logic = { Write-PodeRequestLog -Request $WebEvent.Request -Response $WebEvent.Response -Path $WebEvent.Path @@ -364,87 +615,135 @@ function Add-PodeRequestLogEndware { } } +<# +.SYNOPSIS +Tests if any loggers are configured or if logging is enabled. + +.DESCRIPTION +This function checks if any loggers are configured or if logging is enabled within the Pode context. It returns a boolean value indicating the presence of configured loggers or the status of logging. + +.EXAMPLE +Test-PodeLoggersExist +#> function Test-PodeLoggersExist { - if (($null -eq $PodeContext.Server.Logging) -or ($null -eq $PodeContext.Server.Logging.Types)) { + # Check if the logging context or logging types are null + if (($null -eq $PodeContext.Server.Logging) -or ($null -eq $PodeContext.Server.Logging.Type)) { return $false } - return (($PodeContext.Server.Logging.Types.Count -gt 0) -or ($PodeContext.Server.Logging.Enabled)) + # Return true if there are any logging types configured or if logging is enabled + return (($PodeContext.Server.Logging.Type.Count -gt 0) -or ($PodeContext.Server.Logging.Enabled)) } -function Start-PodeLoggingRunspace { - # skip if there are no loggers configured, or logging is disabled +<# +.SYNOPSIS +Starts the Pode logger dispatcher which processes and dispatches log entries. + +.DESCRIPTION +This function initializes and starts a logger dispatcher runspace that processes log entries from a queue and dispatches them to the appropriate logging methods. It handles batching of log entries and ensures that log entries are processed in a timely manner. + +.EXAMPLE +Start-PodeLoggerDispatcher +#> +function Start-PodeLoggerDispatcher { + # Skip if there are no loggers configured, or logging is disabled if (!(Test-PodeLoggersExist)) { return } - $script = { + $scriptBlock = { + + $log = @{} + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start try { while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { - # Check for suspension token and wait for the debugger to reset if active - Test-PodeSuspensionToken - try { - # if there are no logs to process, just sleep for a few seconds - but after checking the batch - if ($PodeContext.LogsToProcess.Count -eq 0) { - Test-PodeLoggerBatch - Start-Sleep -Seconds 5 - continue + # Check if the log queue has reached its limit + if ([Pode.PodeLogger]::Count -ge $PodeContext.Server.Logging.QueueLimit) { + Invoke-PodeHandleFailure -Message "Reached the log Queue Limit of $($PodeContext.Server.Logging.QueueLimit)" -FailureAction $logger.Method.Arguments.FailureAction } - # safely pop off the first log from the array - $log = (Lock-PodeObject -Return -Object $PodeContext.LogsToProcess -ScriptBlock { - $log = $PodeContext.LogsToProcess[0] - $null = $PodeContext.LogsToProcess.RemoveAt(0) - return $log - }) + # Try to dequeue a log entry from the queue + if ( [Pode.PodeLogger]::TryDequeue([ref]$log)) { + # If the log is null, check batch then sleep and skip + if ($null -eq $log) { + Start-Sleep -Milliseconds 100 + continue + } + if ($log.Name -eq [pode.PodeLogger]::ListenerLogName) { - # run the log item through the appropriate method - $logger = Get-PodeLogger -Name $log.Name - $now = [datetime]::Now + if ($log.Item -is [System.Exception]) { + Write-PodeErrorLog -Exception $log.Item -Level 'Error' -ThreadId $log.Item.ThreadId + } + else { + Write-PodeLog -Message $log.Item.Message -ThreadId $log.Item.ThreadId -Tag 'Listener' -Level $log.Item.Level - # if the log is null, check batch then sleep and skip - if ($null -eq $log) { - Start-Sleep -Milliseconds 100 - continue - } + } + continue + } - # convert to log item into a writable format - $rawItems = $log.Item - $_args = @($log.Item) + @($logger.Arguments) - $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat) - - # check batching - $batch = $logger.Method.Batch - if ($batch.Size -gt 1) { - # add current item to batch - $batch.Items += $result - $batch.RawItems += $log.Item - $batch.LastUpdate = $now - - # if the current amount of items matches the batch, write - $result = $null - if ($batch.Items.Length -ge $batch.Size) { - $result = $batch.Items - $rawItems = $batch.RawItems + # Run the log item through the appropriate method + $logger = $PodeContext.Server.Logging.Type[$log.Name] + $now = [datetime]::Now + + # Convert the log item into a writable format + $rawItem = $log.Item + $_args = @($log.Item) + @($logger.Arguments) + + $item = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat) + + # Check batching + $batch = $logger.Method.Batch + if ($batch.Size -gt 1) { + # Add current item to batch + $batch.Items += $item + $batch.RawItems += $log.Item + $batch.LastUpdate = $now + + # If the current amount of items matches the batch size, write + $item = $null + if ($batch.Items.Length -ge $batch.Size) { + $item = $batch.Items + $rawItem = $batch.RawItems + } + + # If we're writing, reset the items + if ($null -ne $item) { + $batch.Items = @() + $batch.RawItems = @() + } } - # if we're writing, reset the items - if ($null -ne $result) { - $batch.Items = @() - $batch.RawItems = @() + # Send the writable log item off to the log writer + if ($null -ne $item) { + foreach ($method in $logger.Method) { + if ($method.NoRunspace) { + # Legacy for custom methods + # $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Logging.Method[$method.Id].ScriptBlock -Arguments $_args -UsingVariables $method.UsingVariables -Splat + $_args = @(, $item) + @($method.Arguments) + @(, $rawItem) + $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat + } + else { + $_args = @{ + Item = $item + Options = $method.Arguments + RawItem = $rawItem + } + $PodeContext.Server.Logging.Method[$method.Id].Queue.Enqueue($_args) + } + } } - } - # send the writable log item off to the log writer - if ($null -ne $result) { - $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) - $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat + # Small sleep to lower CPU usage + Start-Sleep -Milliseconds 100 + } + else { + # Check the logger batch + Test-PodeLoggerBatch + Start-Sleep -Seconds 5 } - - # small sleep to lower cpu usage - Start-Sleep -Milliseconds 100 } catch { $_ | Write-PodeErrorLog @@ -458,9 +757,25 @@ function Start-PodeLoggingRunspace { $_ | Write-PodeErrorLog throw $_.Exception } + } - Add-PodeRunspace -Type Main -Name 'Logging' -ScriptBlock $script + # Retrieve unique method IDs + $uniqueMethodIds = ($PodeContext.Server.Logging.Type.values.Method.Id | Select-Object -Unique) + if ($uniqueMethodIds.Count -gt 0) { + # Set maximum runspaces for the logs pool + if ($PodeContext.RunspacePools['logs'].Pool.SetMaxRunspaces($uniqueMethodIds.Count + 1)) { + foreach ($methodId in $uniqueMethodIds) { + if ($null -ne $PodeContext.Server.Logging.Method[$methodId]) { + $PodeContext.Server.Logging.Method[$methodId].Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + $PodeContext.Server.Logging.Method[$methodId].Runspace = Add-PodeRunspace -PassThru -Type Logs -ScriptBlock $PodeContext.Server.Logging.Method[$methodId].ScriptBlock -Parameters @{ MethodId = $methodId } -Name 'Method' | Out-Null + } + } + } + } + + # Add the logger dispatcher runspace + Add-PodeRunspace -Type Logs -ScriptBlock $scriptBlock -Name 'Dispatcher' } <# @@ -477,7 +792,7 @@ function Test-PodeLoggerBatch { $now = [datetime]::Now # check each logger, and see if its batch needs to be written - foreach ($logger in $PodeContext.Server.Logging.Types.Values) { + foreach ($logger in $PodeContext.Server.Logging.Type.Values) { $batch = $logger.Method.Batch if (($batch.Size -gt 1) -and ($batch.Items.Length -gt 0) -and ($batch.Timeout -gt 0) ` -and ($null -ne $batch.LastUpdate) -and ($batch.LastUpdate.AddSeconds($batch.Timeout) -le $now) @@ -493,3 +808,221 @@ function Test-PodeLoggerBatch { } } } + +<# +.SYNOPSIS + Creates a new log batch information object. + +.DESCRIPTION + The `New-PodeLogBatchInfo` function initializes and returns a hashtable that contains the details of a log batch, + including a unique batch identifier, size, timeout, and placeholders for items to be logged. + +.OUTPUTS + [hashtable] + Returns a hashtable with the following keys: + - `Id`: A unique identifier for the log batch, generated using `New-PodeGuid`. + - `Size`: The number of log items to be batched. + - `Timeout`: The timeout (in seconds) for sending log items if a new log isn't received. + - `LastUpdate`: Initially set to `$null`, this tracks the last time the batch was updated. + - `Items`: An empty array to hold formatted log items. + - `RawItems`: An empty array to hold unformatted/raw log items. + +.EXAMPLE + $logBatch = New-PodeLogBatchInfo -Batch 10 -BatchTimeout 30 + + This creates a new log batch with a size of 10 items and a timeout of 30 seconds before the batch is processed. + +.NOTES + This function is used for batching log items before they are processed. The size and timeout determine + how many items or how much time can pass before a batch of logs is processed. + + This is an internal function and may change in future releases of Pode. +#> + +function New-PodeLogBatchInfo { + # batch details + return @{ + Id = New-PodeGuid + Size = $Batch + Timeout = $BatchTimeout + LastUpdate = $null + Items = @() + RawItems = @() + } +} + +<# +.SYNOPSIS + Tests whether a given date format string is valid. + +.DESCRIPTION + The `Test-PodeDateFormat` function checks if a provided date format string can successfully format and parse a date. + It uses the current date and time to validate the format. If the format is valid, it returns `$true`. + If the format is invalid, it returns `$false`. + +.PARAMETER DateFormat + The date format string to be tested. This can be any custom date format supported by .NET. + +.EXAMPLE + Test-PodeDateFormat -DateFormat 'yyyy-MM-dd' + + This command checks if the 'yyyy-MM-dd' date format is valid and returns `$true` if it is, or `$false` if it isn't. + +.EXAMPLE + Test-PodeDateFormat -DateFormat 'invalidFormat' + + This command tests the string 'invalidFormat' as a date format and returns `$false` since it's not a valid format. + +.OUTPUTS + [bool] + Returns `$true` if the provided date format string is valid, otherwise returns `$false`. + +.NOTES + This function attempts to format and then parse the current date using the provided date format string. + If an exception is thrown during the process, the format is deemed invalid. + + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeDateFormat { + param ( + [string]$DateFormat + ) + + $sampleDate = [DateTime]::Now + try { + # Try to format the sample date using the provided format + $formattedDate = $sampleDate.ToString($DateFormat) + + # Try to parse the formatted date back to a DateTime object using the same format + [DateTime]::ParseExact($formattedDate, $DateFormat, $null) + + # If no exceptions are thrown, the format is valid + return $true + } + catch { + # If an exception is thrown, the format is invalid + return $false + } +} + + + +function Enable-PodeLoggingInternal { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true )] + [hashtable[]] + $Method, + + [Parameter(Mandatory = $true)] + [ValidateSet('Errors', 'Default' )] + [string] + $Type, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Verbose', 'Debug', '*')] + [string[]] + $Levels , + + [switch] + $Raw + ) + switch ($Type.ToLowerInvariant()) { + + 'errors' { + $name = [Pode.PodeLogger]::ErrorLogName + $scriptBlock = (Get-PodeLoggingInbuiltType -Type Errors) + } + 'Default' { + $name = [Pode.PodeLogger]::DefaultLogName + $scriptBlock = (Get-PodeLoggingInbuiltType -Type Default) + } + } + # error if it's already enabled + if ($PodeContext.Server.Logging.Type.Contains($Name)) { + # Error Logging has already been enabled + throw ($PodeLocale.loggingAlreadyEnabledExceptionMessage -f 'Error') + } + # all errors? + if ($Levels -contains '*') { + $Levels = @('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Verbose', 'Debug') + } + + # add the error logger + $PodeContext.Server.Logging.Type[$name] = @{ + Method = $Method + ScriptBlock = $scriptBlock + Arguments = @{ + Raw = $Raw.IsPresent + Levels = $Levels + DataFormat = $Method.Arguments.DataFormat + } + Standard = $true + } + + $Method.ForEach({ $_.Logger += $name }) + + return $PodeContext.Server.Logging.Type[$name] + +} + +<# +.SYNOPSIS + Handles failure actions based on the provided parameters. + +.DESCRIPTION + This function processes failure scenarios by either ignoring the failure, + reporting it on the console and continuing, or reporting it on the console + and halting the server. The behavior is controlled by the 'FailureAction' + parameter. + +.PARAMETER Message + The message to be displayed in case of a failure. + +.PARAMETER FailureAction + Specifies the action to take in case of failure. Accepted values are: + - 'Ignore': Do nothing and continue execution. + - 'Report': Display the message on the console and continue execution. + - 'Halt': Display the message on the console and halt the server. + +.EXAMPLE + Invoke-PodeHandleFailure -Message "An error occurred." -FailureAction "Report" + This will display the message "An error occurred." on the console and continue execution. + +.EXAMPLE + Invoke-PodeHandleFailure -Message "Critical failure." -FailureAction "Halt" + This will display the message "Critical failure." on the console and halt the server. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Invoke-PodeHandleFailure { + param( + [Parameter(Mandatory = $true)] + [string] + $Message, + + [Parameter(Mandatory = $true)] + [string] + [ValidateSet('Ignore', 'Report', 'Halt' )] + $FailureAction + + ) + switch ($FailureAction.ToLowerInvariant()) { + 'ignore' { + # Do nothing and continue + } + 'report' { + # Report on console and continue + Write-PodeHost $Message -ForegroundColor Yellow + } + 'halt' { + # Report on console and halt + Write-PodeHost $Message -ForegroundColor Red + Write-PodeHost 'Pode Server shutting down.' -ForegroundColor Red + Close-PodeServer + } + } +} + diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 25cdfb85f..4697ea40e 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -88,8 +88,8 @@ function Start-PodeWebServer { # Create the listener $listener = & $("New-Pode$($PodeContext.Server.ListenerType)Listener") -CancellationToken $PodeContext.Tokens.Cancellation.Token - $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $listener.ErrorLoggingEnabled = (Test-PodeLoggerEnabled -Type Error) + $listener.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeLoggerLevel -Type Error) } else { @() } $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize $listener.ShowServerDetails = [bool]$PodeContext.Server.Security.ServerDetails diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 index da4bd0b46..b7845273a 100644 --- a/src/Private/Runspaces.ps1 +++ b/src/Private/Runspaces.ps1 @@ -42,7 +42,6 @@ function Add-PodeRunspace { param( [Parameter(Mandatory = $true)] - [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files', 'Timers')] [string] $Type, diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 0725b2e67..ed0b76c6c 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -98,7 +98,7 @@ function Start-PodeInternalServer { if (!$PodeContext.Server.IsServerless) { # start runspace for loggers - Start-PodeLoggingRunspace + Start-PodeLoggerDispatcher # start runspace for schedules Start-PodeScheduleRunspace @@ -255,7 +255,7 @@ function Restart-PodeInternalServer { $PodeContext.Server.Views.Clear() $PodeContext.Timers.Items.Clear() - $PodeContext.Server.Logging.Types.Clear() + Clear-PodeLogging # clear schedules $PodeContext.Schedules.Items.Clear() diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index e6ce3a762..eaef6a644 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -66,8 +66,8 @@ function Start-PodeSmtpServer { # create the listener $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token) - $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $listener.ErrorLoggingEnabled = (Test-PodeLoggerEnabled -Type Error) + $listener.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeLoggerLevel -Type Error) } else { @() } $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 12b829d1b..89e1918d2 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -60,8 +60,8 @@ function Start-PodeTcpServer { # create the listener $listener = [PodeListener]::new($PodeContext.Tokens.Cancellation.Token) - $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $listener.ErrorLoggingEnabled = (Test-PodeLoggerEnabled -Type Error) + $listener.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeLoggerLevel -Type Error) } else { @() } $listener.RequestTimeout = $PodeContext.Server.Request.Timeout $listener.RequestBodySize = $PodeContext.Server.Request.BodySize diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index cb7c92928..19ae4445d 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -21,8 +21,8 @@ function New-PodeWebSocketReceiver { try { $receiver = [PodeReceiver]::new($PodeContext.Tokens.Cancellation.Token) - $receiver.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) - $receiver.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) + $receiver.ErrorLoggingEnabled = (Test-PodeLoggerEnabled -Type Error) + $receiver.ErrorLoggingLevels = If ( $listener.ErrorLoggingEnabled) { @(Get-PodeLoggerLevel -Type Error) } else { @() } $PodeContext.Server.WebSockets.Receiver = $receiver $PodeContext.Receivers += $receiver } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index ef83806eb..519cd43f1 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -290,6 +290,10 @@ function Start-PodeServer { # Call the function using splatting Set-PodeConsoleOverrideConfiguration @ConfigParameters + if ($PodeContext.Server.Logging.Enabled) { + Enable-PodeLog + } + # start the file monitor for interally restarting Start-PodeFileMonitor diff --git a/src/Public/EndWare.ps1 b/src/Public/EndWare.ps1 new file mode 100644 index 000000000..191f22a2b --- /dev/null +++ b/src/Public/EndWare.ps1 @@ -0,0 +1,65 @@ +<# +.SYNOPSIS +Adds a ScriptBlock as Endware to run at the end of each web Request. + +.DESCRIPTION +Adds a ScriptBlock as Endware to run at the end of each web Request. + +.PARAMETER ScriptBlock +The ScriptBlock to add. It will be supplied the current web event. + +.PARAMETER ArgumentList +An array of arguments to supply to the Endware's ScriptBlock. + +.EXAMPLE +Add-PodeEndware -ScriptBlock { /* logic */ } +#> +function Add-PodeEndware { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [object[]] + $ArgumentList + ) + + # check for scoped vars + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + + # add the scriptblock to array of endware that needs to be run + $PodeContext.Server.Endware += @{ + Logic = $ScriptBlock + UsingVariables = $usingVars + Arguments = $ArgumentList + } +} + +<# +.SYNOPSIS +Automatically loads endware ps1 files + +.DESCRIPTION +Automatically loads endware ps1 files from either a /endware folder, or a custom folder. Saves space dot-sourcing them all one-by-one. + +.PARAMETER Path +Optional Path to a folder containing ps1 files, can be relative or literal. + +.EXAMPLE +Use-PodeEndware + +.EXAMPLE +Use-PodeEndware -Path './endware' +#> +function Use-PodeEndware { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Path + ) + + Use-PodeFolder -Path $Path -DefaultPath 'endware' +} \ No newline at end of file diff --git a/src/Public/Logging.ps1 b/src/Public/Logging.ps1 index 727a1fcd3..7794501ef 100644 --- a/src/Public/Logging.ps1 +++ b/src/Public/Logging.ps1 @@ -1,15 +1,30 @@ + + + <# .SYNOPSIS -Create a new method of outputting logs. + Creates a new method for outputting logs (Deprecated). .DESCRIPTION -Create a new method of outputting logs. + This function has been deprecated and will be removed in future versions. It creates various logging methods such as Terminal, File, Event Viewer, and Custom logging. + Please use the appropriate new functions for each logging method: + - `New-PodeTerminalLoggingMethod` for terminal logging. + - `New-PodeFileLoggingMethod` for file logging. + - `New-PodeEventViewerLoggingMethod` for Event Viewer logging. + - `New-PodeCustomLoggingMethod` for custom logging. .PARAMETER Terminal -If supplied, will use the inbuilt Terminal logging output method. + Deprecated. Please use `New-PodeTerminalLoggingMethod` instead. + If supplied, will use the inbuilt Terminal logging output method. .PARAMETER File -If supplied, will use the inbuilt File logging output method. + Deprecated. Please use `New-PodeFileLoggingMethod` instead. + If supplied, will use the inbuilt File logging output method. + +.PARAMETER EventViewer + Deprecated. Please use `New-PodeEventViewerLoggingMethod` instead. + If supplied, will use the inbuilt Event Viewer logging output method. + .PARAMETER Path The File Path of where to store the logs. @@ -148,106 +163,88 @@ function New-PodeLoggingMethod { $ArgumentList ) - # batch details - $batchInfo = @{ - Size = $Batch - Timeout = $BatchTimeout - LastUpdate = $null - Items = @() - RawItems = @() - } # return info on appropriate logging type switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { 'terminal' { - return @{ - ScriptBlock = (Get-PodeLoggingTerminalMethod) - Batch = $batchInfo - Arguments = @{} - } + # WARNING: Function `New-PodeLoggingMethod` is deprecated. Please use '{0}' function instead. + Write-PodeHost ($PodeLocale.deprecatedFunctionWarningMessage -f 'New-PodeLoggingMethod', 'New-PodeTerminalLoggingMethod') -ForegroundColor Yellow + + return New-PodeTerminalLoggingMethod } 'file' { - $Path = (Protect-PodeValue -Value $Path -Default './logs') - $Path = (Get-PodeRelativePath -Path $Path -JoinRoot) - $null = New-Item -Path $Path -ItemType Directory -Force - - return @{ - ScriptBlock = (Get-PodeLoggingFileMethod) - Batch = $batchInfo - Arguments = @{ - Name = $Name - Path = $Path - MaxDays = $MaxDays - MaxSize = $MaxSize - FileId = 0 - Date = $null - NextClearDown = [datetime]::Now.Date - } + # WARNING: Function `New-PodeLoggingMethod` is deprecated. Please use '{0}' function instead. + Write-PodeHost ($PodeLocale.deprecatedFunctionWarningMessage -f 'New-PodeLoggingMethod', 'New-PodeFileLoggingMethod') -ForegroundColor Yellow + + $fileParams = @{ + Path = $PSBoundParameters['Path'] + Name = $PSBoundParameters['Name'] + MaxDays = $PSBoundParameters['MaxDays'] + MaxSize = $PSBoundParameters['MaxSize'] } + return New-PodeFileLoggingMethod @fileParams } 'eventviewer' { - # only windows - if (!(Test-PodeIsWindows)) { - # Event Viewer logging only supported on Windows - throw ($PodeLocale.eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage) - } - - # create source - if (![System.Diagnostics.EventLog]::SourceExists($Source)) { - $null = [System.Diagnostics.EventLog]::CreateEventSource($Source, $EventLogName) - } + # WARNING: Function `New-PodeLoggingMethod` is deprecated. Please use '{0}' function instead. + Write-PodeHost ($PodeLocale.deprecatedFunctionWarningMessage -f 'New-PodeLoggingMethod', 'New-PodeEventViewerLoggingMethod') -ForegroundColor Yellow - return @{ - ScriptBlock = (Get-PodeLoggingEventViewerMethod) - Batch = $batchInfo - Arguments = @{ - LogName = $EventLogName - Source = $Source - ID = $EventID - } + $eventViewerParams = @{ + EventLogName = $PSBoundParameters['EventLogName'] + Source = $PSBoundParameters['Source'] + EventID = $PSBoundParameters['EventID'] } + return New-PodeEventViewerLoggingMethod @eventViewerParams } 'custom' { + # WARNING: Function `New-PodeLoggingMethod` is deprecated. Please use '{0}' function instead. + Write-PodeHost ($PodeLocale.deprecatedFunctionWarningMessage -f 'New-PodeLoggingMethod', 'New-PodeCustomLoggingMethod') -ForegroundColor Yellow + + # Convert scoped variables for the script block if not using a runspace $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState return @{ + Id = New-PodeGuid ScriptBlock = $ScriptBlock UsingVariables = $usingVars - Batch = $batchInfo + Batch = New-PodeLogBatchInfo + Logger = @() Arguments = $ArgumentList + NoRunspace = $true } } } } - <# .SYNOPSIS -Enables Request Logging using a supplied output method. + Enables Request Logging using a supplied output method. .DESCRIPTION -Enables Request Logging using a supplied output method. + Enables Request Logging using a supplied output method. .PARAMETER Method -The Method to use for output the log entry (From New-PodeLoggingMethod). + The Method to use for output the log entry (From New-PodeLoggingMethod). .PARAMETER UsernameProperty -An optional property path within the $WebEvent.Auth.User object for the user's Username. (Default: Username). + An optional property path within the $WebEvent.Auth.User object for the user's Username. (Default: Username). .PARAMETER Raw -If supplied, the log item returned will be the raw Request item as a hashtable and not a string (for Custom methods). + If supplied, the log item returned will be the raw Request item as a hashtable and not a string. + +.PARAMETER LogFormat + The format to use for the log entries. Options are: Extended, Common, Combined, JSON (Default: Combined). .EXAMPLE -New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging #> function Enable-PodeRequestLogging { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [hashtable] + [hashtable[]] $Method, [Parameter()] @@ -255,126 +252,318 @@ function Enable-PodeRequestLogging { $UsernameProperty, [switch] - $Raw + $Raw, + + [string] + [ValidateSet('Extended', 'Common', 'Combined', 'JSON' )] + $LogFormat = 'Combined' ) + begin { + $pipelineMethods = @() - Test-PodeIsServerless -FunctionName 'Enable-PodeRequestLogging' -ThrowError + Test-PodeIsServerless -FunctionName 'Enable-PodeRequestLogging' -ThrowError - $name = Get-PodeRequestLoggingName + $name = [Pode.PodeLogger]::RequestLogName - # error if it's already enabled - if ($PodeContext.Server.Logging.Types.Contains($name)) { - # Request Logging has already been enabled - throw ($PodeLocale.requestLoggingAlreadyEnabledExceptionMessage) - } + # error if it's already enabled + if ($PodeContext.Server.Logging.Type.Contains($name)) { + # Request Logging has already been enabled + throw ($PodeLocale.loggingAlreadyEnabledExceptionMessage -f 'Request') + } - # ensure the Method contains a scriptblock - if (Test-PodeIsEmpty $Method.ScriptBlock) { - # The supplied output Method for Request Logging requires a valid ScriptBlock - throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Request') + # username property + if ([string]::IsNullOrWhiteSpace($UsernameProperty)) { + $UsernameProperty = 'Username' + } } - - # username property - if ([string]::IsNullOrWhiteSpace($UsernameProperty)) { - $UsernameProperty = 'Username' + process { + # ensure the Method contains a scriptblock + if ((! $PodeContext.Server.Logging.Method.ContainsKey($_.Id)) -and (! $_.ContainsKey('Scriptblock'))) { + # The supplied output Method for Request Logging requires a valid ScriptBlock + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Request') + } + $pipelineMethods += $_ } + end { - # add the request logger - $PodeContext.Server.Logging.Types[$name] = @{ - Method = $Method - ScriptBlock = (Get-PodeLoggingInbuiltType -Type Requests) - Properties = @{ - Username = $UsernameProperty + if ($pipelineMethods.Count -gt 1) { + $Method = $pipelineMethods } - Arguments = @{ - Raw = $Raw + + # add the request logger + $PodeContext.Server.Logging.Type[$name] = @{ + Method = $Method + ScriptBlock = (Get-PodeLoggingInbuiltType -Type Requests) + Properties = @{ + Username = $UsernameProperty + } + Arguments = @{ + Raw = $Raw.IsPresent + DataFormat = $Method.Arguments.DataFormat + LogFormat = $LogFormat + } + Standard = $true } + + $Method.ForEach({ $_.Logger += $name }) } } + <# .SYNOPSIS -Disables Request Logging. + Enables Error Logging using a supplied output method. .DESCRIPTION -Disables Request Logging. + Enables Error Logging using a supplied output method. + +.PARAMETER Method + The Method to use for output the log entry (From New-PodeLoggingMethod). + +.PARAMETER Levels + The Levels of errors that should be logged (default is Error). + +.PARAMETER Raw + If supplied, the log item returned will be the raw Error item as a hashtable and not a string (for Custom methods). + +.PARAMETER DisableDefaultLog + If supplied, the error logs will NOT be duplicated to the default logging method. .EXAMPLE -Disable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging #> -function Disable-PodeRequestLogging { +function Enable-PodeErrorLogging { [CmdletBinding()] - param() + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable[]] + $Method, - Remove-PodeLogger -Name (Get-PodeRequestLoggingName) + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Verbose', 'Debug', '*')] + [string[]] + $Levels = @('Error'), + + [switch] + $Raw, + + [switch] + $DisableDefaultLog + ) + + begin { + $pipelineMethods = @() + } + + process { + # ensure the Method contains a scriptblock + if ((! $PodeContext.Server.Logging.Method.ContainsKey($_.Id)) -and (! $_.ContainsKey('Scriptblock'))) { + # The supplied output Method for Error Logging requires a valid ScriptBlock + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Error') + } + $pipelineMethods += $_ + } + + end { + + if ($pipelineMethods.Count -gt 1) { + $Method = $pipelineMethods + } + + $logging = Enable-PodeLoggingInternal -Method $Method -Type Errors -Levels $Levels -Raw:$Raw + + + $logging.DuplicateToDefaultLog = ! $DisableDefaultLog.IsPresent + $Method.ForEach({ $_.Logger += $name }) + } } <# .SYNOPSIS -Enables Error Logging using a supplied output method. + Enables Default Logging using a supplied output method. .DESCRIPTION -Enables Error Logging using a supplied output method. + Enables Default Logging using a supplied output method. .PARAMETER Method -The Method to use for output the log entry (From New-PodeLoggingMethod). + The Method to use for output the log entry (From New-PodeLoggingMethod). .PARAMETER Levels -The Levels of errors that should be logged (default is Error). + The Levels that should be logged (default is 'Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational'). .PARAMETER Raw -If supplied, the log item returned will be the raw Error item as a hashtable and not a string (for Custom methods). + If supplied, the log item returned will be the raw Default item as a hashtable and not a string (for Custom methods). .EXAMPLE -New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + New-PodeLoggingMethod -Terminal | Enable-PodeDefaultLogging #> -function Enable-PodeErrorLogging { +function Enable-PodeDefaultLogging { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [hashtable] + [hashtable[]] $Method, [Parameter()] [ValidateNotNullOrEmpty()] - [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug', '*')] + [ValidateSet('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Verbose', 'Debug', '*')] [string[]] - $Levels = @('Error'), + $Levels = @('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational'), [switch] $Raw ) - $name = Get-PodeErrorLoggingName + begin { + $pipelineMethods = @() + } - # error if it's already enabled - if ($PodeContext.Server.Logging.Types.Contains($name)) { - # Error Logging has already been enabled - throw ($PodeLocale.errorLoggingAlreadyEnabledExceptionMessage) + process { + # ensure the Method contains a scriptblock + if ((! $PodeContext.Server.Logging.Method.ContainsKey($_.Id)) -and (! $_.ContainsKey('Scriptblock'))) { + # The supplied output Method for Error Logging requires a valid ScriptBlock + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Error') + } + $pipelineMethods += $_ } - # ensure the Method contains a scriptblock - if (Test-PodeIsEmpty $Method.ScriptBlock) { - # The supplied output Method for Error Logging requires a valid ScriptBlock - throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Error') + end { + + if ($pipelineMethods.Count -gt 1) { + $Method = $pipelineMethods + } + Enable-PodeLoggingInternal -Method $Method -Type Default -Levels $Levels -Raw:$Raw + $Method.ForEach({ $_.Logger += $name }) + } +} +<# +.SYNOPSIS + Enables a generic logging method in Pode. + +.DESCRIPTION + This function enables a generic logging method in Pode, allowing logs to be written based on the defined method and log levels. It ensures the method is not already enabled and validates the provided script block. + +.PARAMETER Method + The hashtable defining the logging method, including the ScriptBlock for log output. + +.PARAMETER Levels + An array of log levels to be enabled for the logging method (Default: 'Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Info', 'Verbose', 'Debug'). + +.PARAMETER Name + The name of the logging method to be enabled. + +.PARAMETER Raw + If set, the raw log data will be included in the logging output. + +.EXAMPLE + $method = New-PodeLoggingMethod -syslog -Server 127.0.0.1 -Transport UDP + $method | Add-PodeLoggingMethod -Name "mysyslog" +#> +function Add-PodeLoggingMethod { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable[]] + $Method, + + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Verbose', 'Debug', '*')] + [string[]] + $Levels = @('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational'), + + [switch] + $Raw + ) + begin { + $pipelineMethods = @() + # error if it's already enabled + if ($PodeContext.Server.Logging.Type.Contains($Name)) { + throw ($PodeLocale.loggingAlreadyEnabledExceptionMessage -f $Name) + } + + if ($Levels -contains '*') { + $Levels = @('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Info', 'Verbose', 'Debug') + } + } - # all errors? - if ($Levels -contains '*') { - $Levels = @('Error', 'Warning', 'Informational', 'Verbose', 'Debug') + process { + # ensure the Method contains a scriptblock + if ((! $PodeContext.Server.Logging.Method.ContainsKey($_.Id)) -and (! $_.ContainsKey('Scriptblock'))) { + # The supplied output Method for the '{0}' Logging method requires a valid ScriptBlock. + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f $Name) + } + $pipelineMethods += $_ } + end { + + if ($pipelineMethods.Count -gt 1) { + $Method = $pipelineMethods + } - # add the error logger - $PodeContext.Server.Logging.Types[$name] = @{ - Method = $Method - ScriptBlock = (Get-PodeLoggingInbuiltType -Type Errors) - Arguments = @{ - Raw = $Raw - Levels = $Levels + # add the error logger + $PodeContext.Server.Logging.Type[$Name] = @{ + Method = $Method + ScriptBlock = (Get-PodeLoggingInbuiltType -Type General) + Arguments = @{ + Raw = $Raw.IsPresent + Levels = $Levels + DataFormat = $Method.Arguments.DataFormat + } + Standard = $true } + + $Method.ForEach({ $_.Logger += $Name }) } } +<# +.SYNOPSIS + Disables Request Logging. + +.DESCRIPTION + Disables Request Logging. + +.EXAMPLE + Disable-PodeRequestLogging +#> +function Disable-PodeRequestLogging { + [CmdletBinding()] + param() + + Remove-PodeLogger -Name ([Pode.PodeLogger]::RequestLogName) +} + +<# +.SYNOPSIS + Disables a generic logging method in Pode. + +.DESCRIPTION + This function disables a generic logging method in Pode. + +.PARAMETER Name + The name of the logging method to be disable. + +.EXAMPLE + Remove-PodeLoggingMethod -Name 'TestLog' +#> +function Remove-PodeLoggingMethod { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + Remove-PodeLogger -Name $Name +} + <# .SYNOPSIS Disables Error Logging. @@ -389,30 +578,50 @@ function Disable-PodeErrorLogging { [CmdletBinding()] param() - Remove-PodeLogger -Name (Get-PodeErrorLoggingName) + Remove-PodeLogger -Name ([Pode.PodeLogger]::ErrorLogName) + } + <# .SYNOPSIS -Adds a custom Logging method for parsing custom log items. +Disables Default Logging. .DESCRIPTION -Adds a custom Logging method for parsing custom log items. +Disables Default Logging. + +.EXAMPLE +Disable-PodeDefaultLogging +#> +function Disable-PodeDefaultLogging { + [CmdletBinding()] + param() + + Remove-PodeLogger -Name ([Pode.PodeLogger]::DefaultLogName) + +} + +<# +.SYNOPSIS + Adds a custom Logging method for parsing custom log items. + +.DESCRIPTION + Adds a custom Logging method for parsing custom log items. .PARAMETER Name -A unique Name for the Logging method. + A unique Name for the Logging method. .PARAMETER Method -The Method to use for output the log entry (From New-PodeLoggingMethod). + The Method to use for output the log entry (From New-PodeLoggingMethod). .PARAMETER ScriptBlock -The ScriptBlock defining logic that transforms an item, and returns it for outputting. + The ScriptBlock defining logic that transforms an item, and returns it for outputting. .PARAMETER ArgumentList -An array of arguments to supply to the Custom Logger's ScriptBlock. + An array of arguments to supply to the Custom Logger's ScriptBlock. .EXAMPLE -New-PodeLoggingMethod -Terminal | Add-PodeLogger -Name 'Main' -ScriptBlock { /* logic */ } + New-PodeLoggingMethod -Terminal | Add-PodeLogger -Name 'Default' -ScriptBlock { /* logic */ } #> function Add-PodeLogger { [CmdletBinding()] @@ -442,42 +651,57 @@ function Add-PodeLogger { $ArgumentList ) - # ensure the name doesn't already exist - if ($PodeContext.Server.Logging.Types.ContainsKey($Name)) { - # Logging method already defined - throw ($PodeLocale.loggingMethodAlreadyDefinedExceptionMessage -f $Name) + Begin { + $pipelineItemCount = 0 } - # ensure the Method contains a scriptblock - if (Test-PodeIsEmpty $Method.ScriptBlock) { - # The supplied output Method for the Logging method requires a valid ScriptBlock - throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f $Name) + Process { + $pipelineItemCount++ } - # check for scoped vars - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + End { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + + # ensure the name doesn't already exist + if ($PodeContext.Server.Logging.Type.ContainsKey($Name)) { + # Logging method already defined + throw ($PodeLocale.loggingMethodAlreadyDefinedExceptionMessage -f $Name) + } + + # ensure the Method contains a scriptblock + if (Test-PodeIsEmpty $Method.ScriptBlock) { + # The supplied output Method for the Logging method requires a valid ScriptBlock + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f $Name) + } + + # check for scoped vars + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - # add logging method to server - $PodeContext.Server.Logging.Types[$Name] = @{ - Method = $Method - ScriptBlock = $ScriptBlock - UsingVariables = $usingVars - Arguments = $ArgumentList + # add logging method to server + $PodeContext.Server.Logging.Type[$Name] = @{ + Method = $Method + ScriptBlock = $ScriptBlock + UsingVariables = $usingVars + Arguments = $ArgumentList + } } } <# .SYNOPSIS -Removes a configured Logging method. + Removes a configured Logging method. .DESCRIPTION -Removes a configured Logging method. + Removes a configured Logging method by its name. + This function handles the removal of the logging method and ensures that any associated runspaces and script blocks are properly disposed of if they are no longer in use. .PARAMETER Name -The Name of the Logging method. + The Name of the Logging method. .EXAMPLE -Remove-PodeLogger -Name 'LogName' + Remove-PodeLogger -Name 'LogName' #> function Remove-PodeLogger { [CmdletBinding()] @@ -486,8 +710,47 @@ function Remove-PodeLogger { [string] $Name ) + Process { + + # Check if the specified logging type exists + if ($PodeContext.Server.Logging.Type.Contains($Name)) { + # Retrieve the method associated with the logging type + $method = $PodeContext.Server.Logging.Type[$Name].Method + # If it's not a legacy method remove the runspace + if (! $method.NoRunspace) { + # Remove the logger name from the method's logger collection + if ($method.Logger.Count -eq 1) { + $method.Logger = @() + } + else { + $method.Logger = $method.Logger | Where-Object { $_ -ne $Name } + } - $null = $PodeContext.Server.Logging.Types.Remove($Name) + # Check if there are no more loggers associated with the method + if ($method.Logger.Count -eq 0) { + # If the method's runspace is still active, stop and dispose of it + if ($PodeContext.Server.Logging.Method.ContainsKey($method.Id)) { + $PodeContext.Server.Logging.Method[$method.Id].Runspace.Pipeline.Stop() + $PodeContext.Server.Logging.Method[$method.Id].Runspace.Pipeline.Dispose() + + # Decrease the maximum runspaces for the 'logs' pool if applicable + $maxRunspaces = $PodeContext.RunspacePools['logs'].Pool.GetMaxRunspaces + if ($maxRunspaces -gt 1) { + $PodeContext.RunspacePools['logs'].Pool.SetMaxRunspaces($maxRunspaces - 1) + } + # Remove the method's script block if it exists + $PodeContext.Server.Logging.Method.Remove($method.Id) + } + } + } + + # Finally, remove the logging type from the Types collection + $null = $PodeContext.Server.Logging.Type.Remove($Name) + } + else { + throw $PodeLocale.loggerDoesNotExistExceptionMessage + } + } } <# @@ -498,165 +761,381 @@ Clears all Logging methods that have been configured. Clears all Logging methods that have been configured. .EXAMPLE -Clear-PodeLoggers +Clear-PodeLogger #> -function Clear-PodeLoggers { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] +function Clear-PodeLogger { [CmdletBinding()] param() - $PodeContext.Server.Logging.Types.Clear() + $PodeContext.Server.Logging.Type.Clear() +} + +# Create the alias for back compatibility +if (!(Test-Path Alias:Clear-PodeLoggers)) { + New-Alias Clear-PodeLoggers -Value Clear-PodeLogger } <# .SYNOPSIS -Writes and Exception or ErrorRecord using the inbuilt error logging. + Logs an Exception, ErrorRecord, or a custom error message using Pode's built-in logging mechanism. .DESCRIPTION -Writes and Exception or ErrorRecord using the inbuilt error logging. + This function logs exceptions, error records, or custom error messages with optional error categories and levels. It can also log inner exceptions and associate the error with a specific thread ID. Error levels can be set, and inner exceptions can be checked for more detailed logging. .PARAMETER Exception -An Exception to write. + The exception object to log. This is used when logging caught exceptions. .PARAMETER ErrorRecord -An ErrorRecord to write. + The error record to log. This is used when handling errors through PowerShell's error handling mechanism. .PARAMETER Level -The Level of the error being logged. + The logging level for the error. Supported levels are: Error, Warning, Informational, Verbose, Debug (Default: Error). .PARAMETER CheckInnerException -If supplied, any exceptions are check for inner exceptions. If one is present, this is also logged. + If specified, logs any inner exceptions associated with the provided exception. + +.PARAMETER ThreadId + The ID of the thread where the error occurred. If not specified, the current thread's ID is used. + +.PARAMETER Tag + A string that identifies the source application, service, or process generating the log message. + The tag helps distinguish log messages from different sources, making it easier to filter and analyze logs. Default is '-'. + +.PARAMETER SuppressDefaultLog + A switch to suppress writing the error to the default log. .EXAMPLE -try { /* logic */ } catch { $_ | Write-PodeErrorLog } + try { + # Some operation + } catch { + $_ | Write-PodeErrorLog + } .EXAMPLE -[System.Exception]::new('error message') | Write-PodeErrorLog + [System.Exception]::new('Custom error message') | Write-PodeErrorLog -CheckInnerException + #> + function Write-PodeErrorLog { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Exception')] - [System.Exception] - $Exception, + [System.Exception] $Exception, - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Error')] - [System.Management.Automation.ErrorRecord] - $ErrorRecord, + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ErrorRecord')] + [System.Management.Automation.ErrorRecord] $ErrorRecord, [Parameter()] [ValidateNotNullOrEmpty()] - [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug')] - [string] - $Level = 'Error', + [ValidateSet('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Info', 'Verbose', 'Debug' )] + [string] $Level = 'Error', [Parameter(ParameterSetName = 'Exception')] - [switch] - $CheckInnerException - ) + [switch] $CheckInnerException, - # do nothing if logging is disabled, or error logging isn't setup - $name = Get-PodeErrorLoggingName - if (!(Test-PodeLoggerEnabled -Name $name)) { - return - } + [Parameter()] + [int] $ThreadId, - # do nothing if the error level isn't present - $levels = @(Get-PodeErrorLoggingLevel) - if ($levels -inotcontains $Level) { - return - } + [string] + $Tag = '-', - # build error object for what we need - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'exception' { - $item = @{ - Category = $Exception.Source - Message = $Exception.Message - StackTrace = $Exception.StackTrace - } - } + [Parameter()] + [switch] + $SuppressDefaultLog + ) - 'error' { - $item = @{ - Category = $ErrorRecord.CategoryInfo.ToString() - Message = $ErrorRecord.Exception.Message - StackTrace = $ErrorRecord.ScriptStackTrace + Process { + $name = [Pode.PodeLogger]::ErrorLogName + if (Test-PodeLoggerEnabled -Name $name) { + Write-PodeLog @PSBoundParameters -name $name -SuppressErrorLog + if ((Test-PodeLoggerEnabled -Type Default) -and ($PodeContext.Server.Logging.Type[$name].DuplicateToDefaultLog) -and (! $SuppressDefaultLog) ) { + Write-PodeLog @PSBoundParameters -name ([Pode.PodeLogger]::DefaultLogName) -SuppressErrorLog } } } - - # add general info - $item['Server'] = $PodeContext.Server.ComputerName - $item['Level'] = $Level - $item['Date'] = [datetime]::Now - $item['ThreadId'] = [int]$ThreadId - - # add the item to be processed - $null = $PodeContext.LogsToProcess.Add(@{ - Name = $name - Item = $item - }) - - # for exceptions, check the inner exception - if ($CheckInnerException -and ($null -ne $Exception.InnerException) -and ![string]::IsNullOrWhiteSpace($Exception.InnerException.Message)) { - $Exception.InnerException | Write-PodeErrorLog - } } <# .SYNOPSIS -Write an object to a configured custom Logging method. + Writes an object, exception, or custom message to a configured custom or built-in logging method. .DESCRIPTION -Write an object to a configured custom Logging method. + This function writes an object, custom log message, or exception to a logging method in Pode. + It supports both custom and built-in logging methods, allowing structured logging with different log levels, messages, tags, and additional details like thread ID. + The logging method can be used to write errors, warnings, and informational logs in a structured manner, depending on the log level and source of the log. + Optionally, it can suppress reporting of errors to the error log if the same error is logged. .PARAMETER Name -The Name of the Logging method. + The name of the logging method (e.g., 'Console', 'File', 'Syslog'). .PARAMETER InputObject -The Object to write. + The object to write to the logging method. This is the default parameter set. + +.PARAMETER Level + The log level for the custom logging method (Default: 'Informational'). Log levels include 'Informational', 'Warning', 'Error', etc. + +.PARAMETER Message + The log message for the custom logging method. Required for custom logging. + +.PARAMETER Tag + A string that identifies the source application, service, or process generating the log message. + The tag helps distinguish log messages from different sources, making it easier to filter and analyze logs. Default is '-'. + +.PARAMETER ThreadId + The ID of the thread where the log entry is generated. If not specified, the current thread ID will be used. + +.PARAMETER Exception + An exception object to log. Required for the 'Exception' parameter set. + +.PARAMETER ErrorRecord + The error record to log. This is used when handling errors through PowerShell's error handling mechanism. + +.PARAMETER Category + The category of the custom error message (Default: NotSpecified). + +.PARAMETER CheckInnerException + If specified, any inner exceptions of the provided exception are also logged. + +.PARAMETER SuppressErrorLog + A switch to suppress writing the error to the error log if it has already been logged by this function. Useful to prevent duplicate error logging. .EXAMPLE -$object | Write-PodeLog -Name 'LogName' + $object | Write-PodeLog -Name 'LogName' + +.EXAMPLE + Write-PodeLog -Name 'CustomLog' -Level 'Error' -Message 'An error occurred.' + +.EXAMPLE + try { + # Some code that throws an exception + } catch { + Write-PodeLog -Name 'Syslog' -Exception $_ -SuppressErrorLog + } #> function Write-PodeLog { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'Message')] param( - [Parameter(Mandatory = $true)] + [Parameter()] [string] $Name, - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [object] - $InputObject + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'InputObject')] + [psobject] + $InputObject, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Exception')] + [System.Exception] + $Exception, + + [Parameter(ParameterSetName = 'Exception')] + [switch] + $CheckInnerException, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ErrorRecord')] + [System.Management.Automation.ErrorRecord] $ErrorRecord, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Error', 'Emergency', 'Alert', 'Critical', 'Warning', 'Notice', 'Informational', 'Info', 'Verbose', 'Debug')] + [string] + $Level, + + [Parameter( Mandatory = $true, ParameterSetName = 'Message')] + [string] + $Message, + + [Parameter(ParameterSetName = 'ErrorRecord')] + [Parameter(ParameterSetName = 'Message')] + [Parameter(ParameterSetName = 'Exception')] + [Parameter()] + [string] + $Tag, + + [Parameter(ParameterSetName = 'InputObject')] + [Parameter(ParameterSetName = 'Message')] + [System.Management.Automation.ErrorCategory] $Category = [System.Management.Automation.ErrorCategory]::NotSpecified, + + [Parameter()] + [int] + $ThreadId, + + [Parameter()] + [switch] + $SuppressErrorLog + ) + begin { + if ($null -eq $PodeContext.Server.Logging) { + Write-Debug 'Pode not yet initialised' + return + } + + if (!$Name) { + $Name = [Pode.PodeLogger]::DefaultLogName + } + + if (!$PodeContext.Server.Logging.Type.ContainsKey($Name)) { + throw $PodeLocale.loggerDoesNotExistExceptionMessage + } + # Get the configured log method. + $log = $PodeContext.Server.Logging.Type[$Name] - # do nothing if logging is disabled, or logger isn't setup - if (!(Test-PodeLoggerEnabled -Name $Name)) { - return } + Process { + # Define the log item based on the selected parameter set. + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'inputobject' { + if (!$Level) { $Level = 'Informational' } # Default to Informational. + if ( @(Get-PodeLoggerLevel -Name $Name) -inotcontains $Level) { return } # If the level is not configured, use the + + $logItem = @{ + Name = $Name + Level = $Levelf + } + $logItem.Item = if ( $InputObject.PSObject.TypeNames -contains 'System.Management.Automation.PSCustomObject') { + Convert-PsCustomObjectToOrderedHashtable -InputObject $InputObject + } + else { + $InputObject + } + break + } + 'message' { + if (!$Level) { $Level = 'Informational' } # Default to Informational. + if ( @(Get-PodeLoggerLevel -Name $Name) -inotcontains $Level) { return } # If the log level is not configured, return. + + $logItem = @{ + Name = $Name + Item = @{ + Server = $PodeContext.Server.ComputerName + Category = $Category.ToString() + Level = $Level + Date = if ($log.Method.Arguments.AsUTC) { [datetime]::UtcNow } else { [datetime]::Now } + Message = $Message + Tag = $Tag + ThreadId =if($ThreadId) { $ThreadId } else { [System.Threading.Thread]::CurrentThread.ManagedThreadId } + } + } + break + } + 'exception' { + if (!$Level) { $Level = 'Error' } # Default to Error. + if ( @(Get-PodeLoggerLevel -Name $Name) -inotcontains $Level) { return } # If the level is not supported, return. + + $logItem = @{ + Name = $Name + Item = @{ + Server = $PodeContext.Server.ComputerName + Level = $Level + Date = if ($log.Method.Arguments.AsUTC) { [datetime]::UtcNow } else { [datetime]::Now } + Category = $Exception.Source + Message = $Exception.Message + StackTrace = $Exception.StackTrace + Tag = $Tag + ThreadId =if($ThreadId) { $ThreadId } else { [System.Threading.Thread]::CurrentThread.ManagedThreadId } + } + } + + break + } + 'errorrecord' { + if (!$Level) { $Level = 'Error' } # Default to Error. + if ( @(Get-PodeLoggerLevel -Name $Name) -inotcontains $Level) { return } # If the level is not supported, return. + + $logItem = @{ + Name = $Name + Item = @{ + Server = $PodeContext.Server.ComputerName + Level = $Level + Date = if ($log.Method.Arguments.AsUTC) { [datetime]::UtcNow } else { [datetime]::Now } + Category = $ErrorRecord.CategoryInfo.ToString() + Message = $ErrorRecord.Exception.Message + StackTrace = $ErrorRecord.ScriptStackTrace + Tag = $Tag + ThreadId =if($ThreadId) { $ThreadId } else { [System.Threading.Thread]::CurrentThread.ManagedThreadId } + } + } + break + } + } + if ($log.Standard) { + # Add server details to the log item. + # $logItem.Item.Server = $PodeContext.Server.ComputerName + + # Add the current date and time (UTC or local) to the log item. + # $logItem.Item.Date = if ($log.Method.Arguments.AsUTC) { [datetime]::UtcNow } else { [datetime]::Now } + + # Set the thread ID if provided, otherwise use the current thread ID. + # $logItem.Item.ThreadId = if ($ThreadId) { $ThreadId } else { [System.Threading.Thread]::CurrentThread.ManagedThreadId } + + # If error logging is not suppressed, log errors or exceptions. + if ((! $SuppressErrorLog.IsPresent) -and (Test-PodeLoggerEnabled -Type Error)) { + if ($PSCmdlet.ParameterSetName.ToLowerInvariant() -eq 'exception') { + [Pode.PodeLogger]::Enqueue( @{ + Name = [Pode.PodeLogger]::ErrorLogName + Item = @{ + Server = $logItem.Item.Server + Level = $Level + Date = $(if ($PodeContext.Server.Logging.Type[$Name].Method.Arguments.AsUTC) { $logItem.Item.Date.ToUniversalTime() }else { $logItem.Item.Date.ToLocaltime() }) + Category = $Exception.Source + Message = $Exception.Message + StackTrace = $Exception.StackTrace + Tag = $Tag + ThreadId = $logItem.Item.ThreadId + } + }) - # add the item to be processed - $null = $PodeContext.LogsToProcess.Add(@{ - Name = $Name - Item = $InputObject - }) + } + elseif ($PSCmdlet.ParameterSetName.ToLowerInvariant() -eq 'errorrecord') { + [Pode.PodeLogger]::Enqueue( @{ + Name = [Pode.PodeLogger]::ErrorLogName + Item = @{ + Server = $logItem.Item.Server + Level = $Level + Date = $(if ($PodeContext.Server.Logging.Type[$Name].Method.Arguments.AsUTC) { $logItem.Item.Date.ToUniversalTime() }else { $logItem.Item.Date.ToLocaltime() }) + Category = $ErrorRecord.CategoryInfo.ToString() + Message = $ErrorRecord.Exception.Message + StackTrace = $ErrorRecord.ScriptStackTrace + Tag = $Tag + ThreadId = $logItem.Item.ThreadId + } + }) + } + elseif ($Level -eq 'Error') { + [Pode.PodeLogger]::Enqueue( @{ + Name = [Pode.PodeLogger]::ErrorLogName + Item = @{ + Server = $logItem.Item.Server + Level = $Level + Date = $(if ($PodeContext.Server.Logging.Type[$Name].Method.Arguments.AsUTC) { $logItem.Item.Date.ToUniversalTime() }else { $logItem.Item.Date.ToLocaltime() }) + Category = $Category.ToString() + Message = $Message + Tag = $Tag + ThreadId = $logItem.Item.ThreadId + } + }) + } + } + } + + # Enqueue the log item for processing. + [Pode.PodeLogger]::Enqueue($logItem) + } } <# .SYNOPSIS -Masks values within a log item to protect sensitive information. + Masks values within a log item to protect sensitive information. .DESCRIPTION -Masks values within a log item, or any string, to protect sensitive information. -Patterns, and the Mask, can be configured via the server.psd1 configuration file. + Masks values within a log item, or any string, to protect sensitive information. + Patterns, and the Mask, can be configured via the server.psd1 configuration file. .PARAMETER Item -The string Item to mask values. + The string Item to mask values. .EXAMPLE -$value = Protect-PodeLogItem -Item 'Username=Morty, Password=Hunter2' + $value = Protect-PodeLogItem -Item 'Username=Morty, Password=Hunter2' #> function Protect-PodeLogItem { [CmdletBinding()] @@ -667,54 +1146,27 @@ function Protect-PodeLogItem { $Item ) - # do nothing if there are no masks - if (Test-PodeIsEmpty $PodeContext.Server.Logging.Masking.Patterns) { - return $item - } - - # attempt to apply each mask - foreach ($mask in $PodeContext.Server.Logging.Masking.Patterns) { - if ($Item -imatch $mask) { - # has both keep before/after - if ($Matches.ContainsKey('keep_before') -and $Matches.ContainsKey('keep_after')) { - $Item = ($Item -ireplace $mask, "`${keep_before}$($PodeContext.Server.Logging.Masking.Mask)`${keep_after}") - } - - # has just keep before - elseif ($Matches.ContainsKey('keep_before')) { - $Item = ($Item -ireplace $mask, "`${keep_before}$($PodeContext.Server.Logging.Masking.Mask)") - } + Process { - # has just keep after - elseif ($Matches.ContainsKey('keep_after')) { - $Item = ($Item -ireplace $mask, "$($PodeContext.Server.Logging.Masking.Mask)`${keep_after}") - } - - # normal mask - else { - $Item = ($Item -ireplace $mask, $PodeContext.Server.Logging.Masking.Mask) - } - } + return ([pode.PodeLogger]::ProtectLogItem($Item, $PodeContext.Server.Logging.Masking)) } - - return $Item } <# .SYNOPSIS -Automatically loads logging ps1 files + Automatically loads logging ps1 files .DESCRIPTION -Automatically loads logging ps1 files from either a /logging folder, or a custom folder. Saves space dot-sourcing them all one-by-one. + Automatically loads logging ps1 files from either a /logging folder, or a custom folder. Saves space dot-sourcing them all one-by-one. .PARAMETER Path -Optional Path to a folder containing ps1 files, can be relative or literal. + Optional Path to a folder containing ps1 files, can be relative or literal. .EXAMPLE -Use-PodeLogging + Use-PodeLogging .EXAMPLE -Use-PodeLogging -Path './my-logging' + Use-PodeLogging -Path './my-logging' #> function Use-PodeLogging { [CmdletBinding()] @@ -725,4 +1177,216 @@ function Use-PodeLogging { ) Use-PodeFolder -Path $Path -DefaultPath 'logging' -} \ No newline at end of file +} + +<# +.SYNOPSIS + Enables logging in Pode. + +.DESCRIPTION + This function enables logging in Pode by setting the appropriate flags in the Pode context. + +.PARAMETER Terminal + A switch parameter that, if specified, enables terminal logging for the Pode C# listener. + +.EXAMPLE + Enable-PodeLog + This example enables all logging except terminal logging. + +.EXAMPLE + Enable-PodeLog -Terminal + This example enables all logging including terminal logging for the Pode C# listener. +#> +function Enable-PodeLog { + param( + [switch] + $Terminal + ) + + # Enable Pode logging + [pode.PodeLogger]::Enabled = $true + $PodeContext.Server.Logging.Enabled = $true + + # Enable terminal logging for the Pode C# listener if the Terminal switch is specified + [pode.PodeLogger]::Terminal = $Terminal.IsPresent +} + + +<# +.SYNOPSIS + Disables logging in Pode. + +.DESCRIPTION + This function disables logging in Pode by setting the appropriate flags in the Pode context. + It allows you to optionally keep terminal logging enabled. + +.PARAMETER KeepTerminal + A switch parameter that, if specified, keeps terminal logging enabled for the Pode C# listener even when other logging is disabled. + +.EXAMPLE + Disable-PodeLog + This example disables all logging including terminal logging. + +.EXAMPLE + Disable-PodeLog -KeepTerminal + This example disables all logging except terminal logging. +#> +function Disable-PodeLog { + param( + [switch] + $KeepTerminal + ) + + # Disable Pode logging + [pode.PodeLogger]::Enabled = $false + $PodeContext.Server.Logging.Enabled = $false + + # Optionally disable terminal logging if the KeepTerminal switch is not specified + if (! $KeepTerminal.IsPresent) { + [pode.PodeLogger]::Terminal = $false + } +} + + + +<# +.SYNOPSIS + Clears the Pode logging. + +.DESCRIPTION + This function clears all the logs in Pode by calling the Clear method on the PodeLogger class. + +.EXAMPLE + Clear-PodeLogging +#> +function Clear-PodeLogging { + $PodeContext.Server.Logging.Type.Clear() + $PodeContext.Server.Logging.Method.Clear() + [pode.PodeLogger]::Clear() +} + + +<# +.SYNOPSIS +Determines if a specified logger or a predefined log type is enabled. + +.DESCRIPTION +This function checks if logging is enabled in Pode and verifies if the specified logger or a predefined log type +(Error, Default, Request) exists within the logging configuration. + +.PARAMETER Name +The name of the logger to check. If not specified, it checks if logging is generally enabled. + +.PARAMETER Type +The type of predefined logging to check. Accepted values: 'Error', 'Default', 'Request'. + +.EXAMPLE +Test-PodeLoggerEnabled -Name 'MyCustomLogger' +# Checks if the custom logger 'MyCustomLogger' is enabled. + +.EXAMPLE +Test-PodeLoggerEnabled -Type Error +# Checks if error logging is enabled. + +.EXAMPLE +Test-PodeLoggerEnabled -Type Default +# Checks if default logging is enabled. + +.EXAMPLE +Test-PodeLoggerEnabled -Type Request +# Checks if request logging is enabled. +#> +function Test-PodeLoggerEnabled { + param( + [Parameter(Position = 0, Mandatory = $false, ParameterSetName = 'ByName')] + [string]$Name, + + [Parameter(Position = 0, Mandatory = $false, ParameterSetName = 'ByType')] + [ValidateSet('Error', 'Default', 'Request')] + [string]$Type + ) + + # Ensure logging is enabled in Pode before checking specific loggers + if (![pode.PodeLogger]::Enabled -or ($null -eq $PodeContext)) { + return $false + } + + # Determine the logger name if using a predefined log type + if ($Type) { + $Name = switch ($Type) { + 'Error' { [Pode.PodeLogger]::ErrorLogName } + 'Default' { [Pode.PodeLogger]::DefaultLogName } + 'Request' { [Pode.PodeLogger]::RequestLogName } + } + } + + # If no name is provided, return whether logging is generally enabled + if ([string]::IsNullOrEmpty($Name)) { + return $true + } + + # Check if the specified logger exists in Pode's logging configuration + return $PodeContext.Server.Logging.Type.ContainsKey($Name) +} + +<# +.SYNOPSIS + Retrieves the logging levels for a specified logger or predefined log type in Pode. + +.DESCRIPTION + This function retrieves the logging levels configured for a specified Pode logger. + It supports both predefined log types ('Error', 'Default', 'Request') and custom logger names. + +.PARAMETER Name + The name of the logger to retrieve. + +.PARAMETER Type + The type of predefined logging to retrieve levels for. Accepted values: 'Error', 'Default', 'Request'. + +.OUTPUTS + An array of logging levels. + +.EXAMPLE + Get-PodeLoggerLevel -Type Error + # Retrieves the logging levels for error logging. + +.EXAMPLE + Get-PodeLoggerLevel -Type Default + # Retrieves the logging levels for default logging. + +.EXAMPLE + Get-PodeLoggerLevel -Type Request + # Retrieves the logging levels for request logging. + +.EXAMPLE + Get-PodeLoggerLevel -Name 'MyCustomLogger' + # Retrieves the logging levels for a custom logger named 'MyCustomLogger'. +#> +function Get-PodeLoggerLevel { + param( + [Parameter(Position = 0, Mandatory = $true, ParameterSetName = 'ByName')] + [string]$Name, + + [Parameter(Position = 0, Mandatory = $true, ParameterSetName = 'ByType')] + [ValidateSet('Error', 'Default', 'Request')] + [string]$Type + ) + + # Determine the logger name if using a predefined log type + if ($Type) { + $Name = switch ($Type) { + 'Error' { [Pode.PodeLogger]::ErrorLogName } + 'Default' { [Pode.PodeLogger]::DefaultLogName } + 'Request' { [Pode.PodeLogger]::RequestLogName } + } + } + + # Ensure the logger is enabled before retrieving levels + if (!(Test-PodeLoggerEnabled -Name $Name)) { + return @() + } + + # Retrieve the logger and return its levels + $Logger = Get-PodeLogger -Name $Name + return $Logger.Arguments.Levels +} diff --git a/src/Public/LoggingMethod.ps1 b/src/Public/LoggingMethod.ps1 new file mode 100644 index 000000000..367954fae --- /dev/null +++ b/src/Public/LoggingMethod.ps1 @@ -0,0 +1,2188 @@ + +<# +.SYNOPSIS + Creates a new terminal logging method in Pode. + +.DESCRIPTION + This function sets up a logging method that outputs log messages to the terminal using Pode's internal terminal logging logic. It allows specifying a custom date format, or uses the ISO 8601 format if requested. Additionally, it supports logging time in UTC. + +.PARAMETER DataFormat + The custom date format to use for log entries. If not provided, a default format of 'dd/MMM/yyyy:HH:mm:ss zzz' is used. + This parameter is mutually exclusive with the ISO8601 parameter. + +.PARAMETER ISO8601 + If set, the date format will follow ISO 8601 (equivalent to -DataFormat 'yyyy-MM-ddTHH:mm:ssK'). + This parameter is mutually exclusive with the DataFormat parameter. + +.PARAMETER AsUTC + If set, the time will be logged in UTC instead of local time. + +.PARAMETER DefaultTag + The tag to use if none is specified on the log entry. Defaults to '-'. + +.OUTPUTS + Hashtable: Returns a hashtable containing the logging method configuration. + +.EXAMPLE + $logMethod = New-PodeTerminalLoggingMethod -DataFormat 'yyyy/MM/dd HH:mm:ss' + + Creates a terminal logging method using the specified custom date format. + +.EXAMPLE + $logMethod = New-PodeTerminalLoggingMethod -ISO8601 -AsUTC + + Creates a terminal logging method that logs messages using the ISO 8601 date format and logs the time in UTC. +#> +function New-PodeTerminalLoggingMethod { + [CmdletBinding(DefaultParameterSetName = 'DataFormat')] + [OutputType([hashtable])] + param( + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ + Test-PodeDateFormat $_ + })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-' + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + # Use ISO8601 format if specified + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' + } + default { + # Use default format if no DataFormat is provided + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Terminal logging logic + $methodId = New-PodeGuid + $PodeContext.Server.Logging.Method[$methodId] = @{ + ScriptBlock = (Get-PodeLoggingTerminalMethod) + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + } + + # Return the logging method configuration + return @{ + Type = 'Terminal' + Id = $methodId + Batch = New-PodeLogBatchInfo + Logger = @() + Arguments = @{ + DataFormat = $DataFormat + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + } + } +} + +<# +.SYNOPSIS + Creates a new file-based logging method in Pode. + +.DESCRIPTION + This function sets up a logging method that outputs log messages to a file. It supports configuring log file paths, names, formats, sizes, and retention policies, along with various log formatting options such as custom date formats or ISO 8601. + +.PARAMETER Path + The file path where the logs will be stored. Defaults to './logs'. + +.PARAMETER Name + The base name for the log files. This parameter is mandatory. + +.PARAMETER Format + The format of the log entries. Supported options are: RFC3164, RFC5424, Simple, and Default (Default: Default). + +.PARAMETER Separator + The character(s) used to separate log fields in each entry. Defaults to a space (' '). + +.PARAMETER MaxLength + The maximum length of log entries. Defaults to -1 (no limit). + +.PARAMETER MaxDays + The maximum number of days to keep log files. Logs older than this will be removed automatically. Defaults to 0 (no automatic removal). + +.PARAMETER MaxSize + The maximum size of a log file in bytes. Once this size is exceeded, a new log file will be created. Defaults to 0 (no size limit). + +.PARAMETER FailureAction + Specifies the action to take if logging fails. Options are: Ignore, Report, Halt (Default: Ignore). + +.PARAMETER DataFormat + The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER Encoding + The encoding to use for Syslog messages. Supported values are ASCII, BigEndianUnicode, Default, Unicode, UTF32, UTF7, and UTF8. Defaults to UTF8. + +.PARAMETER ISO8601 + If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.PARAMETER AsUTC + If set, logs the time in UTC instead of the local time. + +.PARAMETER DefaultTag + The tag to use if none is specified on the log entry. Defaults to '-'. + +.OUTPUTS + Hashtable: Returns a hashtable containing the logging method configuration. + + +.EXAMPLE + $logMethod = New-PodeFileLoggingMethod -Path './logs' -Name 'requests' + + Creates a new file logging method that stores logs in the './logs' directory with the base name 'requests'. + +.EXAMPLE + $logMethod = New-PodeFileLoggingMethod -Name 'requests' -MaxDays 7 -MaxSize 100MB + + Creates a file logging method that keeps logs for 7 days and creates new files once the log file reaches 100MB in size. +#> +function New-PodeFileLoggingMethod { + [CmdletBinding(DefaultParameterSetName = 'DataFormat')] + [OutputType([hashtable])] + param( + [string] + $Path = './logs', + + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [ValidateSet('RFC3164', 'RFC5424', 'Simple', 'Default')] + [string] + $Format = 'Default', + + [Parameter()] + [string] + $Separator = ' ', + + [Parameter()] + [int] + $MaxLength = -1, + + [Parameter()] + [ValidateRange(0, [int]::MaxValue)] + [int] + $MaxDays = 0, + + [Parameter()] + [ValidateRange(0, [int]::MaxValue)] + [int] + $MaxSize = 0, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ + Test-PodeDateFormat $_ + })] + [string] + $DataFormat, + + [Parameter()] + [ValidateSet('ASCII', 'BigEndianUnicode', 'Default', 'Unicode', 'UTF32', 'UTF7', 'UTF8')] + [string] + $Encoding = 'UTF8', + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-' + ) + + # Determine the date format based on the parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Resolve the log file path + $Path = (Protect-PodeValue -Value $Path -Default './logs') + $Path = (Get-PodeRelativePath -Path $Path -JoinRoot -Resolve) + if (! (Test-Path -Path $Path -PathType Leaf)) { + $null = New-Item -Path $Path -ItemType Directory -Force + } + # Create a unique ID for this logging method + $methodId = New-PodeGuid + + # Register the logging method in Pode's context + $PodeContext.Server.Logging.Method[$methodId] = @{ + ScriptBlock = (Get-PodeLoggingFileMethod) + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + } + + # Return the logging method configuration + return @{ + Type = 'File' + Id = $methodId + Batch = New-PodeLogBatchInfo + Logger = @() + Arguments = @{ + Name = $Name + Path = $Path + MaxDays = $MaxDays + MaxSize = $MaxSize + FileId = 0 + Date = $null + NextClearDown = [datetime]::Now.Date + FailureAction = $FailureAction + DataFormat = $DataFormat + AsUTC = $AsUTC.IsPresent + Encoding = $Encoding + Format = $Format + MaxLength = $MaxLength + Separator = $Separator + DefaultTag = $DefaultTag + } + } +} + +<# +.SYNOPSIS + Creates a new Event Viewer logging method in Pode. + +.DESCRIPTION + This function sets up a logging method that outputs log messages to the Windows Event Viewer. It allows configuring the log name, source, and event ID, along with date formatting options like custom formats or ISO 8601. + +.PARAMETER EventLogName + The name of the event log to write to. Defaults to 'Application'. + +.PARAMETER Source + The source of the log entries. Defaults to 'Pode'. + +.PARAMETER EventID + The ID of the event to log. Defaults to 0. + +.PARAMETER FailureAction + Specifies the action to take if logging fails. Options are: Ignore, Report, Halt (Default: Ignore). + +.PARAMETER DataFormat + The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 + If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.PARAMETER AsUTC + If set, logs the time in UTC instead of local time. + +.OUTPUTS + Hashtable: Returns a hashtable containing the logging method configuration. + +.EXAMPLE + $logMethod = New-PodeEventViewerLoggingMethod -EventLogName 'Application' -Source 'PodeApp' + + Creates a new Event Viewer logging method that writes to the 'Application' log with the source 'PodeApp'. + +.EXAMPLE + $logMethod = New-PodeEventViewerLoggingMethod -Source 'MyApp' -EventID 1001 -ISO8601 + + Creates a new Event Viewer logging method with ISO 8601 date format, writing to the 'MyApp' source and using event ID 1001. + +#> +function New-PodeEventViewerLoggingMethod { + [CmdletBinding(DefaultParameterSetName = 'DataFormat')] + [OutputType([hashtable])] + param( + [string] + $EventLogName = 'Application', + + [string] + $Source = 'Pode', + + [int] + $EventID = 0, + + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601, + + [Parameter()] + [switch] + $AsUTC + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Check if the platform is Windows + if (!(Test-PodeIsWindows)) { + # Event Viewer logging is only supported on Windows + throw ($PodeLocale.eventViewerLoggingSupportedOnWindowsOnlyExceptionMessage) + } + + # Ensure the event source exists in the Event Log + if (![System.Diagnostics.EventLog]::SourceExists($Source)) { + [System.Diagnostics.EventLog]::CreateEventSource($Source, $EventLogName) + } + + # Create the method ID and configure the logging method + $methodId = New-PodeGuid + $PodeContext.Server.Logging.Method[$methodId] = @{ + ScriptBlock = (Get-PodeLoggingEventViewerMethod) + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + } + + # Return the logging method configuration + return @{ + Type = 'EventViewer' + Id = $methodId + Batch = New-PodeLogBatchInfo + Logger = @() + Arguments = @{ + LogName = $EventLogName + Source = $Source + ID = $EventID + FailureAction = $FailureAction + DataFormat = $DataFormat + AsUTC = $AsUTC.IsPresent + Tag = $Source + } + } +} + + +<# +.SYNOPSIS + Creates a new custom logging method in Pode. + +.DESCRIPTION + This function sets up a custom logging method that uses a script block to define the logging logic. It supports the option to run the logging method in a separate runspace and allows for custom options, date formatting, and failure handling. + +.PARAMETER ScriptBlock + A non-empty script block that defines the custom logging logic. This parameter is mandatory. + +.PARAMETER ArgumentList + An array of arguments to pass to the custom script block. + +.PARAMETER CustomOptions + A hashtable of custom options that will be passed to the script block when used inside a runspace. + +.PARAMETER FailureAction + Specifies the action to take if logging fails. Options are: Ignore, Report, Halt (Default: Ignore). + +.PARAMETER DataFormat + The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 + If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.PARAMETER AsUTC + If set, logs the time in UTC instead of local time. + +.EXAMPLE + $logMethod = New-PodeCustomLoggingMethod -ScriptBlock { param($logItem) Write-Output $logItem } -UseRunspace + + Creates a custom logging method using a script block that writes log items to the output. The method runs in a separate runspace. + +.EXAMPLE + $logMethod = New-PodeCustomLoggingMethod -ScriptBlock { param($logItem) Write-Output $logItem } -DataFormat 'yyyy/MM/dd HH:mm:ss' + + Creates a custom logging method with a custom date format. + +.OUTPUTS + Hashtable: Returns a hashtable containing the custom logging method configuration. +#> +function New-PodeCustomLoggingMethod { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUSeDeclaredVarsMoreThanAssignments', '')] + [CmdletBinding(DefaultParameterSetName = 'DataFormat')] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [ValidateScript({ + if (Test-PodeIsEmpty $_) { + # A non-empty ScriptBlock is required for the Custom logging output method + throw ($PodeLocale.nonEmptyScriptBlockRequiredForCustomLoggingExceptionMessage) + } + return $true + }) + ] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [object[]] + $ArgumentList, + + [Parameter()] + [hashtable] + $CustomOptions = @{}, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601, + + [Parameter()] + [switch] + $AsUTC + ) + + # Determine the date format based on the parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Create the script block for the custom logging method running in a separate runspace + $enanchedScriptBlock = { + param($MethodId) + + $log = @{} + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + Start-Sleep -Milliseconds 100 + + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + if ($null -ne $log) { + $Item = $log.item + $Options = $log.options + $RawItem = $log.rawItem + try { + # Original ScriptBlock Start + <# ScriptBlock #> + # Original ScriptBlock End + } + catch { + Invoke-PodeHandleFailure -Message "Custom Logging $MethodId Error. message: $_" -FailureAction $options.FailureAction + } + } + } + } + } + + $methodId = New-PodeGuid + + # Register the enhanced script block in Pode's logging method + $PodeContext.Server.Logging.Method[$methodId] = @{ + ScriptBlock = [ScriptBlock]::Create($enanchedScriptBlock.ToString().Replace('<# ScriptBlock #>', $ScriptBlock.ToString())) + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + } + + return @{ + Type = 'Custom' + Id = $methodId + Batch = New-PodeLogBatchInfo + Logger = @() + Arguments = @{ + FailureAction = $FailureAction + DataFormat = $DataFormat + AsUTC = $AsUTC + } + $CustomOptions + } +} + +<# +.SYNOPSIS + Creates a new Syslog logging method in Pode. + +.DESCRIPTION + This function sets up a logging method that sends log messages to a remote Syslog server. It supports various Syslog protocols (RFC3164, RFC5424), transports (UDP, TCP, TLS), and encoding formats. The function also allows for custom date formatting or ISO 8601 compliance and can skip certificate checks for TLS connections. + +.PARAMETER Server + The Syslog server to send logs to. This parameter is mandatory. + +.PARAMETER Port + The port on the Syslog server to send logs to. Defaults to 514. + +.PARAMETER Transport + The transport protocol to use. Supported values are UDP, TCP, and TLS. Defaults to UDP. + +.PARAMETER TlsProtocol + The TLS protocol version to use if TLS transport is selected. Defaults to TLS 1.3. + +.PARAMETER SyslogProtocol + The Syslog protocol to use for message formatting. Supported values are RFC3164 and RFC5424. Defaults to RFC5424. + +.PARAMETER Encoding + The encoding to use for Syslog messages. Supported values are ASCII, BigEndianUnicode, Default, Unicode, UTF32, UTF7, and UTF8. Defaults to UTF8. + +.PARAMETER SkipCertificateCheck + If set, skips certificate validation for TLS connections. + +.PARAMETER FailureAction + Specifies the action to take if logging fails. Options are: Ignore, Report, Halt (Default: Ignore). + +.PARAMETER DataFormat + The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 + If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.PARAMETER AsUTC + If set, logs the time in UTC instead of local time. + +.PARAMETER DefaultTag + The tag to use if none is specified on the log entry. Defaults to '-'. + +.EXAMPLE + $logMethod = New-PodeSyslogLoggingMethod -Server '192.168.1.100' -Transport 'TCP' -SyslogProtocol 'RFC3164' + + Creates a new Syslog logging method that sends logs to the Syslog server at 192.168.1.100 using TCP and RFC3164 format. + +.EXAMPLE + $logMethod = New-PodeSyslogLoggingMethod -Server '192.168.1.100' -SyslogProtocol 'RFC5424' -ISO8601 -AsUTC + + Creates a Syslog logging method that uses RFC5424 format with ISO 8601 date formatting and logs time in UTC. + +.OUTPUTS + Hashtable: Returns a hashtable containing the Syslog logging method configuration. +#> +function New-PodeSyslogLoggingMethod { + [CmdletBinding(DefaultParameterSetName = 'DataFormat')] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $Server, + + [Parameter()] + [Int16] + $Port = 514, + + [Parameter()] + [ValidateSet('UDP', 'TCP', 'TLS')] + [string] + $Transport = 'UDP', + + [Parameter()] + [System.Security.Authentication.SslProtocols] + $TlsProtocol = [System.Security.Authentication.SslProtocols]::Tls13, + + [Parameter()] + [ValidateSet('RFC3164', 'RFC5424')] + [string] + $SyslogProtocol = 'RFC5424', + + [Parameter()] + [ValidateSet('ASCII', 'BigEndianUnicode', 'Default', 'Unicode', 'UTF32', 'UTF7', 'UTF8')] + [string] + $Encoding = 'UTF8', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-' + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Select encoding based on the provided value + $selectedEncoding = [System.Text.Encoding]::$Encoding + if ($null -eq $selectedEncoding) { + throw ($PodeLocale.invalidEncodingExceptionMessage -f $Encoding) + } + + # Create the method ID and configure the logging method + $methodId = New-PodeGuid + $PodeContext.Server.Logging.Method[$methodId] = @{ + ScriptBlock = (Get-PodeLoggingSysLogMethod) + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + } + + # Return the logging method configuration + return @{ + Type = 'Syslog' + Id = $methodId + Batch = New-PodeLogBatchInfo + Logger = @() + Arguments = @{ + Server = $Server + Port = $Port + Transport = $Transport + Hostname = $Hostname + TlsProtocols = $TlsProtocol + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + Format = $SyslogProtocol + Encoding = $selectedEncoding + FailureAction = $FailureAction + DataFormat = $DataFormat + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + } + } +} + +<# +.SYNOPSIS +Configures logging to AWS CloudWatch Logs. + +.DESCRIPTION +The `New-PodeAwsLoggingMethod` function configures a logging method for AWS CloudWatch Logs. It initializes a logging queue and sends log events to AWS CloudWatch using the specified log group and stream names. + +.PARAMETER BaseUrl +The base URL for the AWS CloudWatch Logs API, typically `https://logs..amazonaws.com`. + +.PARAMETER Region +The AWS region where the CloudWatch Log Group resides, such as `us-east-1`. + +.PARAMETER LogGroupName +The name of the AWS CloudWatch Log Group to send logs to. + +.PARAMETER LogStreamName +The name of the AWS CloudWatch Log Stream within the log group. + +.PARAMETER AuthorizationHeader +The AWS authorization header, generated using AWS Signature Version 4. + +.PARAMETER FailureAction +Specifies the action to take if the logging request fails. Valid values are `Ignore`, `Report`, and `Halt`. The default is `Ignore`. + +.PARAMETER SkipCertificateCheck +If present, skips SSL certificate validation when sending logs. + +.PARAMETER AsUTC +If present, converts timestamps to UTC. + +.PARAMETER DefaultTag +Sets a default tag for log entries. Defaults to `-`. + +.PARAMETER DataFormat +The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 +If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.EXAMPLE +PS> New-PodeAwsLoggingMethod -BaseUrl 'https://logs.us-east-1.amazonaws.com' -Region 'us-east-1' -LogGroupName 'MyLogGroup' -LogStreamName 'MyLogStream' -AuthorizationHeader 'AWS4-HMAC-SHA256 ...' + +Configures AWS CloudWatch logging with specified log group, log stream, and AWS authorization details. + +.NOTES +This function sends logs to AWS CloudWatch in batches, using a `ConcurrentQueue` to manage queued logs. +#> +function New-PodeAwsLoggingMethod { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $BaseUrl, + + [Parameter(Mandatory = $true)] + [string] + $Region, + + [Parameter(Mandatory = $true)] + [string] + $LogGroupName, + + [Parameter(Mandatory = $true)] + [string] + $LogStreamName, + + [Parameter(Mandatory = $true)] + [string] + $AuthorizationHeader, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601 + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Generate a unique ID for this logging method instance. + $methodId = New-PodeGuid + + # Add the logging method configuration to the PodeContext for tracking and execution. + $PodeContext.Server.Logging.Method[$methodId] = @{ + # Queue to hold log entries until they can be processed. + # Using a concurrent queue ensures thread-safe interactions in the runspace. + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + + # ScriptBlock responsible for processing log entries in a separate runspace. + # This block continuously dequeues and sends log entries to AWS CloudWatch Logs. + ScriptBlock = { + param($MethodId) # Pass the unique method ID to identify this logging configuration. + + # Temporary hashtable to hold a dequeued log entry. + $log = @{ } + + # Loop continuously until a cancellation is requested (graceful shutdown). + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + # Brief pause to reduce CPU usage in the loop. + Start-Sleep -Milliseconds 100 + + # Attempt to dequeue a log entry from the queue. + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + # Only process if a valid log entry was dequeued. + if ($null -ne $log) { + # Retrieve log data and configuration options. + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem + + # Ensure both $Item and $RawItem are treated as arrays to handle multiple log entries. + $Item = @($Item) + $RawItem = @($RawItem) + + # Define the AWS CloudWatch Logs endpoint URL. + $url = "https://logs.$($Options.Region).amazonaws.com" + + # Set up headers with the AWS authorization header and content type. + $headers = @{ + 'X-Amz-Date' = (Get-Date -Format 'yyyyMMddTHHmmssZ') # Current timestamp in AWS-required format + 'Content-Type' = 'application/x-amz-json-1.1' + 'X-Amz-Target' = 'Logs_20140328.PutLogEvents' # AWS target for CloudWatch log ingestion + 'Authorization' = $Options.AuthorizationHeader # AWS Signature v4 for authentication + } + + # Format each log entry for CloudWatch Logs. + $events = $Item | ForEach-Object { + @{ + message = ([pode.PodeLogger]::ProtectLogItem($_, $PodeContext.Server.Logging.Masking)) #($_ | Protect-PodeLogItem) # Sanitize log message content # Sanitize log message content + timestamp = [math]::Round(($RawItem.Date).ToUniversalTime().Subtract(([datetime]::UnixEpoch)).TotalMilliseconds) # Timestamp in milliseconds since epoch + } + } + + # Create the payload body for AWS CloudWatch Logs. + $body = @{ + logGroupName = $Options.LogGroupName # Target log group + logStreamName = $Options.LogStreamName # Target log stream within the group + logEvents = $events + } | ConvertTo-Json -Compress + + # Send the log data to CloudWatch Logs via HTTP POST. + try { + Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body -SkipCertificateCheck:$Options.SkipCertificateCheck + } + catch { + # Handle any failures based on the configured FailureAction (e.g., Ignore, Report, Halt). + Invoke-PodeHandleFailure -Message "Failed to send log to AWS CloudWatch Logs: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + + } + + # Return the logging method configuration as a hashtable. + return @{ + Type = 'AWS' # Specifies the type of logging platform. + Id = $methodId # Unique identifier for this logging method. + Batch = New-PodeLogBatchInfo # Contains batch information for Pode logging. + Logger = @() # Initialize an empty logger array if needed for Pode processing. + Arguments = @{ + BaseUrl = $BaseUrl + Region = $Region + LogGroupName = $LogGroupName + LogStreamName = $LogStreamName + AuthorizationHeader = $AuthorizationHeader + FailureAction = $FailureAction + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + DataFormat = $DataFormat + } + } +} + +<# +.SYNOPSIS +Configures logging to Azure Monitor Logs. + +.DESCRIPTION +The `New-PodeAzureLoggingMethod` function sets up logging for Azure Monitor Logs, allowing log data to be sent to a specified Azure Log Analytics workspace. It uses the shared key authorization method to authenticate with Azure. + +.PARAMETER WorkspaceId +The Azure Log Analytics Workspace ID. + +.PARAMETER AuthorizationHeader +The authorization header for Azure, generated using the Workspace ID and shared key. + +.PARAMETER LogType +The custom log type name in Azure Monitor. Defaults to `CustomLog`. + +.PARAMETER FailureAction +Specifies the action to take if the logging request fails. Valid values are `Ignore`, `Report`, and `Halt`. The default is `Ignore`. + +.PARAMETER SkipCertificateCheck +If present, skips SSL certificate validation when sending logs. + +.PARAMETER AsUTC +If present, converts timestamps to UTC. + +.PARAMETER DefaultTag +Sets a default tag for log entries. Defaults to `-`. + +.PARAMETER DataFormat +The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 +If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.EXAMPLE +PS> New-PodeAzureLoggingMethod -WorkspaceId '12345' -AuthorizationHeader 'SharedKey 12345:abcdef...' -LogType 'ApplicationLogs' + +Sets up Azure Monitor logging with the specified workspace ID and authorization details. + +.NOTES +This function sends logs to Azure Monitor Logs using the Azure REST API, formatted for ingestion by Azure Log Analytics. +#> +function New-PodeAzureLoggingMethod { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $WorkspaceId, + + [Parameter(Mandatory = $true)] + [string] + $AuthorizationHeader, # Azure Shared Key authorization + + [Parameter()] + [string] + $LogType = 'CustomLog', + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601 + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Generate a unique ID for this logging method instance. + $methodId = New-PodeGuid + + # Add the logging method configuration to the PodeContext for tracking and execution. + $PodeContext.Server.Logging.Method[$methodId] = @{ + # Queue to hold log entries until they can be processed. + # Using a concurrent queue ensures thread-safe interactions in the runspace. + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + + # ScriptBlock responsible for processing log entries in a separate runspace. + # This block continuously dequeues and sends log entries to Azure Monitor. + ScriptBlock = { + param($MethodId) # Pass the unique method ID to identify this logging configuration. + + # Temporary hashtable to hold a dequeued log entry. + $log = @{ } + + # Loop continuously until a cancellation is requested (graceful shutdown). + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + # Brief pause to reduce CPU usage in the loop. + Start-Sleep -Milliseconds 100 + + # Attempt to dequeue a log entry from the queue. + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + # Only process if a valid log entry was dequeued. + if ($null -ne $log) { + # Retrieve log data and configuration options. + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem + + # Ensure both $Item and $RawItem are treated as arrays to handle multiple log entries. + $Item = @($Item) + $RawItem = @($RawItem) + + # Define the Azure Monitor HTTP Data Collector API endpoint URL for the specified workspace. + $url = "https://$($Options.WorkspaceId).ods.opinsights.azure.com/api/logs?api-version=2016-04-01" + + # Set up headers, including the authorization header, log type, and time-generated field. + $headers = @{ + 'Authorization' = $Options.AuthorizationHeader # Azure Shared Key + 'Log-Type' = $Options.LogType # Specifies the Log Type name + 'x-ms-date' = (Get-Date -Format 'R') # RFC1123 date format for request header + 'time-generated-field' = 'timestamp' + } + + # Format each log entry for Azure Monitor. + $records = $Item | ForEach-Object { + @{ + message = ([pode.PodeLogger]::ProtectLogItem($_, $PodeContext.Server.Logging.Masking)) #($_ | Protect-PodeLogItem) # Sanitize log message content # Sanitize log message content + severity = $RawItem.Level.ToUpperInvariant() # Set log severity level + timestamp = $RawItem.Date.ToString('yyyy-MM-ddTHH:mm:ss.fffZ') # Format timestamp in ISO 8601 + tag = $RawItem.Tag # Include tag if provided + } + } + + # Convert the list of records to JSON format for Azure Monitor ingestion. + $body = $records | ConvertTo-Json -Compress + + # Send the log data to Azure Monitor via HTTP POST. + try { + Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body -SkipCertificateCheck:$Options.SkipCertificateCheck + } + catch { + # Handle any failures based on the configured FailureAction (e.g., Ignore, Report, Halt). + Invoke-PodeHandleFailure -Message "Failed to send log to Azure Monitor: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + } + + # Return the logging method configuration as a hashtable. + return @{ + Type = 'Azure' # Specifies the type of logging platform. + Id = $methodId # Unique identifier for this logging method. + Batch = New-PodeLogBatchInfo # Contains batch information for Pode logging. + Logger = @() # Initialize an empty logger array if needed for Pode processing. + Arguments = @{ + WorkspaceId = $WorkspaceId + AuthorizationHeader = $AuthorizationHeader + LogType = $LogType + FailureAction = $FailureAction + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + DataFormat = $DataFormat + } + } +} + +<# +.SYNOPSIS +Configures logging to Google Cloud Logging. + +.DESCRIPTION +The `New-PodeGoogleLoggingMethod` function sets up logging for Google Cloud Logging, allowing log entries to be sent to Google Cloud using the project ID and access token for authentication. + +.PARAMETER ProjectId +The Google Cloud Project ID. + +.PARAMETER AccessToken +OAuth 2.0 access token for authenticating with Google Cloud. + +.PARAMETER LogName +The name of the log in Google Cloud Logging. Defaults to `default_log`. + +.PARAMETER FailureAction +Specifies the action to take if the logging request fails. Valid values are `Ignore`, `Report`, and `Halt`. The default is `Ignore`. + +.PARAMETER SkipCertificateCheck +If present, skips SSL certificate validation when sending logs. + +.PARAMETER AsUTC +If present, converts timestamps to UTC. + +.PARAMETER DefaultTag +Sets a default tag for log entries. Defaults to `-`. + +.PARAMETER DataFormat +The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 +If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.EXAMPLE +PS> New-PodeGoogleLoggingMethod -ProjectId 'my-project-id' -AccessToken 'ya29.a0AfH6SM...' -LogName 'ApplicationLogs' + +Sets up Google Cloud Logging with the specified project ID and access token. + +.NOTES +This function sends log entries to Google Cloud Logging using the Google Cloud Logging REST API, allowing for structured logging within a specific project. +#> +function New-PodeGoogleLoggingMethod { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $ProjectId, + + [Parameter(Mandatory = $true)] + [string] + $AccessToken, # OAuth 2.0 token + + [Parameter()] + [string] + $LogName = 'default_log', + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601 + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Generate a unique ID for this logging method instance. + $methodId = New-PodeGuid + + # Add the logging method configuration to the PodeContext for tracking and execution. + $PodeContext.Server.Logging.Method[$methodId] = @{ + # Queue to hold log entries until they can be processed. + # Using a concurrent queue ensures thread-safe interactions in the runspace. + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + + # ScriptBlock responsible for processing log entries in a separate runspace. + # This block continuously dequeues and sends log entries to Google Cloud Logging. + ScriptBlock = { + param($MethodId) # Pass the unique method ID to identify this logging configuration. + + # Temporary hashtable to hold a dequeued log entry. + $log = @{ } + + # Loop continuously until a cancellation is requested (graceful shutdown). + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + # Brief pause to reduce CPU usage in the loop. + Start-Sleep -Milliseconds 100 + + # Attempt to dequeue a log entry from the queue. + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + # Only process if a valid log entry was dequeued. + if ($null -ne $log) { + # Retrieve log data and configuration options. + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem + + # Ensure both $Item and $RawItem are treated as arrays to handle multiple log entries. + $Item = @($Item) + $RawItem = @($RawItem) + + # Define the Google Cloud Logging API endpoint URL. + $url = 'https://logging.googleapis.com/v2/entries:write' + + # Set up headers with the authorization token and JSON content type. + $headers = @{ + 'Authorization' = "Bearer $($Options.AccessToken)" # OAuth 2.0 Bearer token + 'Content-Type' = 'application/json' + } + + # Format each log entry for Google Cloud Logging. + $entries = $Item | ForEach-Object { + @{ + textPayload = ([pode.PodeLogger]::ProtectLogItem($_, $PodeContext.Server.Logging.Masking)) #($_ | Protect-PodeLogItem) # Sanitize log message content # Sanitize log message content + severity = $RawItem.Level.ToUpperInvariant() # Set log severity level + timestamp = $RawItem.Date.ToString('yyyy-MM-ddTHH:mm:ss.fffZ') # Format timestamp in ISO 8601 + labels = @{ + tag = $RawItem.Tag # Include tag if provided + } + resource = @{ + type = 'global' # Set resource type to global + labels = @{ + project_id = $Options.ProjectId # Add the project ID + } + } + } + } + + # Create the payload body for Google Cloud Logging. + $body = @{ + entries = $entries + logName = "projects/$($Options.ProjectId)/logs/$($Options.LogName)" # Define log name path + } | ConvertTo-Json -Compress + + # Send the log data to Google Cloud Logging via HTTP POST. + try { + Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body -SkipCertificateCheck:$Options.SkipCertificateCheck + } + catch { + # Handle any failures based on the configured FailureAction (e.g., Ignore, Report, Halt). + Invoke-PodeHandleFailure -Message "Failed to send log to Google Cloud Logging: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + } + + # Return the logging method configuration as a hashtable. + return @{ + Type = 'Google' # Specifies the type of logging platform. + Id = $methodId # Unique identifier for this logging method. + Batch = New-PodeLogBatchInfo # Contains batch information for Pode logging. + Logger = @() # Initialize an empty logger array if needed for Pode processing. + Arguments = @{ + ProjectId = $ProjectId + AccessToken = $AccessToken + LogName = $LogName + FailureAction = $FailureAction + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + DataFormat = $DataFormat + } + } +} + +<# +.SYNOPSIS +Configures logging to Datadog Logs. + +.DESCRIPTION +The `New-PodeDatadogLoggingMethod` function sets up logging for Datadog, allowing log entries to be sent to Datadog’s log intake endpoint using the provided API key. + +.PARAMETER ApiKey +The Datadog API key used to authenticate requests. + +.PARAMETER BaseUrl +The Datadog intake URL, typically `https://http-intake.logs.datadoghq.com/v1/input`. + +.PARAMETER FailureAction +Specifies the action to take if the logging request fails. Valid values are `Ignore`, `Report`, and `Halt`. The default is `Ignore`. + +.PARAMETER SkipCertificateCheck +If present, skips SSL certificate validation when sending logs. + +.PARAMETER AsUTC +If present, converts timestamps to UTC. + +.PARAMETER DefaultTag +Sets a default tag for log entries. Defaults to `-`. + +.PARAMETER DataFormat +The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 +If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.EXAMPLE +PS> New-PodeDatadogLoggingMethod -ApiKey 'my-datadog-api-key' -BaseUrl 'https://http-intake.logs.datadoghq.com/v1/input' + +Configures Datadog logging using the provided API key and URL. + +.NOTES +This function sends logs to Datadog Logs using a REST API call with the API key as authorization. +#> +function New-PodeDatadogLoggingMethod { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $ApiKey, + + [Parameter(Mandatory = $true)] + [string] + $BaseUrl, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601 + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Generate a unique ID for this logging method instance. + $methodId = New-PodeGuid + + # Add the logging method configuration to the PodeContext for tracking and execution. + $PodeContext.Server.Logging.Method[$methodId] = @{ + # Queue to hold log entries until they can be processed. + # Using a concurrent queue ensures thread-safe interactions in the runspace. + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + + # ScriptBlock responsible for processing log entries in a separate runspace. + # This block continuously dequeues and sends log entries to Datadog. + ScriptBlock = { + param($MethodId) # Pass the unique method ID to identify this logging configuration. + + # Temporary hashtable to hold a dequeued log entry. + $log = @{ } + + # Loop continuously until a cancellation is requested (graceful shutdown). + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + # Brief pause to reduce CPU usage in the loop. + Start-Sleep -Milliseconds 100 + + # Attempt to dequeue a log entry from the queue. + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + # Only process if a valid log entry was dequeued. + if ($null -ne $log) { + # Retrieve log data and configuration options. + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem + + # Ensure both $Item and $RawItem are treated as arrays to handle multiple log entries. + $Item = @($Item) + $RawItem = @($RawItem) + + # Construct the Datadog intake URL for log ingestion. + $url = $Options.BaseUrl + + # Set up headers with the Datadog API key and JSON content type. + $headers = @{ + 'DD-API-KEY' = $Options.ApiKey # API key for Datadog + 'Content-Type' = 'application/json' + } + + # Format each log entry for Datadog. + $events = $Item | ForEach-Object { + @{ + message = ([pode.PodeLogger]::ProtectLogItem($_, $PodeContext.Server.Logging.Masking)) #($_ | Protect-PodeLogItem) # Sanitize log message content # Sanitize log message content + host = $PodeContext.Server.ComputerName # Add hostname + service = $RawItem.Tag # Use tag as the service if provided + date_happened = [math]::Round(($RawItem.Date).ToUniversalTime().Subtract(([datetime]::UnixEpoch)).TotalSeconds) # Convert timestamp to seconds since epoch + status = $RawItem.Level.ToUpperInvariant() # Set log severity level + } + } + + # Convert the list of events to JSON format for Datadog ingestion. + $body = $events | ConvertTo-Json -Compress + + # Send the log data to Datadog via HTTP POST. + try { + Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body -SkipCertificateCheck:$Options.SkipCertificateCheck + } + catch { + # Handle any failures based on the configured FailureAction (e.g., Ignore, Report, Halt). + Invoke-PodeHandleFailure -Message "Failed to send log to Datadog: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + } + + # Return the logging method configuration as a hashtable. + return @{ + Type = 'Datadog' # Specifies the type of logging platform. + Id = $methodId # Unique identifier for this logging method. + Batch = New-PodeLogBatchInfo # Contains batch information for Pode logging. + Logger = @() # Initialize an empty logger array if needed for Pode processing. + Arguments = @{ + ApiKey = $ApiKey + BaseUrl = $BaseUrl + FailureAction = $FailureAction + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + DataFormat = $DataFormat + } + } +} + + +<# +.SYNOPSIS +Configures logging to Elasticsearch. + +.DESCRIPTION +The `New-PodeElasticsearchLoggingMethod` function configures logging for Elasticsearch, allowing log entries to be sent as documents to a specified Elasticsearch index. + +.PARAMETER BaseUrl +The base URL for the Elasticsearch API, typically `http://:9200`. + +.PARAMETER IndexName +The name of the Elasticsearch index where log entries will be stored. + +.PARAMETER FailureAction +Specifies the action to take if the logging request fails. Valid values are `Ignore`, `Report`, and `Halt`. The default is `Ignore`. + +.PARAMETER SkipCertificateCheck +If present, skips SSL certificate validation when sending logs. + +.PARAMETER AsUTC +If present, converts timestamps to UTC. + +.PARAMETER DefaultTag +Sets a default tag for log entries. Defaults to `-`. + +.PARAMETER DataFormat +The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 +If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.EXAMPLE +PS> New-PodeElasticsearchLoggingMethod -BaseUrl 'http://localhost:9200' -IndexName 'application-logs' + +Sets up Elasticsearch logging with the specified base URL and index name. + +.NOTES +This function sends log entries to Elasticsearch by creating documents in the specified index. +#> +function New-PodeElasticsearchLoggingMethod { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $BaseUrl, + + [Parameter(Mandatory = $true)] + [string] + $IndexName, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601 + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Generate a unique ID for this logging method instance. + $methodId = New-PodeGuid + + # Add the logging method configuration to the PodeContext for tracking and execution. + $PodeContext.Server.Logging.Method[$methodId] = @{ + # Queue to hold log entries until they can be processed. + # Using a concurrent queue ensures thread-safe interactions in the runspace. + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + + # ScriptBlock responsible for processing log entries in a separate runspace. + # This block continuously dequeues and sends log entries to Elasticsearch. + ScriptBlock = { + param($MethodId) # Pass the unique method ID to identify this logging configuration. + + # Temporary hashtable to hold a dequeued log entry. + $log = @{ } + + # Loop continuously until a cancellation is requested (graceful shutdown). + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + # Brief pause to reduce CPU usage in the loop. + Start-Sleep -Milliseconds 100 + + # Attempt to dequeue a log entry from the queue. + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + # Only process if a valid log entry was dequeued. + if ($null -ne $log) { + # Retrieve log data and configuration options. + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem + + # Ensure both $Item and $RawItem are treated as arrays to handle multiple log entries. + $Item = @($Item) + $RawItem = @($RawItem) + + # Construct the Elasticsearch URL for document ingestion using the specified index. + $url = "$($Options.BaseUrl)/$($Options.IndexName)/_doc/" + + # Set up headers for JSON content type required by Elasticsearch. + $headers = @{ + 'Content-Type' = 'application/json' + } + + # Format each log entry for Elasticsearch. + $documents = $Item | ForEach-Object { + @{ + message = ([pode.PodeLogger]::ProtectLogItem($_, $PodeContext.Server.Logging.Masking)) #($_ | Protect-PodeLogItem) # Sanitize log message content # Sanitize log message content + timestamp = $RawItem.Date.ToString('yyyy-MM-ddTHH:mm:ss.fffZ') # Format timestamp in ISO 8601 + severity = $RawItem.Level.ToUpperInvariant() # Set log severity level + host = $PodeContext.Server.ComputerName # Add hostname + tag = $RawItem.Tag # Include tag if provided + } + } + + # Convert the list of documents to JSON format for Elasticsearch ingestion. + $body = $documents | ConvertTo-Json -Compress + + # Send the log data to Elasticsearch via HTTP POST. + try { + Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body -SkipCertificateCheck:$Options.SkipCertificateCheck + } + catch { + # Handle any failures based on the configured FailureAction (e.g., Ignore, Report, Halt). + Invoke-PodeHandleFailure -Message "Failed to send log to Elasticsearch: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + } + + # Return the logging method configuration as a hashtable. + return @{ + Type = 'Elasticsearch' # Specifies the type of logging platform. + Id = $methodId # Unique identifier for this logging method. + Batch = New-PodeLogBatchInfo # Contains batch information for Pode logging. + Logger = @() # Initialize an empty logger array if needed for Pode processing. + Arguments = @{ + BaseUrl = $BaseUrl + IndexName = $IndexName + FailureAction = $FailureAction + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + DataFormat = $DataFormat + } + } +} + +<# +.SYNOPSIS +Configures logging to Graylog. + +.DESCRIPTION +The `New-PodeGraylogLoggingMethod` function sets up logging for Graylog, sending log entries to the Graylog server using GELF (Graylog Extended Log Format) over HTTP. + +.PARAMETER BaseUrl +The base URL for the Graylog API, typically `http://:12201/gelf`. + +.PARAMETER FailureAction +Specifies the action to take if the logging request fails. Valid values are `Ignore`, `Report`, and `Halt`. The default is `Ignore`. + +.PARAMETER SkipCertificateCheck +If present, skips SSL certificate validation when sending logs. + +.PARAMETER AsUTC +If present, converts timestamps to UTC. + +.PARAMETER DefaultTag +Sets a default tag for log entries. Defaults to `-`. + +.PARAMETER DataFormat +The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 +If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.EXAMPLE +PS> New-PodeGraylogLoggingMethod -BaseUrl 'http://graylog-server:12201/gelf' + +Configures Graylog logging using the specified base URL. + +.NOTES +This function sends logs to Graylog using GELF, which allows for structured logging. +#> +function New-PodeGraylogLoggingMethod { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $BaseUrl, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601 + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Generate a unique ID for this logging method instance. + $methodId = New-PodeGuid + + # Add the logging method configuration to the PodeContext for tracking and execution. + $PodeContext.Server.Logging.Method[$methodId] = @{ + # Queue to hold log entries until they can be processed. + # Using a concurrent queue ensures thread-safe interactions in the runspace. + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + + # ScriptBlock responsible for processing log entries in a separate runspace. + # This block continuously dequeues and sends log entries to Graylog. + ScriptBlock = { + param($MethodId) # Pass the unique method ID to identify this logging configuration. + + # Temporary hashtable to hold a dequeued log entry. + $log = @{ } + + # Loop continuously until a cancellation is requested (graceful shutdown). + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + # Brief pause to reduce CPU usage in the loop. + Start-Sleep -Milliseconds 100 + + # Attempt to dequeue a log entry from the queue. + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + # Only process if a valid log entry was dequeued. + if ($null -ne $log) { + # Retrieve log data and configuration options. + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem + + # Ensure both $Item and $RawItem are treated as arrays to handle multiple log entries. + $Item = @($Item) + $RawItem = @($RawItem) + + # Construct the Graylog HTTP GELF URL. + $url = $Options.BaseUrl + + # Set up headers for JSON content type required by Graylog. + $headers = @{ + 'Content-Type' = 'application/json' + } + + # Format each log entry for Graylog. + $messages = $Item | ForEach-Object { + @{ + version = '1.1' # GELF version + host = $PodeContext.Server.ComputerName # Add hostname + short_message = ([pode.PodeLogger]::ProtectLogItem($_, $PodeContext.Server.Logging.Masking)) #($_ | Protect-PodeLogItem) # Sanitize log message content # Sanitize log message content + timestamp = [math]::Round(($RawItem.Date).ToUniversalTime().Subtract(([datetime]::UnixEpoch)).TotalSeconds) # Convert timestamp to seconds since epoch + level = $RawItem.Level.ToUpperInvariant() # Set log severity level + _tag = $RawItem.Tag # Include tag if provided + } + } + + # Convert the list of messages to JSON format for Graylog ingestion. + $body = $messages | ConvertTo-Json -Compress + + # Send the log data to Graylog via HTTP POST. + try { + Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body -SkipCertificateCheck:$Options.SkipCertificateCheck + } + catch { + # Handle any failures based on the configured FailureAction (e.g., Ignore, Report, Halt). + Invoke-PodeHandleFailure -Message "Failed to send log to Graylog: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + } + + # Return the logging method configuration as a hashtable. + return @{ + Type = 'Graylog' # Specifies the type of logging platform. + Id = $methodId # Unique identifier for this logging method. + Batch = New-PodeLogBatchInfo # Contains batch information for Pode logging. + Logger = @() # Initialize an empty logger array if needed for Pode processing. + Arguments = @{ + BaseUrl = $BaseUrl + FailureAction = $FailureAction + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + DataFormat = $DataFormat + } + } +} + +<# +.SYNOPSIS +Configures logging to Splunk. + +.DESCRIPTION +The `New-PodeSplunkLoggingMethod` function sets up logging for Splunk, sending log entries to a specified Splunk HTTP Event Collector (HEC) endpoint using a specified token. + +.PARAMETER BaseUrl +The base URL for the Splunk HTTP Event Collector, typically `https://:8088/services/collector`. + +.PARAMETER Token +The Splunk HEC token for authentication. + +.PARAMETER FailureAction +Specifies the action to take if the logging request fails. Valid values are `Ignore`, `Report`, and `Halt`. The default is `Ignore`. + +.PARAMETER SkipCertificateCheck +If present, skips SSL certificate validation when sending logs. + +.PARAMETER AsUTC +If present, converts timestamps to UTC. + +.PARAMETER DefaultTag +Sets a default tag for log entries. Defaults to `-`. + +.PARAMETER DataFormat +The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 +If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.EXAMPLE +PS> New-PodeSplunkLoggingMethod -BaseUrl 'https://splunk-server:8088/services/collector' -Token 'my-splunk-token' + +Configures Splunk logging with the provided URL and token. + +.NOTES +This function sends logs to Splunk through its HTTP Event Collector (HEC). +#> +function New-PodeSplunkLoggingMethod { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $BaseUrl, + + [Parameter(Mandatory = $true)] + [string] + $Token, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601 + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Generate a unique ID for this logging method instance. + $methodId = New-PodeGuid + + # Add the logging method configuration to the PodeContext for tracking and execution. + $PodeContext.Server.Logging.Method[$methodId] = @{ + # Queue to hold log entries until they can be processed. + # Using a concurrent queue ensures thread-safe interactions in the runspace. + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + + # ScriptBlock responsible for processing log entries in a separate runspace. + # This block continuously dequeues and sends log entries to Splunk. + ScriptBlock = { + param($MethodId) # Pass the unique method ID to identify this logging configuration. + + # Temporary hashtable to hold a dequeued log entry. + $log = @{ } + + # Loop continuously until a cancellation is requested (graceful shutdown). + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + # Brief pause to reduce CPU usage in the loop. + Start-Sleep -Milliseconds 100 + + # Attempt to dequeue a log entry from the queue. + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + # Only process if a valid log entry was dequeued. + if ($null -ne $log) { + # Retrieve log data and configuration options. + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem + + # Ensure both $Item and $RawItem are treated as arrays to handle multiple log entries. + $Item = @($Item) + $RawItem = @($RawItem) + + # Construct the Splunk HEC URL. + $url = $Options.BaseUrl + + # Set up headers for Splunk HEC authentication and content type. + $headers = @{ + 'Authorization' = "Splunk $($Options.Token)" # HEC token for Splunk + 'Content-Type' = 'application/json' + } + + # Format each log entry for Splunk. + $events = $Item | ForEach-Object { + @{ + event = ([pode.PodeLogger]::ProtectLogItem($_, $PodeContext.Server.Logging.Masking)) #($_ | Protect-PodeLogItem) # Sanitize log message content # Sanitize log message content + host = $PodeContext.Server.ComputerName # Add hostname + source = $RawItem.Tag # Use tag as the source if provided + time = [math]::Round(($RawItem.Date).ToUniversalTime().Subtract(([datetime]::UnixEpoch)).TotalSeconds) # Convert timestamp to seconds since epoch + fields = @{ + severity = $RawItem.Level.ToUpperInvariant() # Set log severity level + } + } + } + + # Convert the list of events to JSON format for Splunk ingestion. + $body = $events | ConvertTo-Json -Compress + + # Send the log data to Splunk via HTTP POST. + try { + Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body -SkipCertificateCheck:$Options.SkipCertificateCheck + } + catch { + # Handle any failures based on the configured FailureAction (e.g., Ignore, Report, Halt). + Invoke-PodeHandleFailure -Message "Failed to send log to Splunk: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + } + + # Return the logging method configuration as a hashtable. + return @{ + Type = 'Splunk' # Specifies the type of logging platform. + Id = $methodId # Unique identifier for this logging method. + Batch = New-PodeLogBatchInfo # Contains batch information for Pode logging. + Logger = @() # Initialize an empty logger array if needed for Pode processing. + Arguments = @{ + BaseUrl = $BaseUrl + Token = $Token + FailureAction = $FailureAction + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + DataFormat = $DataFormat + } + } +} + +<# +.SYNOPSIS +Configures logging to VMware Log Insight. + +.DESCRIPTION +The `New-PodeLogInsightLoggingMethod` function sets up logging for VMware Log Insight, allowing log entries to be sent to the Log Insight API endpoint. + +.PARAMETER BaseUrl +The base URL for the VMware Log Insight ingestion API, typically `https:///api/v1/messages/ingest/`. + +.PARAMETER Id +The ingestion ID for VMware Log Insight, used to target a specific log stream. + +.PARAMETER FailureAction +Specifies the action to take if the logging request fails. Valid values are `Ignore`, `Report`, and `Halt`. The default is `Ignore`. + +.PARAMETER SkipCertificateCheck +If present, skips SSL certificate validation when sending logs. + +.PARAMETER AsUTC +If present, converts timestamps to UTC. + +.PARAMETER DefaultTag +Sets a default tag for log entries. Defaults to `-`. + +.PARAMETER DataFormat +The custom date format for log entries. Mutually exclusive with ISO8601. + +.PARAMETER ISO8601 +If set, uses the ISO 8601 date format for log entries. Mutually exclusive with DataFormat. + +.EXAMPLE +PS> New-PodeLogInsightLoggingMethod -BaseUrl 'https://loginsight-server/api/v1/messages/ingest/' -Id 'my-log-id' + +Configures Log Insight logging using the provided URL and ID. + +.NOTES +This function sends logs to VMware Log Insight through its ingestion API. +#> +function New-PodeLogInsightLoggingMethod { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $BaseUrl, + + [Parameter(Mandatory = $true)] + [string] + $Id, + + [Parameter()] + [ValidateSet('Ignore', 'Report', 'Halt')] + [string] + $FailureAction = 'Ignore', + + [Parameter()] + [switch] + $SkipCertificateCheck, + + [Parameter()] + [switch] + $AsUTC, + + [Parameter()] + [string] + $DefaultTag = '-', + + [Parameter(ParameterSetName = 'DataFormat')] + [ValidateScript({ Test-PodeDateFormat $_ })] + [string] + $DataFormat, + + [Parameter(ParameterSetName = 'ISO8601')] + [switch] + $ISO8601 + ) + + # Determine the date format based on parameter set + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'iso8601' { + $DataFormat = 'yyyy-MM-ddTHH:mm:ssK' # ISO8601 format + } + default { + if ([string]::IsNullOrEmpty($DataFormat)) { + $DataFormat = 'dd/MMM/yyyy:HH:mm:ss zzz' # Default format + } + } + } + + # Generate a unique method ID for this logging method instance + $methodId = New-PodeGuid + + # Add the logging method configuration to the PodeContext for use in logging + $PodeContext.Server.Logging.Method[$methodId] = @{ + # Queue to hold log entries until they can be processed. + # Using a concurrent queue ensures thread-safe interactions in the runspace. + Queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + + # ScriptBlock responsible for processing log entries in a separate runspace. + # This block continuously dequeues and sends log entries to Splunk. + ScriptBlock = { + param($MethodId) + + # Temporary hashtable to store dequeued log information + $log = @{ } + + # Run while cancellation has not been requested + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + Start-Sleep -Milliseconds 100 # Sleep briefly to avoid constant polling + + # Try to dequeue a log entry from the method's queue + if ($PodeContext.Server.Logging.Method[$MethodId].Queue.TryDequeue([ref]$log)) { + if ($null -ne $log) { + # Extract log data and configuration options + $Item = $log.Item + $Options = $log.Options + $RawItem = $log.RawItem + + # Ensure both $Item and $RawItem are arrays to handle multiple log entries + $Item = @($Item) + $RawItem = @($RawItem) + + # Build the target URL for the Log Insight API endpoint + $url = "$($Options.BaseUrl)/$($Options.Id)" + $headers = @{ + 'Content-Type' = 'application/json' + } + + # Process each log entry and format as required by Log Insight + $messages = $Item | ForEach-Object { + @{ + text = ([pode.PodeLogger]::ProtectLogItem($_, $PodeContext.Server.Logging.Masking)) #($_ | Protect-PodeLogItem) # Sanitize log message content # Sanitize the log message + timestamp = [math]::Round(($RawItem.Date).ToUniversalTime().Subtract(([datetime]::UnixEpoch)).TotalMilliseconds) # Convert date to milliseconds since epoch + fields = @{ + severity = $RawItem.Level.ToUpperInvariant() # Add severity level + tag = $RawItem.Tag # Add a tag if provided + } + } + } + + # Prepare the payload with the formatted messages + $payload = @{ + messages = $messages + } + + # Convert the payload to JSON format + $body = $payload | ConvertTo-Json -Compress + + try { + # Send the log data to VMware Log Insight via HTTP POST + Invoke-RestMethod -Uri $url -Method Post -Body $body -Headers $headers -SkipCertificateCheck:$Options.SkipCertificateCheck + } + catch { + # Handle any failures based on the configured FailureAction + Invoke-PodeHandleFailure -Message "Failed to send log to Log Insight: $_" -FailureAction $Options.FailureAction + } + } + } + } + } + } + + # Return the logging method configuration to the caller + return @{ + Type = 'LogInsight' + Id = $methodId + Batch = New-PodeLogBatchInfo # Contains batch information if needed + Logger = @() + Arguments = @{ + BaseUrl = $BaseUrl + Id = $Id + FailureAction = $FailureAction + SkipCertificateCheck = $SkipCertificateCheck.IsPresent + AsUTC = $AsUTC.IsPresent + DefaultTag = $DefaultTag + DataFormat = $DataFormat + } + } +} diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index e193bbbc3..611db2e54 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -234,84 +234,6 @@ function Get-PodeConfig { return $PodeContext.Server.Configuration } -<# -.SYNOPSIS -Adds a ScriptBlock as Endware to run at the end of each web Request. - -.DESCRIPTION -Adds a ScriptBlock as Endware to run at the end of each web Request. - -.PARAMETER ScriptBlock -The ScriptBlock to add. It will be supplied the current web event. - -.PARAMETER ArgumentList -An array of arguments to supply to the Endware's ScriptBlock. - -.EXAMPLE -Add-PodeEndware -ScriptBlock { /* logic */ } -#> -function Add-PodeEndware { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] - [scriptblock] - $ScriptBlock, - - [Parameter()] - [object[]] - $ArgumentList - ) - begin { - $pipelineItemCount = 0 - } - - process { - $pipelineItemCount++ - } - - end { - if ($pipelineItemCount -gt 1) { - throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } - # check for scoped vars - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - - # add the scriptblock to array of endware that needs to be run - $PodeContext.Server.Endware += @{ - Logic = $ScriptBlock - UsingVariables = $usingVars - Arguments = $ArgumentList - } - } -} - -<# -.SYNOPSIS -Automatically loads endware ps1 files - -.DESCRIPTION -Automatically loads endware ps1 files from either a /endware folder, or a custom folder. Saves space dot-sourcing them all one-by-one. - -.PARAMETER Path -Optional Path to a folder containing ps1 files, can be relative or literal. - -.EXAMPLE -Use-PodeEndware - -.EXAMPLE -Use-PodeEndware -Path './endware' -#> -function Use-PodeEndware { - [CmdletBinding()] - param( - [Parameter()] - [string] - $Path - ) - - Use-PodeFolder -Path $Path -DefaultPath 'endware' -} - <# .SYNOPSIS Imports a Module into the current, and all runspaces that Pode uses. diff --git a/tests/integration/OpenApi.Tests.ps1 b/tests/integration/OpenApi.Tests.ps1 index cc398cdd4..449390156 100644 --- a/tests/integration/OpenApi.Tests.ps1 +++ b/tests/integration/OpenApi.Tests.ps1 @@ -5,6 +5,10 @@ param() Describe 'OpenAPI integration tests' { BeforeAll { + + $helperPath = (Split-Path -Parent -Path $PSCommandPath) -ireplace 'integration', 'shared' + . "$helperPath/TestHelper.ps1" + $mindyCommonHeaders = @{ 'accept' = 'application/json' 'X-API-KEY' = 'test2-api-key' @@ -19,137 +23,9 @@ Describe 'OpenAPI integration tests' { $PortV3 = 8080 $PortV3_1 = 8081 $scriptPath = "$($PSScriptRoot)\..\..\examples\OpenApi-TuttiFrutti.ps1" - Start-Process (Get-Process -Id $PID).Path -ArgumentList "-NoProfile -File `"$scriptPath`" -PortV3 $PortV3 -PortV3_1 $PortV3_1 -Daemon -IgnoreServerConfig" -NoNewWindow - - function Compare-StringRnLn { - param ( - [string]$InputString1, - [string]$InputString2 - ) - return ($InputString1.Trim() -replace "`r`n|`n|`r", "`n") -eq ($InputString2.Trim() -replace "`r`n|`n|`r", "`n") - } - - function Convert-PsCustomObjectToOrderedHashtable { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [PSCustomObject]$InputObject - ) - begin { - # Define a recursive function within the process block - function Convert-ObjectRecursively { - param ( - [Parameter(Mandatory = $true)] - [System.Object] - $InputObject - ) - - # Initialize an ordered dictionary - $orderedHashtable = [ordered]@{} - - # Loop through each property of the PSCustomObject - foreach ($property in $InputObject.PSObject.Properties) { - # Check if the property value is a PSCustomObject - if ($property.Value -is [PSCustomObject]) { - # Recursively convert the nested PSCustomObject - $orderedHashtable[$property.Name] = Convert-ObjectRecursively -InputObject $property.Value - } - elseif ($property.Value -is [System.Collections.IEnumerable] -and -not ($property.Value -is [string])) { - # If the value is a collection, check each element - $convertedCollection = @() - foreach ($item in $property.Value) { - if ($item -is [PSCustomObject]) { - $convertedCollection += Convert-ObjectRecursively -InputObject $item - } - else { - $convertedCollection += $item - } - } - $orderedHashtable[$property.Name] = $convertedCollection - } - else { - # Add the property name and value to the ordered hashtable - $orderedHashtable[$property.Name] = $property.Value - } - } - - # Return the resulting ordered hashtable - return $orderedHashtable - } - } - process { - # Call the recursive helper function for each input object - Convert-ObjectRecursively -InputObject $InputObject - } - } - - function Compare-Hashtable { - param ( - [object]$Hashtable1, - [object]$Hashtable2 - ) - - # Function to compare two hashtable values - function Compare-Value($value1, $value2) { - # Check if both values are hashtables - if ((($value1 -is [hashtable] -or $value1 -is [System.Collections.Specialized.OrderedDictionary]) -and - ($value2 -is [hashtable] -or $value2 -is [System.Collections.Specialized.OrderedDictionary]))) { - return Compare-Hashtable -Hashtable1 $value1 -Hashtable2 $value2 - } - # Check if both values are arrays - elseif (($value1 -is [Object[]]) -and ($value2 -is [Object[]])) { - if ($value1.Count -ne $value2.Count) { - return $false - } - for ($i = 0; $i -lt $value1.Count; $i++) { - $found = $false - for ($j = 0; $j -lt $value2.Count; $j++) { - if ( Compare-Value $value1[$i] $value2[$j]) { - $found = $true - } - } - if ($found -eq $false) { - return $false - } - } - return $true - } - else { - if ($value1 -is [string] -and $value2 -is [string]) { - return Compare-StringRnLn $value1 $value2 - } - # Check if the values are equal - return $value1 -eq $value2 - } - } + Start-Process (Get-Process -Id $PID).Path -ArgumentList "-NoProfile -File `"$scriptPath`" -PortV3 $PortV3 -PortV3_1 $PortV3_1 -Daemon -IgnoreServerConfig" -NoNewWindow - $keys1 = $Hashtable1.Keys - $keys2 = $Hashtable2.Keys - - # Check if both hashtables have the same keys - if ($keys1.Count -ne $keys2.Count) { - return $false - } - - foreach ($key in $keys1) { - if (! ($Hashtable2.Keys -contains $key)) { - return $false - } - - if ($Hashtable2[$key] -is [hashtable] -or $Hashtable2[$key] -is [System.Collections.Specialized.OrderedDictionary]) { - if (! (Compare-Hashtable -Hashtable1 $Hashtable1[$key] -Hashtable2 $Hashtable2[$key])) { - return $false - } - } - elseif (!(Compare-Value $Hashtable1[$key] $Hashtable2[$key])) { - return $false - } - } - - return $true - } - - Start-Sleep -Seconds 5 + Wait-ForWebServer -Port $PortV3 } AfterAll { @@ -161,7 +37,6 @@ Describe 'OpenAPI integration tests' { Describe 'OpenAPI' { it 'Open API v3.0.3' { - Start-Sleep -Seconds 10 $fileContent = Get-Content -Path "$PSScriptRoot/specs/OpenApi-TuttiFrutti_3.0.3.json" $webResponse = Invoke-WebRequest -Uri "http://localhost:$($PortV3)/docs/openapi/v3.0" -Method Get diff --git a/tests/integration/Sessions.Tests.ps1 b/tests/integration/Sessions.Tests.ps1 index b4d42f191..2e5aa69b9 100644 --- a/tests/integration/Sessions.Tests.ps1 +++ b/tests/integration/Sessions.Tests.ps1 @@ -5,13 +5,16 @@ param() Describe 'Session Requests' { BeforeAll { + $helperPath = (Split-Path -Parent -Path $PSCommandPath) -ireplace 'integration', 'shared' + . "$helperPath/TestHelper.ps1" + $Port = 8080 $Endpoint = "http://127.0.0.1:$($Port)" Start-Job -Name 'Pode' -ErrorAction Stop -ScriptBlock { Import-Module -Name "$($using:PSScriptRoot)\..\..\src\Pode.psm1" - Start-PodeServer -Quiet -ScriptBlock { + Start-PodeServer -Daemon -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Http Add-PodeRoute -Method Get -Path '/close' -ScriptBlock { Close-PodeServer @@ -41,7 +44,7 @@ Describe 'Session Requests' { } } - Start-Sleep -Seconds 10 + Wait-ForWebServer -Port $Port } AfterAll { diff --git a/tests/shared/TestHelper.ps1 b/tests/shared/TestHelper.ps1 index 38ccacbe2..f8168b34d 100644 --- a/tests/shared/TestHelper.ps1 +++ b/tests/shared/TestHelper.ps1 @@ -1,3 +1,5 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] +param() <# .SYNOPSIS Ensures the Pode assembly is loaded into the current session. @@ -47,3 +49,311 @@ function Import-PodeAssembly { Add-Type -LiteralPath (Join-Path -Path $netFolder -ChildPath 'Pode.dll') -ErrorAction Stop } } + + +<# +.SYNOPSIS + Compares two strings while normalizing line endings. + +.DESCRIPTION + This function trims both input strings and replaces all variations of line endings (`CRLF`, `LF`, `CR`) with a normalized `LF` (`\n`). + It then compares the normalized strings for equality. + +.PARAMETER InputString1 + The first string to compare. + +.PARAMETER InputString2 + The second string to compare. + +.OUTPUTS + [bool] + Returns `$true` if both strings are equal after normalization; otherwise, returns `$false`. + +.EXAMPLE + Compare-StringRnLn -InputString1 "Hello`r`nWorld" -InputString2 "Hello`nWorld" + # Returns: $true + +.EXAMPLE + Compare-StringRnLn -InputString1 "Line1`r`nLine2" -InputString2 "Line1`rLine2" + # Returns: $true + +.NOTES + This function ensures that strings with different line-ending formats are treated as equal if their content is otherwise identical. +#> +function Compare-StringRnLn { + param ( + [string]$InputString1, + [string]$InputString2 + ) + return ($InputString1.Trim() -replace "`r`n|`n|`r", "`n") -eq ($InputString2.Trim() -replace "`r`n|`n|`r", "`n") +} + +<# +.SYNOPSIS + Converts a PSCustomObject into an ordered hashtable. + +.DESCRIPTION + This function recursively converts a PSCustomObject, including nested objects and collections, into an ordered hashtable. + It ensures that all properties are retained while maintaining their original structure. + +.PARAMETER InputObject + The PSCustomObject to be converted into an ordered hashtable. + +.OUTPUTS + [System.Collections.Specialized.OrderedDictionary] + Returns an ordered hashtable representation of the input PSCustomObject. + +.EXAMPLE + $object = [PSCustomObject]@{ Name = "Pode"; Version = "2.0"; Config = [PSCustomObject]@{ Debug = $true } } + Convert-PsCustomObjectToOrderedHashtable -InputObject $object + # Returns: An ordered hashtable representation of $object. + +.EXAMPLE + $object = [PSCustomObject]@{ Users = @([PSCustomObject]@{ Name = "Alice" }, [PSCustomObject]@{ Name = "Bob" }) } + Convert-PsCustomObjectToOrderedHashtable -InputObject $object + # Returns: An ordered hashtable where 'Users' is an array of ordered hashtables. + +.NOTES + This function preserves key order and supports recursive conversion of nested objects and collections. +#> +function Convert-PsCustomObjectToOrderedHashtable { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [PSCustomObject]$InputObject + ) + begin { + # Define a recursive function within the process block + function Convert-ObjectRecursively { + param ( + [Parameter(Mandatory = $true)] + [System.Object] + $InputObject + ) + + # Initialize an ordered dictionary + $orderedHashtable = [ordered]@{} + + # Loop through each property of the PSCustomObject + foreach ($property in $InputObject.PSObject.Properties) { + # Check if the property value is a PSCustomObject + if ($property.Value -is [PSCustomObject]) { + # Recursively convert the nested PSCustomObject + $orderedHashtable[$property.Name] = Convert-ObjectRecursively -InputObject $property.Value + } + elseif ($property.Value -is [System.Collections.IEnumerable] -and -not ($property.Value -is [string])) { + # If the value is a collection, check each element + $convertedCollection = @() + foreach ($item in $property.Value) { + if ($item -is [PSCustomObject]) { + $convertedCollection += Convert-ObjectRecursively -InputObject $item + } + else { + $convertedCollection += $item + } + } + $orderedHashtable[$property.Name] = $convertedCollection + } + else { + # Add the property name and value to the ordered hashtable + $orderedHashtable[$property.Name] = $property.Value + } + } + + # Return the resulting ordered hashtable + return $orderedHashtable + } + } + process { + # Call the recursive helper function for each input object + Convert-ObjectRecursively -InputObject $InputObject + } +} + +<# +.SYNOPSIS + Compares two hashtables to determine if they are equal. + +.DESCRIPTION + This function recursively compares two hashtables, checking whether they contain the same keys and values. + It also handles nested hashtables and arrays, ensuring deep comparison of all elements. + +.PARAMETER Hashtable1 + The first hashtable to compare. + +.PARAMETER Hashtable2 + The second hashtable to compare. + +.OUTPUTS + [bool] + Returns `$true` if both hashtables are equal, otherwise returns `$false`. + +.EXAMPLE + $hash1 = @{ Name = "Pode"; Version = "2.0"; Config = @{ Debug = $true } } + $hash2 = @{ Name = "Pode"; Version = "2.0"; Config = @{ Debug = $true } } + Compare-Hashtable -Hashtable1 $hash1 -Hashtable2 $hash2 + # Returns: $true + +.EXAMPLE + $hash1 = @{ Name = "Pode"; Version = "2.0" } + $hash2 = @{ Name = "Pode"; Version = "2.1" } + Compare-Hashtable -Hashtable1 $hash1 -Hashtable2 $hash2 + # Returns: $false + +#> +function Compare-Hashtable { + param ( + [object]$Hashtable1, + [object]$Hashtable2 + ) + + # Function to compare two hashtable values + function Compare-Value($value1, $value2) { + # Check if both values are hashtables + if ((($value1 -is [hashtable] -or $value1 -is [System.Collections.Specialized.OrderedDictionary]) -and + ($value2 -is [hashtable] -or $value2 -is [System.Collections.Specialized.OrderedDictionary]))) { + return Compare-Hashtable -Hashtable1 $value1 -Hashtable2 $value2 + } + # Check if both values are arrays + elseif (($value1 -is [Object[]]) -and ($value2 -is [Object[]])) { + if ($value1.Count -ne $value2.Count) { + return $false + } + for ($i = 0; $i -lt $value1.Count; $i++) { + $found = $false + for ($j = 0; $j -lt $value2.Count; $j++) { + if ( Compare-Value $value1[$i] $value2[$j]) { + $found = $true + } + } + if ($found -eq $false) { + return $false + } + } + return $true + } + else { + if ($value1 -is [string] -and $value2 -is [string]) { + return Compare-StringRnLn $value1 $value2 + } + # Check if the values are equal + return $value1 -eq $value2 + } + } + + $keys1 = $Hashtable1.Keys + $keys2 = $Hashtable2.Keys + + # Check if both hashtables have the same keys + if ($keys1.Count -ne $keys2.Count) { + return $false + } + + foreach ($key in $keys1) { + if (! ($Hashtable2.Keys -contains $key)) { + return $false + } + + if ($Hashtable2[$key] -is [hashtable] -or $Hashtable2[$key] -is [System.Collections.Specialized.OrderedDictionary]) { + if (! (Compare-Hashtable -Hashtable1 $Hashtable1[$key] -Hashtable2 $Hashtable2[$key])) { + return $false + } + } + elseif (!(Compare-Value $Hashtable1[$key] $Hashtable2[$key])) { + return $false + } + } + + return $true +} + + +<# +.SYNOPSIS + Waits for a web server to become available at a specified URI or port. + +.DESCRIPTION + This function continuously checks if a web server is online by sending an HTTP request. + It retries until the server responds with a 200 status code or a timeout is reached. + +.PARAMETER Uri + The full URI to check (e.g., "http://127.0.0.1:5000"). If not provided, defaults to "http://localhost:$Port". + +.PARAMETER Port + The port on which the web server is expected to be available. If no URI is provided, the function constructs a default URI using "http://localhost:$Port". + +.PARAMETER Timeout + The maximum number of seconds to wait before timing out. Default is 60 seconds. + +.PARAMETER Interval + The number of seconds to wait between retries. Default is 2 seconds. + +.OUTPUTS + Boolean - Returns $true if the server is online, otherwise $false. + +.EXAMPLE + Wait-ForWebServer -Port 8080 -Timeout 30 -Interval 2 + + Waits up to 30 seconds for the web server on port 8080 to come online. + +.EXAMPLE + Wait-ForWebServer -Uri "http://127.0.0.1:5000" -Timeout 45 + + Waits up to 45 seconds for the web server at "http://127.0.0.1:5000" to respond. + +.NOTES + Author: ChatGPT + This function ensures that the web server is fully responding, not just that the port is open. +#> +function Wait-ForWebServer { + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Position = 0)] + [string]$Uri, + + [Parameter(Position = 1)] + [int]$Port, + + [Parameter()] + [int]$Timeout = 60, + + [Parameter()] + [int]$Interval = 2 + ) + + # Determine the final URI: If no URI is provided, use "http://localhost:$Port" + if (-not $Uri) { + if ($Port -gt 0) { + $Uri = "http://localhost:$Port" + } + else { + return $false + } + } + + $MaxRetries = [math]::Ceiling($Timeout / $Interval) + $RetryCount = 0 + + while ($RetryCount -lt $MaxRetries) { + try { + # Send a request but ignore status codes (any response means the server is online) + $null = Invoke-WebRequest -Uri $Uri -UseBasicParsing -TimeoutSec 3 + Write-Host "Webserver is online at $Uri" + return $true + } + catch { + if ($_.Exception.Response -and $_.Exception.Response.StatusCode -eq 404) { + return $true + } + else { + Write-Host "Waiting for webserver to come online at $Uri... (Attempt $($RetryCount+1)/$MaxRetries)" + } + } + + Start-Sleep -Seconds $Interval + $RetryCount++ + } + + return $false +} diff --git a/tests/unit/Authentication.Tests.ps1 b/tests/unit/Authentication.Tests.ps1 index 38190f523..985f6b9fb 100644 --- a/tests/unit/Authentication.Tests.ps1 +++ b/tests/unit/Authentication.Tests.ps1 @@ -190,4 +190,4 @@ Describe 'Expand-PodeAuthMerge Tests' { { Expand-PodeAuthMerge -Names @('NonExistentAuth') } | Should -Throw } -} +} \ No newline at end of file diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index b9094f5e7..3b1bdb878 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -1079,6 +1079,7 @@ Describe 'Close-PodeServerInternal' { Mock Remove-PodePSDrive {} Mock Write-PodeHost {} Mock Close-PodeCancellationTokenRequest {} + Mock Disable-PodeLog { } } diff --git a/tests/unit/Logging.Tests.ps1 b/tests/unit/Logging.Tests.ps1 index 1353ea076..3dfa8734f 100644 --- a/tests/unit/Logging.Tests.ps1 +++ b/tests/unit/Logging.Tests.ps1 @@ -5,59 +5,111 @@ BeforeAll { $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' + if (!([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' })) { + + # fetch the .net version and the libs path + $version = [System.Environment]::Version.Major + $libsPath = Join-Path -Path $src -ChildPath 'Libs' + + # filter .net dll folders based on version above, and get path for latest version found + if (![string]::IsNullOrWhiteSpace($version)) { + $netFolder = Get-ChildItem -Path $libsPath -Directory -Force | + Where-Object { $_.Name -imatch "net[1-$($version)]" } | + Sort-Object -Property Name -Descending | + Select-Object -First 1 -ExpandProperty FullName + } + + # use netstandard if no folder found + if ([string]::IsNullOrWhiteSpace($netFolder)) { + $netFolder = "$($libsPath)/netstandard2.0" + } + + # append Pode.dll and mount + Add-Type -LiteralPath "$($netFolder)/Pode.dll" -ErrorAction Stop + } + [Pode.PodeLogger]::Enabled = $true + } Describe 'Get-PodeLogger' { It 'Returns null as the logger does not exist' { - $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Types' = @{}; } }; } - Get-PodeLogger -Name 'test' | Should -Be $null + $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Type' = @{}; } }; } + { Get-PodeLogger -Name 'test' } | Should -Throw $PodeLocale.loggerDoesNotExistExceptionMessage } It 'Returns terminal logger for name' { - $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Types' = @{ 'test' = $null }; } }; } + $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Type' = @{ 'test' = $null }; } }; } $result = (Get-PodeLogger -Name 'test') $result | Should -Be $null } It 'Returns custom logger for name' { - $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Types' = @{ 'test' = { Write-Host 'hello' } }; } }; } + $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Type' = @{ 'test' = { Write-PodeHost 'hello' } }; } }; } $result = (Get-PodeLogger -Name 'test') $result | Should -Not -Be $null - $result.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $result.ToString() | Should -Be ({ Write-PodeHost 'hello' }).ToString() } } Describe 'Write-PodeLog' { + BeforeEach { + $PodeContext = @{ + Server = @{ + Logging = @{ + LogsToProcess = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + Type = @{ + test = @{ + Standard = $false + } + } + } + } + } + } It 'Does nothing when logging disabled' { Mock Test-PodeLoggerEnabled { return $false } - $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } Write-PodeLog -Name 'test' -InputObject 'test' - $PodeContext.LogsToProcess.Count | Should -Be 0 + [Pode.PodeLogger]::Count | Should -Be 0 } It 'Adds a log item' { Mock Test-PodeLoggerEnabled { return $true } - $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } - + Mock Get-PodeLoggerLevel { return @('Informational') } Write-PodeLog -Name 'test' -InputObject 'test' - $PodeContext.LogsToProcess.Count | Should -Be 1 - $PodeContext.LogsToProcess[0].Name | Should -Be 'test' - $PodeContext.LogsToProcess[0].Item | Should -Be 'test' + [Pode.PodeLogger]::Count | Should -Be 1 + $item = [Pode.PodeLogger]::Dequeue() + $item.Name | Should -Be 'test' + $item.Item | Should -Be 'test' } } Describe 'Write-PodeErrorLog' { + BeforeEach { + $PodeContext = @{ + Server = @{ + Logging = @{ + Type = @{ + ([Pode.PodeLogger]::ErrorLogName) = @{ + Standard = $false + } + test = @{ + Standard = $false + } + } + } + } + } + } It 'Does nothing when logging disabled' { Mock Test-PodeLoggerEnabled { return $false } - $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } Write-PodeLog -Name 'test' -InputObject 'test' - $PodeContext.LogsToProcess.Count | Should -Be 0 + [Pode.PodeLogger]::Count | Should -Be 0 } It 'Adds an error log item' { @@ -65,17 +117,18 @@ Describe 'Write-PodeErrorLog' { Mock Get-PodeLogger { return @{ Arguments = @{ Levels = @('Error') } - } } + } + } - $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } try { throw 'some error' } catch { Write-PodeErrorLog -ErrorRecord $Error[0] } - $PodeContext.LogsToProcess.Count | Should -Be 1 - $PodeContext.LogsToProcess[0].Item.Message | Should -Be 'some error' + [Pode.PodeLogger]::Count | Should -Be 1 + $item = [Pode.PodeLogger]::Dequeue() + $item.Item.Message | Should -Be 'some error' } It 'Adds an exception log item' { @@ -85,13 +138,12 @@ Describe 'Write-PodeErrorLog' { } } } - $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } - $exp = [exception]::new('some error') Write-PodeErrorLog -Exception $exp - $PodeContext.LogsToProcess.Count | Should -Be 1 - $PodeContext.LogsToProcess[0].Item.Message | Should -Be 'some error' + [Pode.PodeLogger]::Count | Should -Be 1 + $item = [Pode.PodeLogger]::Dequeue() + $item.Item.Message | Should -Be 'some error' } It 'Does not log as Verbose not allowed' { @@ -101,24 +153,22 @@ Describe 'Write-PodeErrorLog' { } } } - $PodeContext = @{ LogsToProcess = [System.Collections.ArrayList]::new() } - $exp = [exception]::new('some error') Write-PodeErrorLog -Exception $exp -Level Verbose - - $PodeContext.LogsToProcess.Count | Should -Be 0 + $item = [Pode.PodeLogger]::Dequeue() + [Pode.PodeLogger]::Count | Should -Be 0 } } -Describe 'Get-PodeRequestLoggingName' { +Describe '[Pode.PodeLogger]::RequestLogName' { It 'Returns logger name' { - Get-PodeRequestLoggingName | Should -Be '__pode_log_requests__' + [Pode.PodeLogger]::RequestLogName | Should -Be '__pode_log_requests__' } } -Describe 'Get-PodeErrorLoggingName' { +Describe '[Pode.PodeLogger]::ErrorLogName' { It 'Returns logger name' { - Get-PodeErrorLoggingName | Should -Be '__pode_log_errors__' + [Pode.PodeLogger]::ErrorLogName | Should -Be '__pode_log_errors__' } } diff --git a/tests/unit/Schedules.Tests.ps1 b/tests/unit/Schedules.Tests.ps1 index 4f854672a..ce426bbe0 100644 --- a/tests/unit/Schedules.Tests.ps1 +++ b/tests/unit/Schedules.Tests.ps1 @@ -129,7 +129,7 @@ Describe 'Add-PodeSchedule' { $start = ([DateTime]::Now.AddHours(3)) $end = ([DateTime]::Now.AddHours(5)) - Add-PodeSchedule -Name 'test' -Cron @('@minutely', '@hourly') -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end + Add-PodeSchedule -Name 'test' -Cron @('@minutely', '@hourly') -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedule = $PodeContext.Schedules.Items['test'] $schedule | Should -Not -Be $null diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 1818fb6bf..1a47b23f2 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -26,7 +26,7 @@ Describe 'Start-PodeInternalServer' { Mock Invoke-PodeScriptBlock {} Mock New-PodeRunspaceState {} Mock New-PodeRunspacePool {} - Mock Start-PodeLoggingRunspace {} + Mock Start-PodeLoggerDispatcher {} Mock Start-PodeTimerRunspace {} Mock Start-PodeScheduleRunspace {} Mock Start-PodeGuiRunspace {} @@ -45,6 +45,8 @@ Describe 'Start-PodeInternalServer' { Mock Add-PodeScopedVariablesInbuilt {} Mock Write-PodeHost {} Mock Show-PodeConsoleInfo {} + Mock Write-PodeErrorLog { } + Mock Write-PodeLog { } } It 'Calls one-off script logic' { @@ -114,10 +116,10 @@ Describe 'Restart-PodeInternalServer' { Mock Close-PodeRunspace {} Mock Remove-PodePSDrive {} Mock Open-PodeConfiguration { return $null } - Mock Start-PodeInternalServer {} - Mock Write-PodeErrorLog {} - Mock Close-PodeDisposable {} - Mock Invoke-PodeEvent {} + Mock Start-PodeInternalServer { } + Mock Write-PodeErrorLog { } + Mock Close-PodeDisposable { } + Mock Invoke-PodeEvent { } } It 'Resetting the server values' { @@ -135,7 +137,9 @@ Describe 'Restart-PodeInternalServer' { key = @{} } Logging = @{ - Types = @{ 'key' = 'value' } + Type = @{ 'key' = 'value' } + LogsToProcess = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new() + Method = @{ 'key' = 'value' } } Middleware = @{ 'key' = 'value' } Endpoints = @{ 'key' = 'value' } @@ -281,7 +285,7 @@ Describe 'Restart-PodeInternalServer' { Restart-PodeInternalServer | Out-Null $PodeContext.Server.Routes['GET'].Count | Should -Be 0 - $PodeContext.Server.Logging.Types.Count | Should -Be 0 + $PodeContext.Server.Logging.Type.Count | Should -Be 0 $PodeContext.Server.Middleware.Count | Should -Be 0 $PodeContext.Server.Endware.Count | Should -Be 0 $PodeContext.Server.Sessions.Count | Should -Be 0 diff --git a/tests/unit/Serverless.Tests.ps1 b/tests/unit/Serverless.Tests.ps1 index 66f29c59c..4509889f3 100644 --- a/tests/unit/Serverless.Tests.ps1 +++ b/tests/unit/Serverless.Tests.ps1 @@ -6,6 +6,7 @@ BeforeAll { $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' + Mock Test-PodeLoggerEnabled { return $false } } Describe 'Start-PodeAzFuncServer' { BeforeAll { @@ -25,6 +26,8 @@ Describe 'Start-PodeAzFuncServer' { Mock Set-PodeServerHeader { } Mock Set-PodeResponseStatus { } Mock Update-PodeServerRequestMetric { } + Mock Write-PodeLog {} + Mock Write-PodeErrorLog {} } It 'Throws error for null data' { { Start-PodeAzFuncServer -Data $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorNullNotAllowed,Start-PodeAzFuncServer' @@ -155,7 +158,11 @@ Describe 'Start-PodeAwsLambdaServer' { Mock Invoke-PodeEndware { } Mock Set-PodeServerHeader { } Mock Set-PodeResponseStatus { } - Mock Update-PodeServerRequestMetric { } } + Mock Update-PodeServerRequestMetric { } + Mock Write-PodeLog {} + Mock Write-PodeErrorLog {} + + } It 'Throws error for null data' { { Start-PodeAwsLambdaServer -Data $null } | Should -Throw -ErrorId 'ParameterArgumentValidationErrorNullNotAllowed,Start-PodeAwsLambdaServer' diff --git a/tests/unit/Timers.Tests.ps1 b/tests/unit/Timers.Tests.ps1 index f8607c73f..d25f7e4d0 100644 --- a/tests/unit/Timers.Tests.ps1 +++ b/tests/unit/Timers.Tests.ps1 @@ -8,6 +8,7 @@ BeforeAll { Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' + $PodeContext = @{ 'Server' = $null; } } @@ -71,7 +72,7 @@ Describe 'Add-PodeTimer' { It 'Adds new timer to session with no limit' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock { Write-Host 'hello' } -Limit 0 -Skip 1 + Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock { Write-Host 'hello' } -Limit 0 -Skip 1 $timer = $PodeContext.Timers.Items['test'] $timer | Should -Not -Be $null