PSJobLogger.psm1
using namespace System.Collections using namespace System.Collections.Concurrent using namespace System.Collections.Generic using namespace System.IO $setVariableOpts = @{ Option = 'Constant' Scope = 'Global' ErrorAction = 'SilentlyContinue' } Set-Variable PSJobLoggerStreamSuccess @setVariableOpts -Value ([int]0) Set-Variable PSJobLoggerStreamError @setVariableOpts -Value ([int]1) Set-Variable PSJobLoggerStreamWarning @setVariableOpts -Value ([int]2) Set-Variable PSJobLoggerStreamVerbose @setVariableOpts -Value ([int]3) Set-Variable PSJobLoggerStreamDebug @setVariableOpts -Value ([int]4) Set-Variable PSJobLoggerStreamInformation @setVariableOpts -Value ([int]5) Set-Variable PSJobLoggerStreamProgress @setVariableOpts -Value ([int]6) Set-Variable PSJobLoggerStreamHost @setVariableOpts -Value ([int]7) Set-Variable PSJobLoggerLogStreams @setVariableOpts -Value @{ $PSJobLoggerStreamSuccess = 'Success' $PSJobLoggerStreamError = 'Error' $PSJobLoggerStreamWarning = 'Warning' $PSJobLoggerStreamVerbose = 'Verbose' $PSJobLoggerStreamDebug = 'Debug' $PSJobLoggerStreamInformation = 'Information' $PSJobLoggerStreamHost = 'Host' $PSJobLoggerStreamProgress = 'Progress' } Set-Variable PSJobLoggerPlainTextLogStreams @setVariableOpts -Value @{ $PSJobLoggerStreamSuccess = 'Success' $PSJobLoggerStreamError = 'Error' $PSJobLoggerStreamWarning = 'Warning' $PSJobLoggerStreamVerbose = 'Verbose' $PSJobLoggerStreamDebug = 'Debug' $PSJobLoggerStreamInformation = 'Information' $PSJobLoggerStreamHost = 'Host' } class PSJobLogger { <# Constant values used as an output stream identifier #> static [int]$StreamSuccess = 0 static [int]$StreamError = 1 static [int]$StreamWarning = 2 static [int]$StreamVerbose = 3 static [int]$StreamDebug = 4 static [int]$StreamInformation = 5 static [int]$StreamProgress = 6 static [int]$StreamHost = 7 <# A list of all available output streams #> static [Hashtable]$LogStreams = @{ [PSJobLogger]::StreamSuccess = 'Success' [PSJobLogger]::StreamError = 'Error' [PSJobLogger]::StreamWarning = 'Warning' [PSJobLogger]::StreamVerbose = 'Verbose' [PSJobLogger]::StreamDebug = 'Debug' [PSJobLogger]::StreamInformation = 'Information' [PSJobLogger]::StreamHost = 'Host' [PSJobLogger]::StreamProgress = 'Progress' } <# A list of plain text output streams #> static [Hashtable]$PlainTextLogStreams = @{ [PSJobLogger]::StreamSuccess = 'Success' [PSJobLogger]::StreamError = 'Error' [PSJobLogger]::StreamWarning = 'Warning' [PSJobLogger]::StreamVerbose = 'Verbose' [PSJobLogger]::StreamDebug = 'Debug' [PSJobLogger]::StreamInformation = 'Information' [PSJobLogger]::StreamHost = 'Host' } # The name of the logger; used to construct a "prefix" that is prepended to each message [String]$Name = '' # A thread-safe dictionary that holds thread-safe collections for each output stream [ConcurrentDictionary[int, ICollection]]$Streams # The file in which to additionally log all messages [String]$Logfile = '' # Indicates that a log file has been defined [Boolean]$ShouldLogToFile = $false # Indicates that message queues should be used [Boolean]$UseQueues = $false # Contains the Id of the parent Progress bar [int]$ProgressParentId = -1 # Contains the desired value of DebugPreference when invoking Write-Debug [String]$DebugPref = 'SilentlyContinue' # Contains the desired value of VerbosePreference when invoking Write-Verbose [String]$VerbosePref = 'SilentlyContinue' PSJobLogger( [String]$Name = 'PSJobLogger', [String]$Logfile = '', [Switch]$UseQueues = $false, [int]$ProgressParentId = -1 ) { $this.Name = $Name if ($Name -eq '') { $this.Name = 'PSJobLogger' } if ($Logfile -ne '') { $this.SetLogfile($Logfile) } $this.UseQueues = $UseQueues $this.ProgressParentId = $ProgressParentId $this.Streams = [ConcurrentDictionary[int, ICollection]]::new() foreach ($stream in [PSJobLogger]::LogStreams.Keys) { switch ($stream) { ([PSJobLogger]::StreamProgress) { if (-not($this.Streams.TryAdd($stream, [ConcurrentDictionary[String, ConcurrentDictionary[String, PSObject]]]::new()))) { Write-Error "unable to add progress stream to stream dict" } } default { if ($this.UseQueues) { if (-not($this.Streams.TryAdd($stream, [ConcurrentQueue[String]]::new()))) { Write-Error "unable to add stream ${stream} to stream dict" } } } } } } [void] SetStreamsFromDictLogger([ConcurrentDictionary[String, PSObject]]$DictLogger) { if ($null -ne $DictLogger -and $DictLogger.ContainsKey('Streams')) { $this.Streams = $DictLogger.Streams } } [void] SetLogfile([String]$Logfile) { if ($Logfile -ne '' -and -not(Test-Path $Logfile)) { $null = New-Item $Logfile -ItemType File -Force -ErrorAction 'SilentlyContinue' if ($Error[0]) { $logfileError = $Error[0] Write-Error "Unable to create log file ${Logfile}: ${logfileError}" return } } $this.Logfile = $Logfile $this.ShouldLogToFile = $this.Logfile -ne '' } [void] LogToFile([int]$Stream, [String]$Message) { if ($this.ShouldLogToFile) { Add-Content -Path $this.Logfile -Value $this.FormatLogfileMessage($Stream, $Message) -ErrorAction 'Continue' } } [void] Output([String]$Message) { $this.EnqueueMessage([PSJobLogger]::StreamSuccess, $Message) } [void] Error([String]$Message) { $this.EnqueueMessage([PSJobLogger]::StreamError, $Message) } [void] Warning([String]$Message) { $this.EnqueueMessage([PSJobLogger]::StreamWarning, $Message) } [void] Verbose([String]$Message) { $this.EnqueueMessage([PSJobLogger]::StreamVerbose, $Message) } [void] Debug([String]$Message) { $this.EnqueueMessage([PSJobLogger]::StreamDebug, $Message) } [void] Information([String]$Message) { $this.EnqueueMessage([PSJobLogger]::StreamInformation, $Message) } [void] Host([String]$Message) { $this.EnqueueMessage([PSJobLogger]::StreamHost, $Message) } [String] FormatLogfileMessage([int]$Stream, [String]$Message) { return "$(Get-Date -Format FileDateUniversal -ErrorAction SilentlyContinue)", "[$($this.Name)]", "($([PSJobLogger]::LogStreams.$Stream))", $Message -join ' ' } [void] EnqueueMessage([int]$Stream, [String]$Message) { # Log the message to a logfile if one is defined $this.LogToFile($Stream, $Message) # Add the message to the desired queue if queues are enabled if ($this.UseQueues) { $this.Streams.$Stream.Enqueue($Message) return } # Write the message to the appropriate stream $this.FlushMessages($Stream, @($Message)) } [void] Progress([String]$Id, [Hashtable]$ArgumentMap) { if ($null -eq $ArgumentMap -or $ArgumentMap.Count -eq 0) { return } [ConcurrentDictionary[String, ConcurrentDictionary[String, PSObject]]]$progressTable = $this.Streams.$([PSJobLogger]::StreamProgress) if (-not($progressTable.ContainsKey($Id))) { if (-not($progressTable.TryAdd($Id, [ConcurrentDictionary[String, PSObject]]::new()))) { Write-Error "unable to add new key for ${Id}" } } [ConcurrentDictionary[String, PSObject]]$progressArgs = $progressTable.$Id foreach ($key in $ArgumentMap.Keys) { if ($null -eq $ArgumentMap.$key) { [PSObject]$removedValue = $null if (-not($progressArgs.TryRemove($key, [ref]$removedValue))) { Write-Error "could not remove key ${key} from progress arg map" } continue } $progressArgs.$key = $ArgumentMap.$key } if ($this.ProgressParentId -ge 0) { if (-not($progressArgs.ContainsKey('ParentId'))) { $null = $progressArgs.TryAdd('ParentId', $this.ProgressParentId) } else { $progressArgs.ParentId = $this.ProgressParentId } } } [void] FlushStreams() { foreach ($stream in [PSJobLogger]::LogStreams.Keys) { $this.FlushOneStream($stream) } } [void] FlushPlainTextStreams() { foreach ($stream in [PSJobLogger]::PlainTextLogStreams.Keys) { $this.FlushOneStream($stream) } } [void] FlushProgressStream() { [ConcurrentDictionary[String, ConcurrentDictionary[String, PSObject]]]$progressQueue = $this.Streams.$([PSJobLogger]::StreamProgress) # write progress records foreach ($recordKey in $progressQueue.Keys) { if ($null -eq $progressQueue.$recordKey) { Write-Warning "FlushProgressStream(): no queue record for ${recordKey}; skipping it" continue } [ConcurrentDictionary[String, PSObject]]$progressArgs = $progressQueue.$recordKey if ($null -ne $progressArgs.Id -and $null -ne $progressArgs.Activity -and $progressArgs.Activity -ne '') { Write-Progress @progressArgs -ErrorAction 'Continue' } # If the arguments included `Completed = $true`, remove the key from the progress stream dictionary if ($progressArgs.GetOrAdd('Completed', $false)) { if (-not($progressQueue.TryRemove($recordKey, [ref]@{}))) { Write-Error "FlushProgressStream(): failed to remove progress stream record ${recordKey}" } } } } [void] FlushOneStream([int]$Stream) { # The Progress stream is handled elsewhere since it contains a different type of data if ($Stream -eq [PSJobLogger]::StreamProgress) { $this.FlushProgressStream() return } # If we're not using queues then there's nothing to flush if (-not($this.UseQueues)) { return } # Drain the queue for the stream [String[]]$messages = @() [ConcurrentQueue[String]]$messageQueue = $this.Streams.$Stream $dequeuedMessage = '' while ($messageQueue.Count -gt 0) { if (-not($messageQueue.TryDequeue([ref]$dequeuedMessage))) { Write-Error "FlushOneStream(): unable to dequeue message from $([PSJobLogger]::LogStreams.$Stream); queue count = $($messageQueue.Count)" break } $messages += $dequeuedMessage } # write messages to the desired stream $this.FlushMessages($Stream, $messages) } [void] FlushMessages([int]$Stream, [String[]]$Messages) { foreach ($message in $Messages) { $formattedMessage = $this.FormatLogfileMessage($Stream, $message) switch ($Stream) { ([PSJobLogger]::StreamSuccess) { Write-Output $formattedMessage -ErrorAction 'Continue' } ([PSJobLogger]::StreamError) { Write-Error -Message $formattedMessage } ([PSJobLogger]::StreamWarning) { Write-Warning -Message $formattedMessage -ErrorAction 'Continue' } ([PSJobLogger]::StreamVerbose) { $VerbosePreference = $this.VerbosePref Write-Verbose -Message $formattedMessage -ErrorAction 'Continue' -Verbose } ([PSJobLogger]::StreamDebug) { $DebugPreference = $this.DebugPref Write-Debug -Message $formattedMessage -ErrorAction 'Continue' -Debug } ([PSJobLogger]::StreamInformation) { Write-Information -MessageData $formattedMessage -ErrorAction 'Continue' } ([PSJobLogger]::StreamHost) { Write-Host $formattedMessage -ErrorAction 'Continue' } ([PSJobLogger]::StreamProgress) { # This should never be reached, but it's here just in case. Write-Error "reached StreamProgress in FlushMessages; this is unexpected. message: ${formattedMessage}" } default { Write-Error "FlushMessages(): unexpected stream ${Stream}; message: ${formattedMessage}" } } } } [ConcurrentDictionary[String,PSObject]] asDictLogger() { $dictLogger = [ConcurrentDictionary[String, PSObject]]::new() $dictElements = @{ Name = $this.Name Logfile = $this.Logfile ShouldLogToFile = $this.ShouldLogToFile UseQueues = $this.UseQueues ProgressParentId = $this.ProgressParentId Streams = $this.Streams DebugPref = $this.DebugPref VerbosePref = $this.VerbosePref } foreach ($key in $dictElements.Keys) { if (-not($dictLogger.TryAdd($key, $dictElements.$key))) { Write-Error "unable to add key ${key} to dict" } } return $dictLogger } } <# .SYNOPSIS Return a newly-initialized PSJobLogger class .PARAMETER Name The name of the logger; defaults to 'PSJobLogger' .PARAMETER Logfile The path and name of a file in which to write log messages (optional) .PARAMETER UseQueues Indicates that messages should be added to queues for each output stream; defaults to $false (optional) .PARAMETER ProgressParentId The Id of the parent progress bar; defaults to -1 (optional) .EXAMPLE PS> $jobLog = Initialize-PSJobLogger -Name MyLogger -Logfile messages.log -ParentProgressId 0 #> function Initialize-PSJobLogger { [CmdletBinding()] [OutputType([PSJobLogger])] param( [String]$Name = 'PSJobLogger', [String]$Logfile = '', [Switch]$UseQueues, [int]$ProgressParentId = -1 ) $jobLogger = [PSJobLogger]::new($Name, $Logfile, $UseQueues, $ProgressParentId) $jobLogger.DebugPref = $DebugPreference $jobLogger.VerbosePref = $VerbosePreference return $jobLogger } function ConvertFrom-DictLogger { [CmdletBinding()] [OutputType([PSJobLogger])] param( [Parameter(Mandatory)][ConcurrentDictionary[String, PSObject]]$DictLogger ) if ($null -eq $DictLogger) { throw 'cannot convert from a null DictLogger' } # Get initialization parameters from the DictLogger $name = '' $null = $DictLogger.TryGetValue('Name', [ref]$name) $logfile = '' $null = $DictLogger.TryGetValue('Logfile', [ref]$logfile) $useQueues = $false $null = $DictLogger.TryGetValue('UseQueues', [ref]$useQueues) $progressParentId = -1 $null = $DictLogger.TryGetValue('ProgressParentId', [ref]$progressParentId) # Create a new PSJobLogger $jobLog = [PSJobLogger]::new($name, $logfile, $useQueues, $progressParentId) # Set preferences $debugPref = $DebugPreference $null = $DictLogger.TryGetValue('DebugPref', [ref]$debugPref) $jobLog.DebugPref = $debugPref $verbosePref = $VerbosePreference $null = $DictLogger.TryGetValue('VerbosePref', [ref]$verbosePref) $jobLog.VerbosePref = $verbosePref # Set the message tables to the Streams from the DictLogger $jobLog.SetStreamsFromDictLogger($DictLogger) return $jobLog } |