cliHelper.logger.psm1
#!/usr/bin/env pwsh using namespace System.IO using namespace System.Text using namespace System.Threading using namespace System.Collections.Generic enum LogEventType { Debug Information Warning Error Fatal } class ILoggerAppender { [void] Log([ILoggerEntry]$entry) { } } class ILoggerEntry { [LogEventType]$Severity [string]$Message [Exception]$Exception [datetime]$Timestamp = [datetime]::UtcNow static [ILoggerEntry] Yield([string]$message) { throw "Not implemented" } } class LoggerEntry : ILoggerEntry { static [ILoggerEntry] Yield([string]$message) { $caller = (Get-PSCallStack)[2].Command $severity = [LogEventType]::$caller return [LoggerEntry]@{ Severity = $severity Message = $message Timestamp = [datetime]::UtcNow } } } class BaseLogger : IDisposable { [List[ILoggerAppender]]$Appenders = [List[ILoggerAppender]]::new() [Type]$EntryType = [LoggerEntry] [guid]$SessionId = [guid]::NewGuid() [string]$LogDirectory [StreamWriter]$StreamWriter static [Hashtable]$Sessions = [Hashtable]::Synchronized(@{}) static [string]$DefaultLogDirectory = "$pwd\Logs" BaseLogger() { } BaseLogger([string]$logDirectory) { $this.LogDirectory = $logDirectory $this.Initialize() } [void] Initialize() { if (-not (Test-Path $this.LogDirectory)) { New-Item -Path $this.LogDirectory -ItemType Directory -Force | Out-Null } $logPath = Join-Path $this.LogDirectory "Log_$($this.SessionId).log" $this.StreamWriter = [StreamWriter]::new($logPath) [BaseLogger]::Sessions[$this.SessionId] = $this } [void] Log([LogEventType]$severity, [string]$message, [Exception]$exception) { $entry = $this.CreateEntry($severity, $message, $exception) $this.ProcessEntry($entry) } [ILoggerEntry] CreateEntry([LogEventType]$severity, [string]$message, [Exception]$exception) { return $this.EntryType::Yield($message, $exception) } [void] ProcessEntry([ILoggerEntry]$entry) { $this.StreamWriter.WriteLine("[{0:u}] [{1}] {2}" -f $entry.Timestamp, $entry.Severity, $entry.Message) foreach ($appender in $this.Appenders) { try { $appender.Log($entry) } catch { Write-Error "Appender error: $_" } } } [void] Dispose() { if ($this.StreamWriter) { $this.StreamWriter.Flush() $this.StreamWriter.Dispose() } [BaseLogger]::Sessions.Remove($this.SessionId) } } class Logger : BaseLogger { Logger() : base() { } Logger([string]$logDirectory) : base($logDirectory) { } [void] Debug([string]$message) { $this.Log([LogEventType]::Debug, $message, $null) } [void] Information([string]$message) { $this.Log([LogEventType]::Information, $message, $null) } [void] Warning([string]$message) { $this.Log([LogEventType]::Warning, $message, $null) } [void] Error([string]$message, [Exception]$ex) { $this.Log([LogEventType]::Error, $message, $ex) } [void] Fatal([string]$message, [Exception]$ex) { $this.Log([LogEventType]::Fatal, $message, $ex) } } class ConsoleAppender : ILoggerAppender { static [hashtable]$ColorMap = @{ Debug = [ConsoleColor]::DarkGray Information = [ConsoleColor]::Green Warning = [ConsoleColor]::Yellow Error = [ConsoleColor]::Red Fatal = [ConsoleColor]::Magenta } [void] Log([ILoggerEntry]$entry) { $color = [ConsoleAppender]::ColorMap[$entry.Severity.ToString()] $message = "[{0}] {1}" -f $entry.Severity.ToString().ToUpper(), $entry.Message Write-Host $message -ForegroundColor $color } } class JsonAppender : ILoggerAppender { [string]$FilePath [System.IO.StreamWriter]$Writer JsonAppender([string]$Path) { $this.FilePath = $Path $this.Writer = [System.IO.StreamWriter]::new($Path) } [void] Log([ILoggerEntry]$entry) { $logObject = [ordered]@{ timestamp = $entry.Timestamp.ToString('o') severity = $entry.Severity.ToString() message = $entry.Message exception = if ($entry.Exception) { $entry.Exception.ToString() } } $this.Writer.WriteLine(($logObject | ConvertTo-Json -Compress)) } [void] Dispose() { if ($this.Writer) { $this.Writer.Dispose() } } } class FileAppender : ILoggerAppender { [StreamWriter]$Writer [ReaderWriterLockSlim]$Lock = [ReaderWriterLockSlim]::new() FileAppender([string]$path) { $this.Writer = [StreamWriter]::new($path, [Encoding]::UTF8) } [void] Log([ILoggerEntry]$entry) { $this.Lock.EnterWriteLock() try { $this.Writer.WriteLine("[{0:u}] [{1}] {2}" -f $entry.Timestamp, $entry.Severity, $entry.Message) } finally { $this.Lock.ExitWriteLock() } } [void] Dispose() { $this.Writer.Dispose() $this.Lock.Dispose() } } # Export types and setup accelerators $typestoExport = @( [Logger], [ILoggerEntry], [LogEventType], [ConsoleAppender], [JsonAppender], [FileAppender] ) $TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') foreach ($Type in $typestoExport) { if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) { $Message = @( "Unable to register type accelerator '$($Type.FullName)'" 'Accelerator already exists.' ) -join ' - ' "TypeAcceleratorAlreadyExists $Message" | Write-Debug } } foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Add($Type.FullName, $Type) } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure(); $scripts = @(); $Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue $scripts += $Public foreach ($file in $scripts) { Try { if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue } . "$($file.fullname)" } Catch { Write-Warning "Failed to import function $($file.BaseName): $_" $host.UI.WriteErrorLine($_) } } $Param = @{ Function = $Public.BaseName Cmdlet = '*' Alias = '*' Verbose = $false } Export-ModuleMember @Param |