Action.Logging.psm1
# ======================================== # Action.Logging Module # ======================================== # # Module Version : 2.0 # Date Created: 06-21-2025 # Last Revised: 06-22-2025 # 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 } # Module-level variables for async logging $script:LogQueue = [System.Collections.Concurrent.ConcurrentQueue[PSObject]]::new() $script:LoggerRunspace = $null $script:LoggerPowerShell = $null $script:CurrentLogFile = $null $script:AsyncLoggingEnabled = $false $script:LoggingStopRequested = $false $script:ConsoleOutputEnabled = $true <# .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. .PARAMETER LogPath The path where log files will be stored. Default is based on script name. .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 -LogPath "C:\custom\logs" #> function Write-EnhancedLog { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Message, [Parameter(Mandatory = $false)] [LogLevel]$Level = [LogLevel]::INFO, [Parameter(Mandatory = $false)] [string]$LogPath ) # If no LogPath specified, use calling script name if (-not $LogPath) { $callingScript = (Get-PSCallStack)[1].ScriptName if ($callingScript) { $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($callingScript) $LogPath = "C:\temp\automation\logs\$scriptName" } else { $LogPath = "C:\temp\automation\logs\PowerShell" } } try { # Ensure log directory exists if (-not (Test-Path $LogPath)) { New-Item -ItemType Directory -Path $LogPath -Force | Out-Null } # Create session-based log file if not exists if (-not $script:CurrentLogFile) { $script:CurrentLogFile = "$LogPath\$(Get-Date -Format 'MMddyyyy-HHmmss').log" # Clean up old logs when creating new session Clear-OldLog -LogPath $LogPath } $timestamp = Get-Date -Format 'MM-dd-yyyy HH:mm:ss' $logEntry = "$timestamp [$Level] $Message" # Write to log file Add-Content -Path $script:CurrentLogFile -Value $logEntry -Encoding UTF8 -ErrorAction SilentlyContinue # Output to console with color coding if enabled if ($script:ConsoleOutputEnabled) { switch ($Level) { ([LogLevel]::ERROR) { Write-Host $logEntry -ForegroundColor Red } ([LogLevel]::WARN) { Write-Host $logEntry -ForegroundColor Yellow } ([LogLevel]::INFO) { Write-Host $logEntry -ForegroundColor Green } ([LogLevel]::DEBUG) { Write-Host $logEntry -ForegroundColor Cyan } ([LogLevel]::TRACE) { Write-Host $logEntry -ForegroundColor Gray } } } } catch { if ($script:ConsoleOutputEnabled) { Write-Warning "Failed to write to log: $($_.Exception.Message)" # Fallback to console output only Write-Host "$timestamp [$Level] $Message" -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 creation 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-OldLogs -LogPath "C:\temp\automation\logs\MyScript" .EXAMPLE Clear-OldLogs -LogPath "C:\temp\automation\logs\MyScript" -KeepCount 5 #> function Clear-OldLog { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$LogPath, [Parameter(Mandatory = $false)] [int]$KeepCount = 10 ) try { if (Test-Path $LogPath) { $logFiles = Get-ChildItem -Path $LogPath -Filter "*.log" | Sort-Object CreationTime -Descending if ($logFiles.Count -gt $KeepCount) { $filesToDelete = $logFiles[$KeepCount..($logFiles.Count - 1)] $filesToDelete | Remove-Item -Force -ErrorAction SilentlyContinue Write-Verbose "Cleaned up $($filesToDelete.Count) old log files from $LogPath" } } } catch { Write-Warning "Failed to clean up old logs in ${LogPath}: $($_.Exception.Message)" } } # Start async logging background worker function Start-AsyncLogger { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $false)] [string]$LogPath ) # 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 log path if (-not $LogPath) { $callingScript = (Get-PSCallStack)[1].ScriptName if ($callingScript) { $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($callingScript) $LogPath = "C:\temp\automation\logs\$scriptName" } else { $LogPath = "C:\temp\automation\logs\PowerShell" } } # Ensure log directory exists if (-not (Test-Path $LogPath)) { New-Item -ItemType Directory -Path $LogPath -Force | Out-Null } # Create session log file $script:CurrentLogFile = "$LogPath\$(Get-Date -Format 'MMddyyyy-HHmmss').log" Clear-OldLog -LogPath $LogPath # Create runspace for background logging $script:LoggerRunspace = [RunspaceFactory]::CreateRunspace() $script:LoggerRunspace.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 }) # 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) while (-not $syncHash.StopRequested) { $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) { switch ($message.Level) { 'ERROR' { Write-Host $message.Entry -ForegroundColor Red } 'WARN' { Write-Host $message.Entry -ForegroundColor Yellow } 'INFO' { Write-Host $message.Entry -ForegroundColor Green } 'DEBUG' { Write-Host $message.Entry -ForegroundColor Cyan } 'TRACE' { Write-Host $message.Entry -ForegroundColor Gray } } } } # Flush buffer to file periodically or when buffer is large $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 { Write-Warning "Async logger failed to write: $_" } } # Sleep briefly if no messages if (-not $hasMessages) { Start-Sleep -Milliseconds 50 } } # Final flush on shutdown if ($logBuffer.Count -gt 0) { try { Add-Content -Path $syncHash.CurrentLogFile -Value $logBuffer -Encoding UTF8 } catch { Write-Warning "Final flush failed: $_" } } } # Start background worker $null = $script:LoggerPowerShell.AddScript($workerScript) $null = $script:LoggerPowerShell.BeginInvoke() # Store sync hash reference $script:SyncHash = $syncHash $script:AsyncLoggingEnabled = $true Write-Host "Async logging started for $LogPath" -ForegroundColor Green } } # Stop async logging gracefully function Stop-AsyncLogger { [CmdletBinding(SupportsShouldProcess)] param() if (-not $script:AsyncLoggingEnabled) { return } if ($PSCmdlet.ShouldProcess("Async Logger", "Stop background logging")) { Write-Host "Stopping async logger..." -ForegroundColor Yellow # Signal stop $script:SyncHash.StopRequested = $true # Wait for queue to empty (max 5 seconds) $timeout = [DateTime]::Now.AddSeconds(5) while ($script:LogQueue.Count -gt 0 -and [DateTime]::Now -lt $timeout) { Start-Sleep -Milliseconds 100 } # Clean up resources if ($script:LoggerPowerShell) { $script:LoggerPowerShell.Stop() $script:LoggerPowerShell.Dispose() $script:LoggerPowerShell = $null } if ($script:LoggerRunspace) { $script:LoggerRunspace.Close() $script:LoggerRunspace.Dispose() $script:LoggerRunspace = $null } $script:AsyncLoggingEnabled = $false $script:LoggingStopRequested = $false Write-Host "Async logger stopped" -ForegroundColor Green } } # Write log entry asynchronously function Write-EnhancedLogAsync { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Message, [Parameter(Mandatory = $false)] [LogLevel]$Level = [LogLevel]::INFO ) # Fall back to sync logging if async not enabled if (-not $script:AsyncLoggingEnabled) { Write-EnhancedLog -Message $Message -Level $Level return } $timestamp = Get-Date -Format 'MM-dd-yyyy HH:mm:ss' $logEntry = "$timestamp [$Level] $Message" # Queue the message $logMessage = [PSCustomObject]@{ Entry = $logEntry Level = $Level.ToString() } $script:LogQueue.Enqueue($logMessage) } # Control console output for logging 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" } } # Export module functions and enum Export-ModuleMember -Function Write-EnhancedLog, Clear-OldLog, Start-AsyncLogger, Stop-AsyncLogger, Write-EnhancedLogAsync, Set-LoggingConsoleOutput |