Public/cliOut/Write-cLog.ps1
function Write-cLog { #.SYNOPSIS # Emits a log record # .DESCRIPTION # This function write a log record to configured targets with the matching level # .PARAMETER Level # The log level of the message. Valid values are DEBUG, INFO, WARNING, ERROR, NOTSET # Other custom levels can be added and are a valid value for the parameter # INFO is the default # .PARAMETER Message # The text message to write # .PARAMETER Arguments # An array of objects used to format <Message> # .PARAMETER Body # An object that can contain additional log metadata (used in target like ElasticSearch) # .PARAMETER ExceptionInfo # An optional ErrorRecord # .EXAMPLE # PS C:\> Write-cLog 'Hello, World!' # .EXAMPLE # PS C:\> Write-cLog -Level ERROR -Message 'Hello, World!' # .EXAMPLE # PS C:\> Write-cLog -Level ERROR -Message 'Hello, {0}!' -Arguments 'World' [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'Invalid rule result')] param( [Parameter(Position = 2, Mandatory = $true)] [string]$Message, [Parameter(Position = 3, Mandatory = $false)] [array]$Arguments, [Parameter(Position = 4, Mandatory = $false)] [object]$Body = $null, [Parameter(Position = 5, Mandatory = $false)] [System.Management.Automation.ErrorRecord]$ExceptionInfo = $null ) begin { #region Set LoggingVariables #Already setup if ($Script:Logging -and $Script:LevelNames) { return } Out-Verbose 'Setting up vars' $Script:NOTSET = 0 $Script:DEBUG = 10 $Script:INFO = 20 $Script:WARNING = 30 $Script:ERROR_ = 40 New-Variable -Name LevelNames -Scope Script -Option ReadOnly -Value ([hashtable]::Synchronized(@{ $NOTSET = 'NOTSET' $ERROR_ = 'ERROR' $WARNING = 'WARNING' $INFO = 'INFO' $DEBUG = 'DEBUG' 'NOTSET' = $NOTSET 'ERROR' = $ERROR_ 'WARNING' = $WARNING 'INFO' = $INFO 'DEBUG' = $DEBUG } ) ) New-Variable -Name ScriptRoot -Scope Script -Option ReadOnly -Value ([System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Module.Path)) New-Variable -Name Defaults -Scope Script -Option ReadOnly -Value @{ Level = $LevelNames[$LevelNames['NOTSET']] LevelNo = $LevelNames['NOTSET'] Format = '[%{timestamp:+%Y-%m-%d %T%Z}] [%{level:-7}] %{message}' Timestamp = '%Y-%m-%d %T%Z' CallerScope = 1 } New-Variable -Name Logging -Scope Script -Option ReadOnly -Value ([hashtable]::Synchronized(@{ Level = $Defaults.Level LevelNo = $Defaults.LevelNo Format = $Defaults.Format CallerScope = $Defaults.CallerScope CustomTargets = [String]::Empty Targets = ([System.Collections.Concurrent.ConcurrentDictionary[string, hashtable]]::new([System.StringComparer]::OrdinalIgnoreCase)) EnabledTargets = ([System.Collections.Concurrent.ConcurrentDictionary[string, hashtable]]::new([System.StringComparer]::OrdinalIgnoreCase)) } ) ) #endregion } DynamicParam { $DynamicParams = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $attribute = [System.Management.Automation.ParameterAttribute]::new() $attribute.ParameterSetName = '__AllParameterSets' $attribute.Mandatory = $Mandatory $attribute.Position = 1 $attributeCollection.Add($attribute) [String[]]$allowedValues = @() switch ($PSCmdlet.ParameterSetName) { "DynamicTarget" { $allowedValues += $Script:Logging.Targets.Keys } "DynamicLevel" { $l = $Script:LevelNames[[int]$Level] $allowedValues += if ($l) { $l } else { $('Level {0}' -f [int]$Level) } } } try { $validateSetAttribute = [System.Management.Automation.ValidateSetAttribute]::new($allowedValues) } catch [System.Management.Automation.PSArgumentOutOfRangeException] { Write-Error "`$allowedValues is out of range`n[`$allowedValues = $allowedValues]" break } $attributeCollection.Add($validateSetAttribute) $dynamicParam = [System.Management.Automation.RuntimeDefinedParameter]::new("Level", [string], $attributeCollection) $DynamicParams.Add("Level", $dynamicParam) $DynamicParams $PsCmdlet.MyInvocation.BoundParameters.GetEnumerator() | ForEach-Object { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue' } } End { $PsCmdlet.MyInvocation.BoundParameters.GetEnumerator() | ForEach-Object { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue' } $levelNumber = if ($Level -is [int] -and $Level -in $Script:LevelNames.Keys) { $Level }elseif ([string]$Level -eq $Level -and $Level -in $Script:LevelNames.Keys) { $Script:LevelNames[$Level] }else { throw ('Level not a valid integer or a valid string: {0}' -f $Level) } $invocationInfo = $(Get-PSCallStack)[$Script:Logging.CallerScope] # Split-Path throws an exception if called with a -Path that is null or empty. [string] $fileName = [string]::Empty if (![string]::IsNullOrEmpty($invocationInfo.ScriptName)) { $fileName = Split-Path -Path $invocationInfo.ScriptName -Leaf } $logMessage = [hashtable] @{ timestamp = [datetime]::now timestamputc = [datetime]::UtcNow level = & { $l = $Script:LevelNames[$levelNumber]; if ($l) { $l } else { $('Level {0}' -f $levelNumber) } } levelno = $levelNumber lineno = $invocationInfo.ScriptLineNumber pathname = $invocationInfo.ScriptName filename = $fileName caller = $invocationInfo.Command message = [string] $Message rawmessage = [string] $Message body = $Body execinfo = $ExceptionInfo pid = $PID } if ($PSBoundParameters.ContainsKey('Arguments')) { $logMessage["message"] = [string] $Message -f $Arguments $logMessage["args"] = $Arguments } #This variable is initiated via Start-LoggingManager if (!$Script:LoggingEventQueue) { New-Variable -Name LoggingEventQueue -Scope Script -Value ([System.Collections.Concurrent.BlockingCollection[hashtable]]::new(100)) } $Script:LoggingEventQueue.Add($logMessage) } } |