ITFabrik.Logger.psm1
|
# Built file. Do not edit directly; edit source files and rebuild. # Build timestamp: 2026-03-05T17:28:42+00:00 # region Private\Classes\LoggerService.ps1 class LoggerSink { [string]$Type [hashtable]$Options LoggerSink([string]$type, [hashtable]$options) { $this.Type = $type $this.Options = $options } } class LoggerService { hidden static [LoggerService] $instance [System.Collections.ArrayList]$Sinks LoggerService() { $this.Sinks = [System.Collections.ArrayList]::new() } static [LoggerService] GetInstance() { if (-not [LoggerService]::instance) { [LoggerService]::instance = [LoggerService]::new() } return [LoggerService]::instance } static [void] Reset() { [LoggerService]::instance = [LoggerService]::new() } [void] RegisterSink([string]$type, [hashtable]$options) { [void]$this.Sinks.Add([LoggerSink]::new($type, $options)) } [object[]] GetSinks() { return ,$this.Sinks.ToArray() } [void] ConfigureService() { $dispatcher = { param( [Parameter(Mandatory)] [string]$Component, [Parameter(Mandatory)] [string]$Message, [Parameter(Mandatory)] [ValidateSet('Info','Success','Warning','Error','Debug','Verbose')] [string]$Severity, [int]$IndentLevel = 0 ) $svc = [LoggerService]::GetInstance() foreach ($sink in $svc.Sinks) { switch ($sink.Type.ToLower()) { 'console' { Invoke-SMConsoleLogger $Component $Message $Severity $IndentLevel } 'file' { Invoke-FileSink -Options $sink.Options -Component $Component -Message $Message -Severity $Severity -IndentLevel $IndentLevel } 'web' { Invoke-WebSink -Options $sink.Options -Component $Component -Message $Message -Severity $Severity -IndentLevel $IndentLevel } } } } Set-Variable -Name 'StepManagerLogger' -Scope Global -Value $dispatcher } } # endregion # region Private\Functions\Format-ConsoleMessage.ps1 function Format-ConsoleMessage { param( [Parameter(Mandatory)] [string]$Component, [Parameter(Mandatory)] [string]$Message, [Parameter(Mandatory)] [ValidateSet('Info','Success','Warning','Error','Debug','Verbose')] [string]$Severity, [int]$IndentLevel = 0, [string]$StepName = '', [string]$ForegroundColor ) $null = $Component # Préfixe partagé (padding sévérité, indentation, step) $parts = Get-LoggerPrefix -Severity $Severity -IndentLevel $IndentLevel -StepName $StepName switch ($Severity) { 'Info' { if(-not $ForegroundColor){ $ForegroundColor = 'Gray' } } 'Success' { if(-not $ForegroundColor){ $ForegroundColor = 'Green' } } 'Warning' { if(-not $ForegroundColor){ $ForegroundColor = 'Yellow' } } 'Error' { if(-not $ForegroundColor){ $ForegroundColor = 'Red' } } 'Debug' { if(-not $ForegroundColor){ $ForegroundColor = 'Cyan' } } 'Verbose' { if(-not $ForegroundColor){ $ForegroundColor = 'Magenta' } } default { if(-not $ForegroundColor){ $ForegroundColor = 'White' } } } $now = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $prefix = "[$Severity]" + $parts.SeverityPad $text = "[$now] $prefix$($parts.Indent)$($parts.StepTag) $Message" [pscustomobject]@{ Text = $text ForegroundColor = $ForegroundColor } } # endregion # region Private\Functions\Format-LoggerLineCmtrace.ps1 function Format-LoggerLineCmtrace { param( [Parameter(Mandatory)][string]$Severity, [Parameter(Mandatory)][string]$Component, [Parameter(Mandatory)][string]$Message, [int]$IndentLevel = 0, [bool]$IsLast = $false ) # CMTrace XML-like format expected by cmtrace.exe # <![LOG[Message]LOG]!><time="HH:mm:ss.ffffff" date="M-d-yyyy" component="..." context="user" type="N" thread="id" file=""> # Map Severity to CMTrace numeric type (1=Info, 2=Warning, 3=Error) $type = switch ($Severity) { 'Warning' { '2' } 'Error' { '3' } default { '1' } } $time = Get-Date -Format 'HH:mm:ss.ffffff' $date = Get-Date -Format 'M-d-yyyy' $context = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $thread = [Threading.Thread]::CurrentThread.ManagedThreadId $prefix = '' if ($IndentLevel -gt 1) { for ($i = 1; $i -lt $IndentLevel; $i++) { $prefix += '│ ' } } if ($IndentLevel -gt 0) { $branchGlyph = if ($IsLast) { '└─ ' } else { '├─ ' } $prefix += $branchGlyph } $msgWithIndent = "$prefix$Message" $content = ('<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="{4}" type="{5}" thread="{6}" file="">' -f ` $msgWithIndent, $time, $date, $Component, $context, $type, $thread) return $content } # endregion # region Private\Functions\Format-LoggerLineDefault.ps1 function Format-LoggerLineDefault { param( [Parameter(Mandatory)][string]$Timestamp, [Parameter(Mandatory)][string]$Severity, [Parameter(Mandatory)][string]$Component, [Parameter(Mandatory)][string]$Message, [int]$IndentLevel = 0 ) # Use shared prefix helper to keep width/indent consistent with console $parts = Get-LoggerPrefix -Severity $Severity -IndentLevel $IndentLevel $sevPadded = $Severity.PadLeft($parts.FieldWidth) $indent = $parts.Indent return "[$Timestamp] [$sevPadded]$indent[$Component] $Message" } # endregion # region Private\Functions\Get-LoggerPrefix.ps1 function Get-LoggerPrefix { param( [Parameter(Mandatory)][ValidateSet('Info','Success','Warning','Error','Debug','Verbose')][string]$Severity, [int]$IndentLevel = 0, [string]$StepName = '' ) # Pad severity to a consistent visual width (inside or after brackets depending on consumer) $fieldWidth = 10 $padCount = [Math]::Max(0, $fieldWidth - $Severity.Length) $severityPad = ' ' * $padCount $indent = if ($IndentLevel -gt 0) { ' ' * ($IndentLevel * 2) } else { '' } $stepTag = if ($StepName) { "[$StepName]" } else { '' } [pscustomobject]@{ SeverityPad = $severityPad Indent = $indent StepTag = $stepTag FieldWidth = $fieldWidth } } # endregion # region Private\Functions\Invoke-FileSink.ps1 function Invoke-FileSink { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Options, [Parameter(Mandatory)][string]$Component, [Parameter(Mandatory)][string]$Message, [Parameter(Mandatory)][ValidateSet('Info','Success','Warning','Error','Debug','Verbose')][string]$Severity, [int]$IndentLevel = 0 ) $path = $Options.Path if (-not $path) { return } $onError = ($Options.OnError | ForEach-Object { $_ }) -as [string] if (-not $onError) { $onError = 'Warn' } if ($onError -notin @('Warn','Continue','Throw')) { $onError = 'Warn' } $format = ($Options.Format | ForEach-Object { $_ }) -as [string]; if (-not $format) { $format = 'Default' } $rotation = ($Options.Rotation | ForEach-Object { $_ }) -as [string] $maxSizeMB = [double]($(if ($Options.ContainsKey('MaxSizeMB')) { $Options.MaxSizeMB } else { 5 })) $maxRolls = [int] ($(if ($Options.ContainsKey('MaxRolls')) { $Options.MaxRolls } else { 3 })) $encoding = $(if ($Options.ContainsKey('Encoding')) { $Options.Encoding } else { 'UTF8BOM' }) $effectivePath = $path if ($rotation -match 'Daily') { $dir = Split-Path -Parent $path $leaf = Split-Path -Leaf $path $date = Get-Date -Format 'yyyy-MM-dd' if ($leaf -match '\.log$') { $base = [System.IO.Path]::GetFileNameWithoutExtension($leaf) $ext = [System.IO.Path]::GetExtension($leaf) $effectivePath = Join-Path $dir ("{0}.{1}{2}" -f $base,$date,$ext) } else { $effectivePath = Join-Path $dir ("{0}.{1}" -f $leaf,$date) } } $dirToEnsure = Split-Path -Parent $effectivePath if ($dirToEnsure -and -not (Test-Path -LiteralPath $dirToEnsure)) { try { New-Item -ItemType Directory -Path $dirToEnsure -Force -ErrorAction Stop | Out-Null } catch { Invoke-LoggerSinkError -Sink 'File' -Action 'creating target directory' -ErrorRecord $_ -Policy $onError return } } # Rotation mode: NewFile -> archive existing file once per session before first write if ($rotation -match 'NewFile') { if (-not $script:FileSinkInitialized) { $script:FileSinkInitialized = @{} } $initKey = "newfile::" + $effectivePath if (-not $script:FileSinkInitialized.ContainsKey($initKey)) { try { if (Test-Path -LiteralPath $effectivePath) { $dir = Split-Path -Parent $effectivePath $base = Split-Path -Leaf $effectivePath for ($i = $maxRolls; $i -ge 1; $i--) { $dstLeaf = "$base.$i" $dst = Join-Path $dir $dstLeaf if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force -ErrorAction SilentlyContinue } if ($i -eq 1) { if (Test-Path -LiteralPath $effectivePath) { Rename-Item -LiteralPath $effectivePath -NewName $dstLeaf -Force -ErrorAction SilentlyContinue } } else { $srcLeaf = "$base.$($i-1)" $src = Join-Path $dir $srcLeaf if (Test-Path -LiteralPath $src) { Rename-Item -LiteralPath $src -NewName $dstLeaf -Force -ErrorAction SilentlyContinue } } } } } catch { Invoke-LoggerSinkError -Sink 'File' -Action 'rotation NewFile' -ErrorRecord $_ -Policy $onError } $script:FileSinkInitialized[$initKey] = $true } } if ($rotation -match 'Size') { try { if (Test-Path -LiteralPath $effectivePath) { $lenBytes = (Get-Item -LiteralPath $effectivePath).Length $lenMB = $lenBytes / 1MB if ($lenMB -ge $maxSizeMB) { $dir = Split-Path -Parent $effectivePath $base = Split-Path -Leaf $effectivePath for ($i = $maxRolls; $i -ge 1; $i--) { $dstLeaf = "$base.$i" $dst = Join-Path $dir $dstLeaf if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force -ErrorAction SilentlyContinue } if ($i -eq 1) { if (Test-Path -LiteralPath $effectivePath) { Rename-Item -LiteralPath $effectivePath -NewName $dstLeaf -Force -ErrorAction SilentlyContinue } } else { $srcLeaf = "$base.$($i-1)" $src = Join-Path $dir $srcLeaf if (Test-Path -LiteralPath $src) { Rename-Item -LiteralPath $src -NewName $dstLeaf -Force -ErrorAction SilentlyContinue } } } } } } catch { Invoke-LoggerSinkError -Sink 'File' -Action 'rotation Size' -ErrorRecord $_ -Policy $onError } } $now = Get-Date switch -Regex ($format) { '^cmtrace$' { $line = Format-LoggerLineCmtrace -Severity $Severity -Component $Component -Message $Message -IndentLevel $IndentLevel } default { $ts = $now.ToString('yyyy-MM-dd HH:mm:ss') $line = Format-LoggerLineDefault -Timestamp $ts -Severity $Severity -Component $Component -Message $Message -IndentLevel $IndentLevel } } try { $encObj = switch -Regex ($encoding) { '^UTF8$' { New-Object System.Text.UTF8Encoding($false); break } '^UTF8BOM$' { New-Object System.Text.UTF8Encoding($true); break } '^ASCII$' { [System.Text.Encoding]::ASCII; break } '^Unicode$' { [System.Text.Encoding]::Unicode; break } '^UTF7$' { [System.Text.Encoding]::UTF7; break } '^UTF32$' { [System.Text.Encoding]::UTF32; break } '^Default$' { [System.Text.Encoding]::Default; break } '^OEM$' { [System.Text.Encoding]::GetEncoding([System.Console]::OutputEncoding.CodePage); break } default { New-Object System.Text.UTF8Encoding($false) } } if (-not $script:FileSinkInitialized) { $script:FileSinkInitialized = @{} } $firstWrite = $false if (-not $rotation) { if (-not $script:FileSinkInitialized.ContainsKey($effectivePath)) { $script:FileSinkInitialized[$effectivePath] = $true $firstWrite = $true } } if ($firstWrite) { [System.IO.File]::WriteAllText($effectivePath, $line + [Environment]::NewLine, $encObj) } else { [System.IO.File]::AppendAllText($effectivePath, $line + [Environment]::NewLine, $encObj) } } catch { Invoke-LoggerSinkError -Sink 'File' -Action 'writing log line' -ErrorRecord $_ -Policy $onError } } # endregion # region Private\Functions\Invoke-LoggerSinkError.ps1 function Invoke-LoggerSinkError { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Sink, [Parameter(Mandatory)][string]$Action, [Parameter(Mandatory)][System.Management.Automation.ErrorRecord]$ErrorRecord, [ValidateSet('Warn','Continue','Throw')][string]$Policy = 'Warn' ) $message = "ITFabrik.Logger sink '$Sink' failed during ${Action}: $($ErrorRecord.Exception.Message)" switch ($Policy) { 'Throw' { throw $ErrorRecord } 'Warn' { Write-Warning $message } 'Continue' { } } } # endregion # region Private\Functions\Invoke-SMConsoleLogger.ps1 function Invoke-SMConsoleLogger { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Component', Justification = 'Keep ITFabrik.Stepper-compatible signature (legacy StepManagerLogger contract).')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Console sink intentionally writes to host.')] param( [Parameter(Mandatory)] [string]$Component, [Parameter(Mandatory)] [string]$Message, [Parameter(Mandatory)] [ValidateSet('Info','Success','Warning','Error','Debug','Verbose')] [string]$Severity, [int]$IndentLevel = 0, [string]$StepName = '', [string]$ForegroundColor ) $obj = Format-ConsoleMessage -Component $Component -Message $Message -Severity $Severity -IndentLevel $IndentLevel -StepName $StepName -ForegroundColor $ForegroundColor Write-Host $obj.Text -ForegroundColor $obj.ForegroundColor } # endregion # region Private\Functions\Invoke-WebSink.ps1 function Invoke-WebSink { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Options, [Parameter(Mandatory)][string]$Component, [Parameter(Mandatory)][string]$Message, [Parameter(Mandatory)][ValidateSet('Info','Success','Warning','Error','Debug','Verbose')][string]$Severity, [int]$IndentLevel = 0 ) $url = $Options.Url if (-not $url) { return } $onError = ($Options.OnError | ForEach-Object { $_ }) -as [string] if (-not $onError) { $onError = 'Warn' } if ($onError -notin @('Warn','Continue','Throw')) { $onError = 'Warn' } $apiKey = $Options.APIKey $headers = @{} if ($apiKey) { $headers['X-API-Key'] = $apiKey } if ($Options.Headers -is [hashtable]) { $Options.Headers.GetEnumerator() | ForEach-Object { $headers[$_.Key] = $_.Value } } $payload = [ordered]@{ timestamp = (Get-Date).ToString('o') component = $Component message = $Message severity = $Severity indent = $IndentLevel host = $env:COMPUTERNAME processId = $PID } try { Invoke-RestMethod -Method Post -Uri $url -Headers $headers -Body ($payload | ConvertTo-Json -Depth 5) -ContentType 'application/json' -ErrorAction Stop | Out-Null } catch { Invoke-LoggerSinkError -Sink 'Web' -Action 'posting HTTP payload' -ErrorRecord $_ -Policy $onError } } # endregion # region Public\Disable-Logger.ps1 function Disable-Logger { [CmdletBinding()] param() Remove-Variable -Name StepManagerLogger -Scope Global -ErrorAction SilentlyContinue } # endregion # region Public\Initialize-LoggerConsole.ps1 function Initialize-LoggerConsole { [CmdletBinding()] param() Initialize-LoggerService -Reset Register-LoggerSink -Type Console } # endregion # region Public\Initialize-LoggerFile.ps1 function Initialize-LoggerFile { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [ValidateSet('Size','Daily','NewFile')] [string]$Rotation, [ValidateRange(0.001, 1024)] [double]$MaxSizeMB = 5, [ValidateRange(1, 100)] [int]$MaxRolls = 3, [ValidateSet('UTF8','UTF8BOM','ASCII','Unicode','UTF7','UTF32','Default','OEM')] [string]$Encoding = 'UTF8BOM', [ValidateSet('Warn','Continue','Throw')] [string]$OnError = 'Warn' ) Initialize-LoggerService -Reset $sinkArgs = @{ Type = 'File' Path = $Path FileFormat = 'Default' MaxSizeMB = $MaxSizeMB MaxRolls = $MaxRolls Encoding = $Encoding OnError = $OnError } if ($PSBoundParameters.ContainsKey('Rotation')) { $sinkArgs.Rotation = $Rotation } Register-LoggerSink @sinkArgs } # endregion # region Public\Initialize-LoggerService.ps1 function Initialize-LoggerService { [CmdletBinding()] param( [switch]$Reset, [scriptblock]$Action ) if ($Reset) { [LoggerService]::Reset() | Out-Null } $svc = [LoggerService]::GetInstance() $svc.ConfigureService() if ($Action) { & $Action -ArgumentList $svc } } # endregion # region Public\Register-LoggerSink.ps1 function Register-LoggerSink { [CmdletBinding(DefaultParameterSetName='Console')] param( [Parameter(Mandatory)][ValidateSet('Console','File','Web')][string]$Type, # Console [Parameter(ParameterSetName='Console')][ValidateSet('Default')][string]$Format = 'Default', # File [Parameter(Mandatory, ParameterSetName='File')][string]$Path, [Parameter(ParameterSetName='File')][ValidateSet('Default','Cmtrace')][string]$FileFormat = 'Default', [Parameter(ParameterSetName='File')][ValidateSet('Size','Daily','NewFile')][string]$Rotation, [Parameter(ParameterSetName='File')][ValidateRange(0.001,1024)][double]$MaxSizeMB = 5, [Parameter(ParameterSetName='File')][ValidateRange(1,100)][int]$MaxRolls = 3, [Parameter(ParameterSetName='File')][ValidateSet('UTF8','UTF8BOM','ASCII','Unicode','UTF7','UTF32','Default','OEM')][string]$Encoding = 'UTF8BOM', [Parameter(ParameterSetName='File')] [Parameter(ParameterSetName='Web')] [ValidateSet('Warn','Continue','Throw')][string]$OnError = 'Warn', # Web [Parameter(Mandatory, ParameterSetName='Web')][string]$Url, [Parameter(ParameterSetName='Web')][string]$APIKey, [Parameter(ParameterSetName='Web')][hashtable]$Headers ) $svc = [LoggerService]::GetInstance() switch ($Type) { 'Console' { $svc.RegisterSink('Console', @{ Format = $Format }) } 'File' { $svc.RegisterSink('File', @{ Path = $Path; Format = $FileFormat; Rotation = $Rotation; MaxSizeMB = $MaxSizeMB; MaxRolls = $MaxRolls; Encoding = $Encoding; OnError = $OnError }) } 'Web' { $svc.RegisterSink('Web', @{ Url = $Url; APIKey = $APIKey; Headers = $Headers; OnError = $OnError }) } } } # endregion # Exports Export-ModuleMember -Function Initialize-LoggerService,Register-LoggerSink,Initialize-LoggerConsole,Initialize-LoggerFile,Disable-Logger |