Action.Logging.psm1
# ======================================== # Action.Logging Module # ======================================== # # Module Version : 2.4.0 # Date Created: 06-21-2025 # Last Revised: 08-31-2025 # Revision details: v2.4.0 - Minor documentation improvements and version consistency updates # Created by : Gonzalo More # Required files: None # Required folders: None # # Licensed under the MIT License. # See LICENSE file in the project root for full license information. # ======================================== <# .SYNOPSIS Action.Logging - Enhanced PowerShell Logging Module with async support. .LINK For full documentation, see README.md #> # Enhanced logging structure with severity levels enum LogLevel { ERROR = 1 WARN = 2 INFO = 3 DEBUG = 4 TRACE = 5 } # Console color mapping for log levels $script:LogLevelColors = @{ [LogLevel]::ERROR = 'Red' [LogLevel]::WARN = 'Yellow' [LogLevel]::INFO = 'Green' [LogLevel]::DEBUG = 'Cyan' [LogLevel]::TRACE = 'Gray' } # Module-level variables for async logging $script:LogQueue = [System.Collections.Concurrent.ConcurrentQueue[PSObject]]::new() $script:LoggerRunspace = $null $script:LoggerPowerShell = $null $script:AsyncHandle = $null # Store the async invocation handle $script:CurrentLogFile = $null $script:AsyncLoggingEnabled = $false $script:ConsoleOutputEnabled = $true $script:FileOutputEnabled = $true $script:DefaultLogRetentionCount = 10 $script:MinLogLevel = [LogLevel]::INFO # Minimum log level for filtering $script:VerifiedPaths = @{} $script:SessionFiles = @{} $script:CustomScriptName = $null $script:EffectiveScriptName = $null $script:CustomLogPath = $null $script:SyncHash = $null # Synchronized hashtable for async logging <# .SYNOPSIS Internal function to initialize the effective script name for logging. .DESCRIPTION Sets the $script:EffectiveScriptName variable based on custom name or auto-detection. Auto-detection is performed only once and the result is cached. .NOTES This is an internal function not exported by the module. #> function Initialize-EffectiveScriptName { [CmdletBinding()] param() # If custom script name is set, use it if ($script:CustomScriptName) { $script:EffectiveScriptName = $script:CustomScriptName return } # Auto-detect the script name by walking the call stack try { $callStack = Get-PSCallStack $scriptName = $null # Walk through the call stack to find the first script that's not this module foreach ($frame in $callStack) { if ($frame.ScriptName -and $frame.ScriptName -ne $PSCommandPath -and -not $frame.ScriptName.EndsWith('Action.Logging.psm1')) { try { $rawName = [System.IO.Path]::GetFileNameWithoutExtension($frame.ScriptName) # Sanitize script name to remove invalid characters for filenames $scriptName = $rawName -replace '[<>:"/\\|?*]', '_' if ($scriptName -and $scriptName.Trim() -ne '') { break } } catch { # Skip this frame if path processing fails continue } } } # Use detected script name or fallback to module name if ($scriptName -and $scriptName.Trim() -ne '') { $script:EffectiveScriptName = $scriptName } else { $script:EffectiveScriptName = "Action.Logging" } } catch { # If call stack analysis fails entirely, use fallback Write-Warning "Failed to detect script name from call stack: $($_.Exception.Message)" $script:EffectiveScriptName = "Action.Logging" } } <# .SYNOPSIS Internal function to format log messages consistently. .DESCRIPTION Centralizes message formatting to ensure consistent timestamping and formatting across all logging functions. Makes future metadata additions easier. .PARAMETER Message The message to format. .PARAMETER Level The log level for the message. .PARAMETER When Optional timestamp. Defaults to current time. .OUTPUTS String. Formatted log entry with timestamp, level, and message. .NOTES This is an internal function not exported by the module. Used by Write-EnhancedLog, Write-EnhancedLogAsync, and async worker. #> function Format-LogMessage { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Message, [Parameter(Mandatory = $true)] [LogLevel]$Level, [Parameter(Mandatory = $false)] [datetime]$When = (Get-Date) ) return "$($When.ToString('yyyy-MM-dd HH:mm:ss')) [$Level] $Message" } <# .SYNOPSIS Internal helper function to get the effective log path. .DESCRIPTION Determines the log path based on custom settings or defaults. Used by Write-EnhancedLog and Start-AsyncLogger. .NOTES - Internal function, not exported - Returns the effective log path #> function Get-EffectiveLogPath { [CmdletBinding()] [OutputType([string])] param() if ($script:CustomLogPath) { # Validate custom path format try { $validatedPath = [System.IO.Path]::GetFullPath($script:CustomLogPath) return $validatedPath } catch { Write-Warning "Invalid custom log path '$($script:CustomLogPath)': $($_.Exception.Message). Using default path." # Fall through to default path logic } } # Ensure effective script name is initialized if (-not $script:EffectiveScriptName) { Initialize-EffectiveScriptName } # Sanitize script name for safe path construction $safeName = $script:EffectiveScriptName -replace '[<>:"/\\|?*]', '_' if (-not $safeName -or $safeName.Trim() -eq '') { $safeName = "Action.Logging" } # Construct and validate default path try { $defaultPath = Join-Path -Path "C:\temp\logs" -ChildPath $safeName $validatedPath = [System.IO.Path]::GetFullPath($defaultPath) return $validatedPath } catch { Write-Warning "Failed to construct log path: $($_.Exception.Message). Using fallback path." return "C:\temp\logs\Action.Logging" } } <# .SYNOPSIS Internal helper function to initialize log path and session file. .DESCRIPTION Consolidates the common logic for path verification, directory creation, session file initialization, and log cleanup. Used by Write-EnhancedLog, Start-AsyncLogger, and Set-LogPath. .PARAMETER Path The log directory path to initialize. .NOTES - Internal function, not exported - Returns the full path to the session log file - Only performs operations if FileOutputEnabled is true #> function Initialize-LogPath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Path ) # Only perform operations if file output is enabled if (-not $script:FileOutputEnabled) { return $null } # Validate path format and length (Windows max path length is typically 260 characters) try { $fullPath = [System.IO.Path]::GetFullPath($Path) if ($fullPath.Length -gt 240) { # Leave room for filename Write-Warning "Log path is too long (${fullPath.Length} characters). Maximum recommended is 240 characters to allow for filenames." return $null } } catch { Write-Warning "Invalid log path format '$Path': $($_.Exception.Message)" return $null } # Check if path has already been verified this session if (-not $script:VerifiedPaths.ContainsKey($fullPath)) { # Create directory if it doesn't exist if (-not (Test-Path $fullPath)) { try { $createdDir = New-Item -ItemType Directory -Path $fullPath -Force if (-not $createdDir -or -not (Test-Path $fullPath)) { Write-Warning "Failed to verify log directory creation: '$fullPath'" return $null } } catch { Write-Warning "Failed to create log directory '$fullPath': $($_.Exception.Message)" return $null } } # Test write permissions try { $testFile = Join-Path -Path $fullPath -ChildPath "write_test_$(Get-Date -Format 'yyyyMMddHHmmss').tmp" "test" | Out-File -FilePath $testFile -Force Remove-Item -Path $testFile -Force -ErrorAction SilentlyContinue } catch { Write-Warning "No write permission to log directory '$fullPath': $($_.Exception.Message)" return $null } $script:VerifiedPaths[$fullPath] = $true # Initialize effective script name if not already done if (-not $script:EffectiveScriptName) { Initialize-EffectiveScriptName } # Create session file name with script name prefix if ($script:EffectiveScriptName -eq "Action.Logging") { $logFileName = "ActionLogging_$(Get-Date -Format 'yyyyMMdd-HHmmss').log" } else { $logFileName = "$($script:EffectiveScriptName)_$(Get-Date -Format 'yyyyMMdd-HHmmss').log" } # Validate final file path length $sessionFile = Join-Path -Path $fullPath -ChildPath $logFileName if ($sessionFile.Length -gt 260) { Write-Warning "Generated log file path exceeds Windows limits (${sessionFile.Length} characters): '$sessionFile'" return $null } # Store the full path to the session file $script:SessionFiles[$fullPath] = $sessionFile # Clean up old logs with error handling try { Clear-OldLog -LogPath $fullPath } catch { Write-Warning "Failed to clean old logs in '$fullPath': $($_.Exception.Message)" # Continue anyway - this shouldn't prevent logging } } # Return the session file path for this log directory return $script:SessionFiles[$fullPath] } <# .SYNOPSIS Writes enhanced log entries with severity levels and color coding. .DESCRIPTION Creates timestamped log entries with severity levels, saves to log files, and displays color-coded output to the console. Automatically creates log directories if they don't exist. .PARAMETER Message The message to log. .PARAMETER Level The severity level of the log entry. Default is INFO. .EXAMPLE Write-EnhancedLog -Message "Script started" -Level INFO .EXAMPLE Write-EnhancedLog -Message "Critical error occurred" -Level ERROR .EXAMPLE Write-EnhancedLog -Message "Debug information" -Level DEBUG #> function Write-EnhancedLog { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Message, [Parameter(Mandatory = $false)] [LogLevel]$Level = [LogLevel]::INFO ) # Filter messages below minimum log level if ($Level -gt $script:MinLogLevel) { return } # Get the effective log path $LogPath = Get-EffectiveLogPath # Create formatted log entry using centralized formatter $logEntry = Format-LogMessage -Message $Message -Level $Level try { # Only handle file operations if file output is enabled if ($script:FileOutputEnabled) { # Initialize log path if needed and get session file $currentLogFile = Initialize-LogPath -Path $LogPath $script:CurrentLogFile = $currentLogFile # Write to log file if we have a valid session file if ($currentLogFile) { Add-Content -Path $currentLogFile -Value $logEntry -Encoding UTF8 -ErrorAction Stop } } # Output to console with color coding if enabled if ($script:ConsoleOutputEnabled) { $color = $script:LogLevelColors[$Level] if ($color) { Write-Host $logEntry -ForegroundColor $color } else { Write-Host $logEntry } } } catch { # Only show file write errors if file output was expected if ($script:FileOutputEnabled -and $script:ConsoleOutputEnabled) { Write-Warning "Failed to write to log file: $($_.Exception.Message)" # Fallback to console output only Write-Host "$logEntry" -ForegroundColor White } } } <# .SYNOPSIS Cleans up old log files, keeping only the most recent ones. .DESCRIPTION Removes old log files from the specified path, keeping only the last 10 log files to manage disk space. Files are sorted by last write time. .PARAMETER LogPath The path containing log files to clean up. .PARAMETER KeepCount Number of recent log files to keep. Default is 10. .EXAMPLE Clear-OldLog -LogPath "C:\temp\logs\MyScript" .EXAMPLE Clear-OldLog -LogPath "C:\temp\logs\MyScript" -KeepCount 5 #> function Clear-OldLog { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true)] [string]$LogPath, [Parameter(Mandatory = $false)] [ValidateRange(1, 100)] [int]$KeepCount = $script:DefaultLogRetentionCount ) try { if (Test-Path $LogPath) { $logFiles = Get-ChildItem -Path $LogPath -Filter "*.log" | Sort-Object LastWriteTime -Descending if ($logFiles.Count -gt $KeepCount) { $filesToDelete = $logFiles[$KeepCount..($logFiles.Count - 1)] foreach ($file in $filesToDelete) { if ($PSCmdlet.ShouldProcess($file.FullName, 'Remove old log file')) { Remove-Item -Path $file.FullName -Force -ErrorAction Stop } } Write-Verbose "Cleaned up $($filesToDelete.Count) old log files from $LogPath" } } } catch { Write-Verbose "Failed to clean up old logs in '${LogPath}': $($_.Exception.Message)" } } <# .SYNOPSIS Starts an asynchronous logging background worker for high-performance logging. .DESCRIPTION Initializes a background PowerShell runspace that handles log writing asynchronously. This provides better performance for high-volume logging scenarios by queuing log entries and writing them in batches to reduce I/O overhead. Uses the path set by Set-LogPath or defaults to C:\temp\logs\[ScriptName]. .EXAMPLE Start-AsyncLogger Starts async logger using the configured or default log path. .EXAMPLE Set-LogPath -Path "D:\MyApp\Logs" Start-AsyncLogger Starts async logger using the custom log path. .NOTES - Only one async logger can run at a time per PowerShell session - Use Write-EnhancedLogAsync to write to the async logger - Use Stop-AsyncLogger to properly shutdown the background worker - Automatically creates log directories if they don't exist #> function Start-AsyncLogger { [CmdletBinding(SupportsShouldProcess)] param() # Don't start if already running if ($script:AsyncLoggingEnabled) { Write-Warning "Async logging is already running" return } if ($PSCmdlet.ShouldProcess("Async Logger", "Start background logging")) { # Initialize effective script name if not already set if (-not $script:EffectiveScriptName) { Initialize-EffectiveScriptName } # Get the effective log path $LogPath = Get-EffectiveLogPath # Initialize log path and get session file $script:CurrentLogFile = Initialize-LogPath -Path $LogPath # Create runspace for background logging $script:LoggerRunspace = [RunspaceFactory]::CreateRunspace() # Only set STA on Windows (not needed for logging anyway) if ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5) { $script:LoggerRunspace.ApartmentState = [System.Threading.ApartmentState]::STA } $script:LoggerRunspace.ThreadOptions = "ReuseThread" $script:LoggerRunspace.Open() # Create synchronized hashtable for shared state $syncHash = [hashtable]::Synchronized(@{ LogQueue = $script:LogQueue CurrentLogFile = $script:CurrentLogFile StopRequested = $false ConsoleOutputEnabled = $script:ConsoleOutputEnabled FileOutputEnabled = $script:FileOutputEnabled LogLevelColors = $script:LogLevelColors LastError = $null ErrorCount = 0 HasErrors = $false }) # Share synchronized hashtable with runspace $script:LoggerRunspace.SessionStateProxy.SetVariable("syncHash", $syncHash) # Create PowerShell instance for background work $script:LoggerPowerShell = [PowerShell]::Create() $script:LoggerPowerShell.Runspace = $script:LoggerRunspace # Define background worker script $workerScript = { $logBuffer = [System.Collections.Generic.List[string]]::new() $lastFlush = [DateTime]::Now $flushInterval = [TimeSpan]::FromMilliseconds(500) # Use color mapping from SyncHash $levelColors = $syncHash.LogLevelColors # Continue processing until stop is requested AND queue is empty while (-not $syncHash.StopRequested -or $syncHash.LogQueue.Count -gt 0) { $hasMessages = $false $message = $null # Collect messages from queue while ($syncHash.LogQueue.TryDequeue([ref]$message)) { $hasMessages = $true $logBuffer.Add($message.Entry) # Console output based on level if enabled if ($syncHash.ConsoleOutputEnabled) { $color = $levelColors[[LogLevel]$message.Level] if ($color) { Write-Host $message.Entry -ForegroundColor $color } else { Write-Host $message.Entry } } } # Flush buffer to file periodically or when buffer is large (only if file output is enabled) if ($syncHash.FileOutputEnabled -and $syncHash.CurrentLogFile) { $shouldFlush = $logBuffer.Count -gt 0 -and ( $logBuffer.Count -ge 50 -or ([DateTime]::Now - $lastFlush) -gt $flushInterval ) if ($shouldFlush) { try { Add-Content -Path $syncHash.CurrentLogFile -Value $logBuffer -Encoding UTF8 $logBuffer.Clear() $lastFlush = [DateTime]::Now } catch { # Store error information in sync hash for main thread visibility $syncHash.LastError = $_.Exception.Message $syncHash.ErrorCount++ $syncHash.HasErrors = $true Write-Warning "Async logger failed to write: $_" } } } else { # Clear buffer when file output is disabled to prevent memory buildup $logBuffer.Clear() } # Sleep briefly if no messages if (-not $hasMessages) { Start-Sleep -Milliseconds 50 } } # Final flush on shutdown (only if file output is enabled) if ($syncHash.FileOutputEnabled -and $syncHash.CurrentLogFile -and $logBuffer.Count -gt 0) { try { Add-Content -Path $syncHash.CurrentLogFile -Value $logBuffer -Encoding UTF8 } catch { # Store error information in sync hash for main thread visibility $syncHash.LastError = $_.Exception.Message $syncHash.ErrorCount++ $syncHash.HasErrors = $true Write-Warning "Final flush failed: $_" } } } # Start background worker $null = $script:LoggerPowerShell.AddScript($workerScript) $script:AsyncHandle = $script:LoggerPowerShell.BeginInvoke() # Store the handle for proper shutdown # Store sync hash reference $script:SyncHash = $syncHash $script:AsyncLoggingEnabled = $true Write-Verbose "Async logging started for $LogPath" } } <# .SYNOPSIS Stops the asynchronous logging background worker gracefully. .DESCRIPTION Properly shuts down the background PowerShell runspace used for async logging. Ensures all queued log entries are written before stopping and cleans up all resources including runspaces and PowerShell instances. .EXAMPLE Stop-AsyncLogger Stops the currently running async logger. .NOTES - Waits up to 5 seconds for remaining log entries to be processed - Properly disposes of PowerShell runspace and instances - Safe to call even if async logger is not running - Should always be called before script termination if async logging was used #> function Stop-AsyncLogger { [CmdletBinding(SupportsShouldProcess)] param() if (-not $script:AsyncLoggingEnabled) { return } if ($PSCmdlet.ShouldProcess("Async Logger", "Stop background logging")) { Write-Verbose "Stopping async logger..." # Signal stop $script:SyncHash.StopRequested = $true # Wait for queue to empty (max 5 seconds) $timeout = [DateTime]::Now.AddSeconds(5) while ($script:SyncHash.LogQueue.Count -gt 0 -and [DateTime]::Now -lt $timeout) { Start-Sleep -Milliseconds 100 } # Wait for the async operation to complete with timeout if ($script:AsyncHandle -and $script:LoggerPowerShell) { $waitTimeout = 3000 # 3 seconds $completed = $script:AsyncHandle.AsyncWaitHandle.WaitOne($waitTimeout) if ($completed) { # Properly end the invocation to get any results/errors try { $null = $script:LoggerPowerShell.EndInvoke($script:AsyncHandle) } catch { Write-Verbose "Error ending async invocation: $($_.Exception.Message)" } } else { # Force stop if wait timed out Write-Verbose "Async logger did not stop gracefully, forcing stop" if ($script:LoggerPowerShell) { $script:LoggerPowerShell.Stop() } } } # Clean up resources if ($script:LoggerPowerShell) { $script:LoggerPowerShell.Dispose() $script:LoggerPowerShell = $null } if ($script:LoggerRunspace) { $script:LoggerRunspace.Close() $script:LoggerRunspace.Dispose() $script:LoggerRunspace = $null } # Clear the handle $script:AsyncHandle = $null $script:SyncHash = $null $script:AsyncLoggingEnabled = $false Write-Verbose "Async logger stopped" } } <# .SYNOPSIS Writes log entries asynchronously using the background logging worker. .DESCRIPTION Queues log messages for asynchronous processing by the background worker. Provides high-performance logging by avoiding blocking I/O operations. Falls back to synchronous logging if async logger is not running. .PARAMETER Message The message to log. This parameter is mandatory. .PARAMETER Level The severity level of the log entry. Default is INFO. Valid values: ERROR, WARN, INFO, DEBUG, TRACE .EXAMPLE Write-EnhancedLogAsync -Message "Process completed successfully" Writes an INFO level message asynchronously. .EXAMPLE Write-EnhancedLogAsync -Message "Critical error occurred" -Level ERROR Writes an ERROR level message asynchronously. .NOTES - Requires Start-AsyncLogger to be called first - Falls back to Write-EnhancedLog if async logger not running - Messages are queued and written in batches for performance - Use Stop-AsyncLogger to ensure all messages are written before exit #> function Write-EnhancedLogAsync { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Message, [Parameter(Mandatory = $false)] [LogLevel]$Level = [LogLevel]::INFO ) # Filter messages below minimum log level if ($Level -gt $script:MinLogLevel) { return } # Fall back to sync logging if async not enabled if (-not $script:AsyncLoggingEnabled) { Write-EnhancedLog -Message $Message -Level $Level return } # Check for async logger errors and warn user if logging is failing if ($script:SyncHash -and $script:SyncHash.HasErrors) { Write-Warning "CRITICAL: Async logging is experiencing failures! Last error: $($script:SyncHash.LastError). Total errors: $($script:SyncHash.ErrorCount). Consider switching to synchronous logging or check log file permissions." # Reset the error flag after warning (prevents spam but still alerts on new errors) $script:SyncHash.HasErrors = $false } # Create formatted log entry using centralized formatter $logEntry = Format-LogMessage -Message $Message -Level $Level # Queue the message $logMessage = [PSCustomObject]@{ Entry = $logEntry Level = $Level } $script:LogQueue.Enqueue($logMessage) } <# .SYNOPSIS Controls whether log entries are displayed in the console. .DESCRIPTION Enables or disables console output for all logging functions in the module. When disabled, log entries are still written to files but not displayed in the console. Affects both synchronous and asynchronous logging. .PARAMETER Enabled Boolean value to enable (True) or disable (False) console output. This parameter is mandatory. .EXAMPLE Set-LoggingConsoleOutput -Enabled $false Disables console output for all logging functions. .EXAMPLE Set-LoggingConsoleOutput -Enabled $true Enables console output for all logging functions. .NOTES - Affects both Write-EnhancedLog and Write-EnhancedLogAsync functions - Changes are applied immediately to running async logger if active - Log files are still created regardless of console output setting - Useful for silent automation scenarios or reducing console noise #> function Set-LoggingConsoleOutput { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true)] [bool]$Enabled ) if ($PSCmdlet.ShouldProcess("Console Output", "Set logging console output to $Enabled")) { $script:ConsoleOutputEnabled = $Enabled # Update async logger if running if ($script:AsyncLoggingEnabled -and $script:SyncHash) { $script:SyncHash.ConsoleOutputEnabled = $Enabled } $status = if ($Enabled) { "enabled" } else { "disabled" } Write-Verbose "Console output $status for logging module" } } <# .SYNOPSIS Controls whether log entries are written to files or only displayed in console. .DESCRIPTION The Set-LoggingFileOutput function enables or disables file logging for the Action.Logging module. When disabled, all logging operations will only output to the console with color coding, without creating any log files or directories. This is useful for environments where file persistence is not needed or desired, such as CI/CD pipelines or containerized applications. .PARAMETER Enabled Boolean value to enable (True) or disable (False) file output. This parameter is mandatory. .EXAMPLE Set-LoggingFileOutput -Enabled $false Disables file output for all logging functions - console only mode. .EXAMPLE Set-LoggingFileOutput -Enabled $true Enables file output for all logging functions (default behavior). .NOTES - Affects both Write-EnhancedLog and Write-EnhancedLogAsync functions - Changes are applied immediately to running async logger if active - Console output remains active regardless of this setting - When disabled, no directories are created and no files are written - Useful for reducing I/O overhead when file persistence isn't required #> function Set-LoggingFileOutput { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true)] [bool]$Enabled ) if ($PSCmdlet.ShouldProcess("File Output", "Set logging file output to $Enabled")) { $script:FileOutputEnabled = $Enabled # Update async logger if running if ($script:AsyncLoggingEnabled -and $script:SyncHash) { $script:SyncHash.FileOutputEnabled = $Enabled } $status = if ($Enabled) { "enabled" } else { "disabled" } Write-Verbose "File output $status for logging module" } } <# .SYNOPSIS Sets the default log retention count for automatic cleanup. .DESCRIPTION Configures how many log files to keep when automatic cleanup occurs. This setting applies to all logging functions in the module. .PARAMETER Count Number of log files to keep. Must be between 1 and 100. Default is 10. .EXAMPLE Set-LogRetention -Count 25 Sets log retention to keep 25 most recent log files. .EXAMPLE Set-LogRetention -Count 1 Sets minimal log retention to keep only 1 most recent log file. #> function Set-LogRetention { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $false)] [ValidateRange(1, 100)] [int]$Count = 10 ) if ($PSCmdlet.ShouldProcess("Log Retention", "Set default log retention count to $Count")) { $script:DefaultLogRetentionCount = $Count Write-Verbose "Log retention count set to $Count files" } } <# .SYNOPSIS Sets the minimum log level for filtering log messages. .DESCRIPTION Configures the minimum severity level for log messages. Messages below this level will be filtered out and not processed, improving performance and reducing noise. This affects both synchronous and asynchronous logging functions. .PARAMETER Level The minimum log level. Valid values: ERROR, WARN, INFO, DEBUG, TRACE. Default is INFO. .EXAMPLE Set-MinLogLevel -Level WARN Sets the minimum log level to WARN. DEBUG and TRACE messages will be filtered out. .EXAMPLE Set-MinLogLevel -Level ERROR Sets the minimum log level to ERROR. Only ERROR messages will be processed. .NOTES - Affects both Write-EnhancedLog and Write-EnhancedLogAsync functions - Lower severity messages are completely filtered out (not processed at all) - Changes take effect immediately for all subsequent log operations - Useful for reducing log volume in production environments #> function Set-MinLogLevel { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true)] [LogLevel]$Level ) if ($PSCmdlet.ShouldProcess("Minimum Log Level", "Set minimum log level to $Level")) { $script:MinLogLevel = $Level Write-Verbose "Minimum log level set to $Level" } } <# .SYNOPSIS Sets a custom script name for log file naming. .DESCRIPTION Configures a custom script name that will be used for all subsequent logging operations instead of auto-detecting the script name from the call stack. Call this function once at the beginning of your script to use a custom name. .PARAMETER ScriptName The custom script name to use for log file naming. If not specified or set to null/empty, the module will revert to auto-detection. .EXAMPLE Set-LogScriptName -ScriptName "MyCustomProcess" Sets the script name to "MyCustomProcess" for all logging operations. .EXAMPLE Set-LogScriptName -ScriptName $null Clears the custom script name and reverts to auto-detection. .NOTES - Call this once at the beginning of your script - Affects all logging functions: Write-EnhancedLog, Write-EnhancedLogAsync, Start-AsyncLogger - The custom name persists for the entire PowerShell session or until changed #> function Set-LogScriptName { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [string]$ScriptName ) if ($PSCmdlet.ShouldProcess("Log Script Name", "Set custom script name to '$ScriptName'")) { if ([string]::IsNullOrWhiteSpace($ScriptName)) { $script:CustomScriptName = $null # Clear effective name to force re-detection on next use $script:EffectiveScriptName = $null # Clear session caches to force new log files with new name $script:SessionFiles = @{} $script:VerifiedPaths = @{} # Update async logger target file if running if ($script:AsyncLoggingEnabled -and $script:SyncHash) { $newLogPath = Get-EffectiveLogPath $script:SyncHash.CurrentLogFile = Initialize-LogPath -Path $newLogPath Write-Verbose "Updated async logger target file to: $($script:SyncHash.CurrentLogFile)" } Write-Verbose "Custom script name cleared - reverting to auto-detection" } else { # Validate script name for invalid filename characters $invalidChars = [IO.Path]::GetInvalidFileNameChars() $hasInvalidChars = $ScriptName.IndexOfAny($invalidChars) -ge 0 if ($hasInvalidChars) { $invalidCharsList = ($invalidChars | ForEach-Object { if ($_ -eq "`0") { "null" } else { "'$_'" } }) -join ", " throw "Invalid script name '$ScriptName'. Script names cannot contain these characters: $invalidCharsList" } # Validate script name is not too long (Windows filename limit minus timestamp and extension) if ($ScriptName.Length -gt 200) { throw "Script name '$ScriptName' is too long (maximum 200 characters)" } $script:CustomScriptName = $ScriptName # Set the effective name immediately $script:EffectiveScriptName = $ScriptName # Clear session caches to force new log files with new name $script:SessionFiles = @{} $script:VerifiedPaths = @{} # Update async logger target file if running if ($script:AsyncLoggingEnabled -and $script:SyncHash) { $newLogPath = Get-EffectiveLogPath $script:SyncHash.CurrentLogFile = Initialize-LogPath -Path $newLogPath Write-Verbose "Updated async logger target file to: $($script:SyncHash.CurrentLogFile)" } Write-Verbose "Custom script name set to: $ScriptName" } } } <# .SYNOPSIS Sets a custom log path for all logging operations. .DESCRIPTION Configures a custom log directory path that will be used for all subsequent logging operations instead of the default path based on script name. Call this function once at the beginning of your script to use a custom log location. .PARAMETER Path The custom log directory path. If not specified or set to null/empty, the module will revert to the default path (C:\temp\logs\[ScriptName]). .EXAMPLE Set-LogPath -Path "D:\MyApp\Logs" Sets the log path to "D:\MyApp\Logs" for all logging operations. .EXAMPLE Set-LogPath -Path $null Clears the custom log path and reverts to default location. .NOTES - Call this once at the beginning of your script - Affects all logging functions: Write-EnhancedLog, Write-EnhancedLogAsync, Start-AsyncLogger - The custom path persists for the entire PowerShell session or until changed - Directory is created if it doesn't exist #> function Set-LogPath { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [string]$Path ) if ($PSCmdlet.ShouldProcess("Log Path", "Set custom log path to '$Path'")) { if ([string]::IsNullOrWhiteSpace($Path)) { # Store old path before clearing $oldPath = $script:CustomLogPath $script:CustomLogPath = $null # Clear verified paths cache for the old custom path if ($oldPath -and $script:VerifiedPaths.ContainsKey($oldPath)) { $script:VerifiedPaths.Remove($oldPath) $script:SessionFiles.Remove($oldPath) } # Update async logger target file if running if ($script:AsyncLoggingEnabled -and $script:SyncHash) { $newLogPath = Get-EffectiveLogPath $script:SyncHash.CurrentLogFile = Initialize-LogPath -Path $newLogPath Write-Verbose "Updated async logger target file to: $($script:SyncHash.CurrentLogFile)" } Write-Verbose "Custom log path cleared - reverting to default location" } else { # Validate path for invalid path characters $invalidChars = [IO.Path]::GetInvalidPathChars() $hasInvalidChars = $Path.IndexOfAny($invalidChars) -ge 0 if ($hasInvalidChars) { $invalidCharsList = ($invalidChars | ForEach-Object { if ($_ -eq "`0") { "null" } else { "'$_'" } }) -join ", " throw "Invalid log path '$Path'. Paths cannot contain these characters: $invalidCharsList" } # Convert to absolute path for consistency try { $Path = [IO.Path]::GetFullPath($Path) } catch { throw "Invalid log path '$Path': $($_.Exception.Message)" } $script:CustomLogPath = $Path # Initialize the log path (handles file output check internally) Initialize-LogPath -Path $Path | Out-Null # Update async logger target file if running if ($script:AsyncLoggingEnabled -and $script:SyncHash) { $script:SyncHash.CurrentLogFile = Initialize-LogPath -Path $Path Write-Verbose "Updated async logger target file to: $($script:SyncHash.CurrentLogFile)" } Write-Verbose "Custom log path set to: $Path" } } } <# .SYNOPSIS Gets the current status and configuration of the Action.Logging module. .DESCRIPTION Returns comprehensive diagnostic information about the current state of the Action.Logging module, including async logger status, configuration settings, file paths, and error information. .OUTPUTS Hashtable. Contains the following keys: - AsyncRunning: Boolean indicating if async logger is active - ConsoleEnabled: Boolean indicating if console output is enabled - FileEnabled: Boolean indicating if file output is enabled - CurrentLogFile: Path to current log file (if file output enabled) - LogPath: Current log directory path - CustomScriptName: Custom script name if set, otherwise null - EffectiveScriptName: The script name currently being used for logs - RetentionCount: Number of log files to retain during cleanup - QueueLength: Number of log messages currently queued for async processing - LastError: Last error from async logging (if any) - ErrorCount: Total number of async logging errors - HasErrors: Boolean indicating if there are unresolved async errors .EXAMPLE $status = Get-LoggingStatus Write-Host "Async logging: $($status.AsyncRunning)" Write-Host "Current log file: $($status.CurrentLogFile)" .EXAMPLE $status = Get-LoggingStatus if ($status.HasErrors) { Write-Warning "Logging errors detected: $($status.ErrorCount)" Write-Warning "Last error: $($status.LastError)" } .NOTES This function is useful for troubleshooting logging issues and monitoring the health of the logging system, especially in automated scenarios. #> function Get-LoggingStatus { [CmdletBinding()] param() # Initialize the effective script name if not already done if (-not $script:EffectiveScriptName) { Initialize-EffectiveScriptName } # Get current log path $currentLogPath = Get-EffectiveLogPath # Build status hashtable $status = @{ AsyncRunning = $script:AsyncLoggingEnabled ConsoleEnabled = $script:ConsoleOutputEnabled FileEnabled = $script:FileOutputEnabled CurrentLogFile = $script:CurrentLogFile LogPath = $currentLogPath CustomScriptName = $script:CustomScriptName EffectiveScriptName = $script:EffectiveScriptName RetentionCount = $script:DefaultLogRetentionCount QueueLength = $script:LogQueue.Count LastError = $null ErrorCount = 0 HasErrors = $false } # Add async error information if available if ($script:SyncHash) { $status.LastError = $script:SyncHash.LastError $status.ErrorCount = $script:SyncHash.ErrorCount $status.HasErrors = $script:SyncHash.HasErrors } return $status } # Export module functions and enum Export-ModuleMember -Function Write-EnhancedLog, Clear-OldLog, Start-AsyncLogger, Stop-AsyncLogger, Write-EnhancedLogAsync, Set-LoggingConsoleOutput, Set-LoggingFileOutput, Set-LogRetention, Set-MinLogLevel, Set-LogScriptName, Set-LogPath, Get-LoggingStatus # The LogLevel enum is already available as [LogLevel] after module import # No need to inject it as a global variable |