Public/Write-EnvkLog.ps1
|
function Write-EnvkLog { <# .SYNOPSIS Writes a timestamped, severity-tagged log entry to both the console and a log file. .DESCRIPTION Formats a log message with an ISO-style timestamp, severity level tag, caller function name, and PowerShell process PID, then writes it to the module log file (Add-Content) and to the console (Write-Host with ANSI color codes). DEBUG entries are suppressed entirely when $script:Config.EnableDebug is not $true. Log file format: 2026-03-01 05:36:12 [INFO] [Start-EnvkAppSequence] [PID:1234] Message text here Console output uses ANSI escape codes for color (PS 5.1 compatible via [char]27): DEBUG = dark gray ([char]27 + [90m) INFO = terminal default (no color codes injected) WARNING = yellow ([char]27 + [33m) ERROR = red ([char]27 + [31m) The log file always receives plain text — no ANSI escape codes are written to disk. File write resilience: Add-Content is wrapped in a 3-attempt retry loop with 50ms between attempts to handle transient IOException from cross-process file locks (e.g., a concurrent parallel run, a log viewer, or Backup-LogFile briefly holding the file). The -Encoding utf8 parameter is specified explicitly for consistency. When an ErrorRecord is supplied, the script name, line number, and exception message are appended to the log entry for quick error triage. .PARAMETER Level The severity level of the message. Must be one of: DEBUG, INFO, WARNING, ERROR. Defaults to 'INFO'. .PARAMETER Message The text to log. Mandatory. .PARAMETER ErrorRecord Optional. A System.Management.Automation.ErrorRecord captured from a catch block. When provided, appends " (Line: <n> in <script>): <exception message>" to the entry. .NOTES Author: Aaron AlAnsari Created: 2026-02-24 Caller resolution: Get-PSCallStack[1] returns the immediate caller of Write-EnvkLog. Index 0 is Write-EnvkLog itself; index 1 is the function that called it. If the call stack is shallow or the caller is a script block, falls back to 'Unknown'. ANSI codes: [char]27 is the ESC character, compatible with PS 5.1 (backtick-e is PS 6+). ANSI codes are injected into Write-Host output only; Add-Content receives plain text. #> [CmdletBinding()] [OutputType([void])] param ( [Parameter()] [ValidateSet('DEBUG', 'INFO', 'WARNING', 'ERROR')] [string]$Level = 'INFO', [Parameter(Mandatory)] [string]$Message, [Parameter()] [System.Management.Automation.ErrorRecord]$ErrorRecord ) # Suppress DEBUG entries when EnableDebug is not set. if ($Level -eq 'DEBUG' -and $script:Config.EnableDebug -ne $true) { return } # Resolve the immediate caller's function name. # Get-PSCallStack[0] = Write-EnvkLog itself; [1] = the calling function. # Placed after the DEBUG-suppression early return to avoid overhead for suppressed entries. $caller = 'Unknown' try { $frame = (Get-PSCallStack)[1] if ($null -ne $frame -and -not [string]::IsNullOrEmpty($frame.FunctionName) -and $frame.FunctionName -ne '<ScriptBlock>') { $caller = $frame.FunctionName } } catch { $caller = 'Unknown' } $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $logMessage = "$timestamp [$Level] [$caller] [PID:$PID] $Message" if ($null -ne $ErrorRecord) { $lineNumber = $ErrorRecord.InvocationInfo.ScriptLineNumber $scriptName = $ErrorRecord.InvocationInfo.ScriptName $errorMsg = $ErrorRecord.Exception.Message $logMessage = "$logMessage (Line: $lineNumber in $scriptName): $errorMsg" } # Ensure the log directory exists before writing. $logDir = Split-Path -Path $script:LogFilePath -Parent if (-not (Test-Path -Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } # Write plain text (no ANSI codes) to the log file. # Retry loop guards against transient IOException when another process (a prior # parallel run, log viewer, or Backup-LogFile) holds a brief exclusive lock. # Five attempts with 100ms between them (400ms total window) is sufficient for # cross-process contention scenarios (e.g., a concurrent PS 7 parallel run holding # the file briefly) without meaningfully delaying sequential execution. # After all retries are exhausted, Write-Host is used as a fallback so the message # is not silently lost (Write-EnvkLog cannot be called recursively). $writeAttempt = 0 $writeSuccess = $false while (-not $writeSuccess -and $writeAttempt -lt 5) { try { Add-Content -Path $script:LogFilePath -Value $logMessage -Encoding utf8 -ErrorAction Stop $writeSuccess = $true } catch [System.IO.IOException] { $writeAttempt++ if ($writeAttempt -lt 5) { Start-Sleep -Milliseconds 100 } } } if (-not $writeSuccess) { # All retry attempts exhausted. Write-Host is the fallback so the message is # not silently dropped. Cannot call Write-EnvkLog here (would recurse). Write-Host "WARNING: Write-EnvkLog could not write to log file after 5 attempts -- $logMessage" } # Build ANSI color sequences for console output. # [char]27 is the ESC character — PS 5.1 compatible (backtick-e requires PS 6+). $esc = [char]27 $reset = "$esc[0m" $colorCode = switch ($Level) { 'DEBUG' { "$esc[90m" } # dark gray / bright black 'INFO' { '' } # terminal default — no color codes for INFO 'WARNING' { "$esc[33m" } # yellow 'ERROR' { "$esc[31m" } # red } if ($colorCode -eq '') { # INFO: no color injection — use plain text to respect terminal theme. Write-Host $logMessage } else { Write-Host "${colorCode}${logMessage}${reset}" } } |