Private/Logging/Write-StructuredLogEntry.ps1
|
function Write-StructuredLogEntry { <# .SYNOPSIS Writes structured log entries with consistent formatting #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Message, [Parameter(Mandatory)] [ValidateSet('Critical', 'Error', 'Warning', 'Information', 'Debug', 'Verbose', 'INFO')] [string]$Level, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Component = 'General', [Parameter()] [ValidateNotNullOrEmpty()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString(), [Parameter()] [string]$LogPath, [Parameter()] [hashtable]$Details = @{} ) # Normalize INFO to Information if ($Level -eq 'INFO') { $Level = 'Information' } try { # Create structured log entry $logEntry = [PSCustomObject]@{ Timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fffK" Level = $Level Component = $Component CorrelationId = $CorrelationId Message = $Message } # Add additional details if ($Details.Count -gt 0) { foreach ($key in $Details.Keys) { $logEntry | Add-Member -MemberType NoteProperty -Name $key -Value $Details[$key] } } # Format the entry - create simple formatted strings $timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fff" $formattedEntryForFile = "[$timestamp] [$Level] [$Component] [$CorrelationId] $Message" if ($Details.Count -gt 0) { $detailsJson = $Details | ConvertTo-Json -Compress $formattedEntryForFile += " | Details: $detailsJson" } $formattedEntryForConsole = "[$Level] [$Component] $Message" # Determine the log path - use parameter if provided, otherwise try to get from logging system state $effectiveLogPath = $LogPath if (-not $effectiveLogPath) { try { if (Get-Command Get-LoggingSystemState -ErrorAction SilentlyContinue) { $loggingState = Get-LoggingSystemState $effectiveLogPath = $loggingState.LogPath } } catch { # If we can't get logging state, just use console output Write-Verbose "Could not retrieve logging system state: $($_.Exception.Message)" } } # Write to appropriate destination # Always write to console for user feedback Write-ConsoleLogEntry -FormattedMessage $formattedEntryForConsole -Level $Level # File output honours the configured -LogLevel; console output does not, so that warnings and # errors still reach an operator who lowered the file verbosity. Security entries bypass this # filter through Write-SecurityStructuredLogEntry. $writeToFile = $true if (Get-Command Test-LogLevel -ErrorAction SilentlyContinue) { $writeToFile = Test-LogLevel $Level } # Also write to file if log path is available if ($effectiveLogPath -and $writeToFile) { try { # -WhatIf on the public command reaches here through $WhatIfPreference; the log is not # what is being rehearsed, so the write is pinned Add-Content -Path $effectiveLogPath -Value $formattedEntryForFile -Encoding UTF8 -WhatIf:$false } catch { Write-Warning "Failed to write log entry to file: $($_.Exception.Message)" } } # Don't return to pipeline - just write to void to prevent output contamination $null = $logEntry } catch { Write-Warning "Failed to write structured log entry: $($_.Exception.Message)" } } function Write-ConsoleLogEntry { [CmdletBinding()] param( [string]$FormattedMessage, [string]$Level ) # -SuppressConsoleOutput on the public commands sets this; the file still receives every entry if ($script:SuppressConsoleOutput) { return } switch ($Level) { 'Critical' { Write-Error $FormattedMessage } 'Error' { Write-Error $FormattedMessage } 'Warning' { Write-Warning $FormattedMessage } 'Information' { Write-Information $FormattedMessage -InformationAction Continue } 'Debug' { Write-Debug $FormattedMessage } 'Verbose' { Write-Verbose $FormattedMessage } default { Write-Information $FormattedMessage -InformationAction Continue } } } |