PSLogs.psm1
|
#Region './prefix.ps1' -1 # The content of this file will be prepended to the top of the psm1 module file. This is useful for custom module setup is needed on import. $ScriptPath = Split-Path $MyInvocation.MyCommand.Path $PSModule = $ExecutionContext.SessionState.Module $PSModuleRoot = $PSModule.ModuleBase #EndRegion './prefix.ps1' 5 #Region './Private/Format-Pattern.ps1' -1 <# .DESCRIPTION Replaces the tokens present in the pattern with the values given inside the source (log) object. .PARAMETER Pattern Parameter The pattern that defines tokens and possible operations onto them. .PARAMETER Source Parameter Log object providing values, if wildcard parameter is not given .PARAMETER Wildcard Parameter If this parameter is given, all tokens are replaced by the wildcard character. .EXAMPLE Format-Pattern -Pattern %{timestamp} -Wildcard #> function Format-Pattern { [CmdletBinding()] [OutputType([String])] param( [AllowEmptyString()] [Parameter(Mandatory)] [string] $Pattern, [object] $Source, [switch] $Wildcard ) [string] $result = $Pattern [regex] $tokenMatcher = '%{(?<token>\w+?)?(?::?\+(?<datefmtU>(?:%[ABCDGHIMRSTUVWXYZabcdeghjklmnprstuwxy].*?)+))?(?::?\+(?<datefmt>(?:.*?)+))?(?::(?<padding>-?\d+))?}' $tokenMatches = @() $tokenMatches += $tokenMatcher.Matches($Pattern) [array]::Reverse($tokenMatches) foreach ($match in $tokenMatches) { $formattedEntry = [string]::Empty $tokenContent = [string]::Empty $token = $match.Groups['token'].value $datefmt = $match.Groups['datefmt'].value $datefmtU = $match.Groups['datefmtU'].value $padding = $match.Groups['padding'].value if ($Wildcard.IsPresent) { $formattedEntry = '*' } else { [hashtable] $dateParam = @{ } if (-not [string]::IsNullOrWhiteSpace($token)) { $tokenContent = $Source.$token $dateParam['Date'] = $tokenContent } if (-not [string]::IsNullOrWhiteSpace($datefmtU)) { $formattedEntry = Get-Date @dateParam -UFormat $datefmtU } elseif (-not [string]::IsNullOrWhiteSpace($datefmt)) { $formattedEntry = Get-Date @dateParam -Format $datefmt } else { $formattedEntry = $tokenContent } if ($padding) { $formattedEntry = "{0,$padding}" -f $formattedEntry } } $result = $result.Substring(0, $match.Index) + $formattedEntry + $result.Substring($match.Index + $match.Length) } return $result } #EndRegion './Private/Format-Pattern.ps1' 85 #Region './Private/Get-LevelName.ps1' -1 function Get-LevelName { [CmdletBinding()] param( [int] $Level ) $l = $Script:LevelNames[$Level] if ($l) { return $l } else { return ('Level {0}' -f $Level) } } #EndRegion './Private/Get-LevelName.ps1' 18 #Region './Private/Get-LevelNumber.ps1' -1 function Get-LevelNumber { [CmdletBinding()] param( $Level ) if ($Level -is [int] -and $Level -in $Script:LevelNames.Keys) { return $Level } elseif ([string] $Level -eq $Level -and $Level -in $Script:LevelNames.Keys) { return $Script:LevelNames[$Level] } else { throw ('Level not a valid integer or a valid string: {0}' -f $Level) } } #EndRegion './Private/Get-LevelNumber.ps1' 20 #Region './Private/Get-LevelsName.ps1' -1 Function Get-LevelsName { [CmdletBinding()] param() return $Script:LevelNames.Keys | Where-Object {$_ -isnot [int]} | Sort-Object } #EndRegion './Private/Get-LevelsName.ps1' 7 #Region './Private/Initialize-LoggingTarget.ps1' -1 function Initialize-LoggingTarget { param() $targets = @() $targets += Get-ChildItem "$ScriptRoot\include" -Filter '*.ps1' if ((![String]::IsNullOrWhiteSpace($Script:Logging.CustomTargets)) -and (Test-Path -Path $Script:Logging.CustomTargets -PathType Container)) { $targets += Get-ChildItem -Path $Script:Logging.CustomTargets -Filter '*.ps1' } foreach ($target in $targets) { $module = . $target.FullName $Script:Logging.Targets[$module.Name] = @{ Init = $module.Init Logger = $module.Logger Description = $module.Description Defaults = $module.Configuration ParamsRequired = $module.Configuration.GetEnumerator() | Where-Object { $_.Value.Required -eq $true } | Select-Object -ExpandProperty Name | Sort-Object } } } #EndRegion './Private/Initialize-LoggingTarget.ps1' 25 #Region './Private/Merge-DefaultConfig.ps1' -1 function Merge-DefaultConfig { param( [string] $Target, [hashtable] $Configuration ) $DefaultConfiguration = $Script:Logging.Targets[$Target].Defaults $ParamsRequired = $Script:Logging.Targets[$Target].ParamsRequired $result = @{} foreach ($Param in $DefaultConfiguration.Keys) { if ($Param -in $ParamsRequired -and $Param -notin $Configuration.Keys) { throw ('Configuration {0} is required for target {1}; please provide one of type {2}' -f $Param, $Target, $DefaultConfiguration[$Param].Type) } if ($Configuration.ContainsKey($Param)) { if ($Configuration[$Param] -is $DefaultConfiguration[$Param].Type) { $result[$Param] = $Configuration[$Param] } else { throw ('Configuration {0} has to be of type {1} for target {2}' -f $Param, $DefaultConfiguration[$Param].Type, $Target) } } else { $result[$Param] = $DefaultConfiguration[$Param].Default } } return $result } #EndRegion './Private/Merge-DefaultConfig.ps1' 30 #Region './Private/New-LoggingDynamicParam.ps1' -1 <# .SYNOPSIS Creates the param used inside the DynamicParam{}-Block .DESCRIPTION New-LoggingDynamicParam creates (or appends) a RuntimeDefinedParameterDictionary with a parameter whos value is validated through a dynamic validate set. .PARAMETER Name displayed parameter name .PARAMETER Level Constructs the validate set out of the currently configured logging level names. .PARAMETER Target Constructs the validate set out of the currently configured logging targets. .PARAMETER DynamicParams Dictionary to be appended. (Useful for multiple dynamic params) .PARAMETER Mandatory Controls if parameter is mandatory for call. Defaults to $true .PARAMETER Alias Optional alias(es) for the parameter. Can be a single string or array of strings. .EXAMPLE DynamicParam{ New-LoggingDynamicParam -Name "Level" -Level -DefaultValue 'Verbose' } DynamicParam{ $dictionary = New-LoggingDynamicParam -Name "Level" -Level New-LoggingDynamicParam -Name "Target" -Target -DynamicParams $dictionary } #> function New-LoggingDynamicParam { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'FP')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'FP')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not alter system state')] [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])] [CmdletBinding(DefaultParameterSetName = 'DynamicTarget')] param( [Parameter(Mandatory = $true, ParameterSetName = 'DynamicLevel')] [Parameter(Mandatory = $true, ParameterSetName = 'DynamicTarget')] [String] $Name, [Parameter(Mandatory = $true, ParameterSetName = 'DynamicLevel')] [switch] $Level, [Parameter(Mandatory = $true, ParameterSetName = 'DynamicTarget')] [switch] $Target, [boolean] $Mandatory = $true, [string[]] $Alias, [System.Management.Automation.RuntimeDefinedParameterDictionary] $DynamicParams ) if (!$DynamicParams) { $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) # Add alias attribute if provided if ($Alias) { $aliasAttribute = [System.Management.Automation.AliasAttribute]::new($Alias) $attributeCollection.Add($aliasAttribute) } [String[]] $allowedValues = @() switch ($PSCmdlet.ParameterSetName) { 'DynamicTarget' { $allowedValues += $Script:Logging.Targets.Keys } 'DynamicLevel' { $allowedValues += Get-LevelsName } } $validateSetAttribute = [System.Management.Automation.ValidateSetAttribute]::new($allowedValues) $attributeCollection.Add($validateSetAttribute) $dynamicParam = [System.Management.Automation.RuntimeDefinedParameter]::new($Name, [string], $attributeCollection) $DynamicParams.Add($Name, $dynamicParam) return $DynamicParams } #EndRegion './Private/New-LoggingDynamicParam.ps1' 107 #Region './Private/Set-LoggingVariables.ps1' -1 function Set-LoggingVariables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Ignored as of now, this is inherited from the original module. This is a internal module cmdlet so the user is not impacted by this.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not alter system state')] param() #Already setup if ($Script:Logging -and $Script:LevelNames) { return } Write-Verbose -Message 'Setting up vars' $Script:NOTSET = 0 $Script:SQL = 5 $Script:DEBUG = 10 $Script:VERBOSE = 14 $Script:INFO = 20 $Script:NOTICE = 24 $Script:SUCCESS = 26 $Script:WARNING = 30 $Script:ERROR_ = 40 $Script:CRITICAL = 50 $Script:ALERT = 60 $Script:EMERGENCY = 70 New-Variable -Name LevelNames -Scope Script -Option ReadOnly -Value ([hashtable]::Synchronized(@{ $NOTSET = 'NOTSET' $ERROR_ = 'ERROR' $WARNING = 'WARNING' $INFO = 'INFO' $DEBUG = 'DEBUG' $VERBOSE = 'VERBOSE' $NOTICE = 'NOTICE' $SUCCESS = 'SUCCESS' $CRITICAL = 'CRITICAL' $ALERT = 'ALERT' $EMERGENCY = 'EMERGENCY' $SQL = 'SQL' 'NOTSET' = $NOTSET 'ERROR' = $ERROR_ 'WARNING' = $WARNING 'INFO' = $INFO 'DEBUG' = $DEBUG 'VERBOSE' = $VERBOSE 'NOTICE' = $NOTICE 'SUCCESS' = $SUCCESS 'CRITICAL' = $CRITICAL 'ALERT' = $ALERT 'EMERGENCY' = $EMERGENCY 'SQL' = $SQL })) 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 './Private/Set-LoggingVariables.ps1' 73 #Region './Private/Start-LoggingManager.ps1' -1 function Start-LoggingManager { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not alter system state')] [CmdletBinding()] param( [TimeSpan]$ConsumerStartupTimeout = '00:00:10' ) New-Variable -Name LoggingEventQueue -Scope Script -Value ([System.Collections.Concurrent.BlockingCollection[hashtable]]::new(100)) New-Variable -Name LoggingRunspace -Scope Script -Option ReadOnly -Value ([hashtable]::Synchronized(@{ })) New-Variable -Name TargetsInitSync -Scope Script -Option ReadOnly -Value ([System.Threading.ManualResetEventSlim]::new($false)) $Script:InitialSessionState = [initialsessionstate]::CreateDefault() if ($Script:InitialSessionState.psobject.Properties['ApartmentState']) { $Script:InitialSessionState.ApartmentState = [System.Threading.ApartmentState]::MTA } # Importing variables into runspace foreach ($sessionVariable in 'ScriptRoot', 'LevelNames', 'Logging', 'LoggingEventQueue', 'TargetsInitSync') { $Value = Get-Variable -Name $sessionVariable -ErrorAction Continue -ValueOnly Write-Verbose "Importing variable $sessionVariable`: $Value into runspace" $v = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $sessionVariable, $Value, '', ([System.Management.Automation.ScopedItemOptions]::AllScope) $Script:InitialSessionState.Variables.Add($v) } # Importing functions into runspace foreach ($Function in 'Format-Pattern', 'Initialize-LoggingTarget', 'Get-LevelNumber') { Write-Verbose "Importing function $($Function) into runspace" $Body = Get-Content Function:\$Function $f = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $Function, $Body $Script:InitialSessionState.Commands.Add($f) } #Setup runspace # Ensure backward compatibility - add default tags to existing targets that don't have them if ($Script:Logging.EnabledTargets) { $targetsToUpdate = @() for ($targetEnum = $Script:Logging.EnabledTargets.GetEnumerator(); $targetEnum.MoveNext(); ) { $uniqueName = $targetEnum.Current.Key $targetConfig = $targetEnum.Current.Value if (-not $targetConfig.ContainsKey('Tags')) { $targetsToUpdate += $uniqueName } } foreach ($uniqueName in $targetsToUpdate) { $Script:Logging.EnabledTargets[$uniqueName].Tags = @('default') Write-Verbose "Added default tags to existing target: $uniqueName" } } $Script:LoggingRunspace.Runspace = [runspacefactory]::CreateRunspace($Script:InitialSessionState) $Script:LoggingRunspace.Runspace.Name = 'LoggingQueueConsumer' $Script:LoggingRunspace.Runspace.Open() $Script:LoggingRunspace.Runspace.SessionStateProxy.SetVariable('ParentHost', $Host) $Script:LoggingRunspace.Runspace.SessionStateProxy.SetVariable('VerbosePreference', $VerbosePreference) # Spawn Logging Consumer $Consumer = { Initialize-LoggingTarget $TargetsInitSync.Set(); # Signal to the parent runspace that logging targets have been loaded foreach ($Log in $Script:LoggingEventQueue.GetConsumingEnumerable()) { if ($Script:Logging.EnabledTargets) { $ParentHost.NotifyBeginApplication() try { #Enumerating through a collection is intrinsically not a thread-safe procedure for ($targetEnum = $Script:Logging.EnabledTargets.GetEnumerator(); $targetEnum.MoveNext(); ) { [string] $UniqueName = $targetEnum.Current.key [hashtable] $TargetConfiguration = $targetEnum.Current.Value # Get the target type - handle both new format (with Type property) and legacy format $TargetType = if ($TargetConfiguration.ContainsKey('Type')) { $TargetConfiguration.Type } else { $UniqueName # Fallback for legacy configurations } if (-not $Script:Logging.Targets.ContainsKey($TargetType)) { $ParentHost.UI.WriteErrorLine("Target type '$TargetType' not found for target '$UniqueName'") continue } $Logger = [scriptblock] $Script:Logging.Targets[$TargetType].Logger $targetLevelNo = Get-LevelNumber -Level $TargetConfiguration.Level # Check level filtering $levelMatches = $Log.LevelNo -ge $targetLevelNo # Check tag filtering - get target tags (default to 'default' for legacy compatibility) $targetTags = if ($TargetConfiguration.ContainsKey('Tags')) { $TargetConfiguration.Tags } else { @('default') } # Get message tags (default to 'default' if not present) $messageTags = if ($null -ne $Log.tags) { $Log.tags } else { @('default') } # Check if any message tag matches any target tag (intersection) $tagMatches = ($messageTags | Where-Object { $_ -in $targetTags }).Count -gt 0 if ($levelMatches -and $tagMatches) { Invoke-Command -ScriptBlock $Logger -ArgumentList @($Log.PSObject.Copy(), $TargetConfiguration) } } } catch { $ParentHost.UI.WriteErrorLine($_) } finally { $ParentHost.NotifyEndApplication() } } } } $Script:LoggingRunspace.Powershell = [Powershell]::Create().AddScript($Consumer, $true) $Script:LoggingRunspace.Powershell.Runspace = $Script:LoggingRunspace.Runspace $Script:LoggingRunspace.Handle = $Script:LoggingRunspace.Powershell.BeginInvoke() #region Handle Module Removal $OnRemoval = { $Module = Get-Module PSLogs if ($Module) { $Module.Invoke({ Wait-Logging Stop-LoggingManager }) } [System.GC]::Collect() } # This scriptblock would be called within the module scope $ExecutionContext.SessionState.Module.OnRemove += $OnRemoval # This scriptblock would be called within the global scope and wouldn't have access to internal module variables and functions that we need $Script:LoggingRunspace.EngineEventJob = Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action $OnRemoval #endregion Handle Module Removal if (-not $TargetsInitSync.Wait($ConsumerStartupTimeout)) { throw 'Timed out while waiting for logging consumer to start up' } } #EndRegion './Private/Start-LoggingManager.ps1' 170 #Region './Private/Stop-LoggingManager.ps1' -1 function Stop-LoggingManager { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not alter system state')] param () $Script:LoggingEventQueue.CompleteAdding() $Script:LoggingEventQueue.Dispose() [void] $Script:LoggingRunspace.Powershell.EndInvoke($Script:LoggingRunspace.Handle) [void] $Script:LoggingRunspace.Powershell.Dispose() $ExecutionContext.SessionState.Module.OnRemove = $null Get-EventSubscriber | Where-Object { $_.Action.Id -eq $Script:LoggingRunspace.EngineEventJob.Id } | Unregister-Event Remove-Variable -Scope Script -Force -Name LoggingEventQueue Remove-Variable -Scope Script -Force -Name LoggingRunspace Remove-Variable -Scope Script -Force -Name TargetsInitSync } #EndRegion './Private/Stop-LoggingManager.ps1' 19 #Region './Public/Add-LoggingLevel.ps1' -1 <# .SYNOPSIS Define a new severity level .DESCRIPTION This function add a new severity level to the ones already defined .PARAMETER Level An integer that identify the severity of the level, higher the value higher the severity of the level By default the module defines this levels: NOTSET 0 DEBUG 10 INFO 20 WARNING 30 ERROR 40 .PARAMETER LevelName The human redable name to assign to the level .EXAMPLE PS C:\> Add-LoggingLevel -Level 41 -LevelName CRITICAL .EXAMPLE PS C:\> Add-LoggingLevel -Level 15 -LevelName VERBOSE .LINK https://logging.readthedocs.io/en/latest/functions/Add-LoggingLevel.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Add-LoggingLevel.ps1 #> function Add-LoggingLevel { [CmdletBinding(HelpUri='https://logging.readthedocs.io/en/latest/functions/Add-LoggingLevel.md')] param( [Parameter(Mandatory)] [int] $Level, [Parameter(Mandatory)] [string] $LevelName ) if ($Level -notin $LevelNames.Keys -and $LevelName -notin $LevelNames.Keys) { $LevelNames[$Level] = $LevelName.ToUpper() $LevelNames[$LevelName] = $Level } elseif ($Level -in $LevelNames.Keys -and $LevelName -notin $LevelNames.Keys) { $LevelNames.Remove($LevelNames[$Level]) | Out-Null $LevelNames[$Level] = $LevelName.ToUpper() $LevelNames[$LevelNames[$Level]] = $Level } elseif ($Level -notin $LevelNames.Keys -and $LevelName -in $LevelNames.Keys) { $LevelNames.Remove($LevelNames[$LevelName]) | Out-Null $LevelNames[$LevelName] = $Level } } #EndRegion './Public/Add-LoggingLevel.ps1' 56 #Region './Public/Add-LoggingTarget.ps1' -1 <# .SYNOPSIS Enable a logging target .DESCRIPTION This function configure and enable a logging target .PARAMETER Type The type of the target to enable and configure .PARAMETER Name Alias for Type parameter (maintained for backward compatibility) .PARAMETER UniqueName Unique identifier for this target instance. If not specified, defaults to the Type value .PARAMETER Configuration An hashtable containing the configurations for the target Can include a 'Tags' key with an array of strings for tag-based message routing .EXAMPLE PS C:\> Add-LoggingTarget -Name Console -Configuration @{Level = 'DEBUG'} .EXAMPLE PS C:\> Add-LoggingTarget -Type File -UniqueName 'ErrorsOnly' -Configuration @{Level = 'ERROR'; Path = 'C:\Temp\errors.log'} .EXAMPLE PS C:\> Add-LoggingTarget -Name File -Configuration @{Level = 'INFO'; Path = 'C:\Temp\script.log'} .EXAMPLE PS C:\> Add-LoggingTarget -Type File -UniqueName 'DatabaseLogs' -Configuration @{Level = 'INFO'; Path = 'C:\Logs\db.log'; Tags = @('Database', 'Performance')} .LINK https://logging.readthedocs.io/en/latest/functions/Add-LoggingTarget.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://logging.readthedocs.io/en/latest/AvailableTargets.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Add-LoggingTarget.ps1 #> function Add-LoggingTarget { [CmdletBinding(HelpUri = 'https://logging.readthedocs.io/en/latest/functions/Add-LoggingTarget.md')] param( [Parameter(Position = 2)] [hashtable] $Configuration = @{} ) dynamicparam { # Create Type parameter with Name as alias for backward compatibility $dictionary = New-LoggingDynamicParam -Name 'Type' -Target -Alias @('Name') # Add UniqueName parameter $uniqueNameAttribute = New-Object System.Management.Automation.ParameterAttribute $uniqueNameAttribute.Mandatory = $false $uniqueNameCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $uniqueNameCollection.Add($uniqueNameAttribute) $uniqueNameParam = New-Object System.Management.Automation.RuntimeDefinedParameter('UniqueName', [string], $uniqueNameCollection) $dictionary.Add('UniqueName', $uniqueNameParam) return $dictionary } end { # Determine target type (Type parameter or Name alias) $targetType = $PSBoundParameters.Type # Determine unique name (use UniqueName if provided, otherwise use target type) $uniqueName = if ($PSBoundParameters.UniqueName) { $PSBoundParameters.UniqueName } else { $targetType } # Allow replacing existing targets with same UniqueName for backward compatibility if ($Script:Logging.EnabledTargets.ContainsKey($uniqueName)) { Write-Verbose "Replacing existing logging target with UniqueName '$uniqueName'" } # Validate that the target type exists if (-not $Script:Logging.Targets.ContainsKey($targetType)) { throw "Logging target type '$targetType' is not available. Available targets: $($Script:Logging.Targets.Keys -join ', ')" } # Create target configuration with type and display name metadata $targetConfig = Merge-DefaultConfig -Target $targetType -Configuration $Configuration $targetConfig.Type = $targetType $targetConfig.UniqueName = $uniqueName # Process tags for case-insensitive matching (default to 'Default' if not specified) if ($Configuration.Tags) { $targetConfig.Tags = $Configuration.Tags | ForEach-Object { $_.ToLower() } } else { $targetConfig.Tags = @('default') } $Script:Logging.EnabledTargets[$uniqueName] = $targetConfig # Special case hack - resolve target file path if it's a relative path # This can't be done in the Init scriptblock of the logging target because that scriptblock gets created in the # log consumer runspace and doesn't inherit the current SessionState. That means that the scriptblock doesn't know the # current working directory at the time when `Add-LoggingTarget` is being called and can't accurately resolve the relative path. if ($targetType -eq 'File') { $Script:Logging.EnabledTargets[$uniqueName].Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Configuration.Path) } if ($Script:Logging.Targets[$targetType].Init -is [scriptblock]) { & $Script:Logging.Targets[$targetType].Init $Script:Logging.EnabledTargets[$uniqueName] } } } #EndRegion './Public/Add-LoggingTarget.ps1' 115 #Region './Public/Get-LoggingAvailableTarget.ps1' -1 <# .SYNOPSIS Returns available logging targets .DESCRIPTION This function returns available logging targtes .EXAMPLE PS C:\> Get-LoggingAvailableTarget .LINK https://logging.readthedocs.io/en/latest/functions/Get-LoggingAvailableTarget.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Get-LoggingAvailableTarget.ps1 #> function Get-LoggingAvailableTarget { [CmdletBinding(HelpUri='https://logging.readthedocs.io/en/latest/functions/Get-LoggingAvailableTarget.md')] param() return $Script:Logging.Targets } #EndRegion './Public/Get-LoggingAvailableTarget.ps1' 21 #Region './Public/Get-LoggingCallerScope.ps1' -1 <# .SYNOPSIS Returns the default caller scope .DESCRIPTION This function returns an int representing the scope where the invocation scope for the caller should be obtained from .EXAMPLE PS C:\> Get-LoggingCallerScope .LINK https://logging.readthedocs.io/en/latest/functions/Get-LoggingCallerScope.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://logging.readthedocs.io/en/latest/LoggingFormat.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Get-LoggingCallerScope.ps1 #> function Get-LoggingCallerScope { [CmdletBinding()] param() return $Script:Logging.CallerScope } #EndRegion './Public/Get-LoggingCallerScope.ps1' 23 #Region './Public/Get-LoggingDefaultFormat.ps1' -1 <# .SYNOPSIS Returns the default message format .DESCRIPTION This function returns a string representing the default message format used by enabled targets that don't override it .EXAMPLE PS C:\> Get-LoggingDefaultFormat .LINK https://logging.readthedocs.io/en/latest/functions/Get-LoggingDefaultFormat.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://logging.readthedocs.io/en/latest/LoggingFormat.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Get-LoggingDefaultFormat.ps1 #> function Get-LoggingDefaultFormat { [CmdletBinding()] param() return $Script:Logging.Format } #EndRegion './Public/Get-LoggingDefaultFormat.ps1' 23 #Region './Public/Get-LoggingDefaultLevel.ps1' -1 <# .SYNOPSIS Returns the default message level .DESCRIPTION This function returns a string representing the default message level used by enabled targets that don't override it .EXAMPLE PS C:\> Get-LoggingDefaultLevel .LINK https://logging.readthedocs.io/en/latest/functions/Get-LoggingDefaultLevel.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Get-LoggingDefaultLevel.ps1 #> function Get-LoggingDefaultLevel { [CmdletBinding(HelpUri = 'https://logging.readthedocs.io/en/latest/functions/Get-LoggingDefaultLevel.md')] param() return Get-LevelName -Level $Script:Logging.LevelNo } #EndRegion './Public/Get-LoggingDefaultLevel.ps1' 26 #Region './Public/Get-LoggingTarget.ps1' -1 <# .SYNOPSIS Returns enabled logging targets .DESCRIPTION This function returns enabled logging targtes .PARAMETER Name The Name of the target to retrieve, if not passed all configured targets will be returned .EXAMPLE PS C:\> Get-LoggingTarget .EXAMPLE PS C:\> Get-LoggingTarget -Name Console .LINK https://logging.readthedocs.io/en/latest/functions/Get-LoggingTarget.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Get-LoggingTarget.ps1 #> function Get-LoggingTarget { [CmdletBinding(HelpUri = 'https://logging.readthedocs.io/en/latest/functions/Get-LoggingTarget.md')] param( [string] $Name = $null ) if ($PSBoundParameters.Name) { return $Script:Logging.EnabledTargets[$Name] } return $Script:Logging.EnabledTargets } #EndRegion './Public/Get-LoggingTarget.ps1' 31 #Region './Public/Remove-LoggingTarget.ps1' -1 <# .SYNOPSIS Remove a logging target .DESCRIPTION This function removes a previously configured logging target .PARAMETER UniqueName The UniqueName of the target to remove. If not specified, removes target by Type name .PARAMETER Name Alias for Type parameter (maintained for backward compatibility) .PARAMETER Type The type of target to remove (used when UniqueName is not specified) .EXAMPLE PS C:\> Remove-LoggingTarget -UniqueName 'ErrorsOnly' Removes the target with UniqueName 'ErrorsOnly' .EXAMPLE PS C:\> Remove-LoggingTarget -Name Console Removes the Console target (backward compatibility) .EXAMPLE PS C:\> Remove-LoggingTarget -Type File Removes the File target (if only one exists) .LINK https://logging.readthedocs.io/en/latest/functions/Remove-LoggingTarget.md .LINK https://logging.readthedocs.io/en/latest/functions/Add-LoggingTarget.md #> function Remove-LoggingTarget { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No system state changed.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Writes help when input is incorrect')] [CmdletBinding(DefaultParameterSetName = 'ByUniqueName', HelpUri = 'https://logging.readthedocs.io/en/latest/functions/Remove-LoggingTarget.md')] param( [Parameter(ParameterSetName = 'ByUniqueName', Position = 0)] [Alias('Name')] [string] $UniqueName, [Parameter(ParameterSetName = 'ByType', Mandatory)] [string] $Type ) if ($PSCmdlet.ParameterSetName -eq 'ByType') { # Remove by Type - find targets with matching Type property $targetsToRemove = @() foreach ($targetEntry in $Script:Logging.EnabledTargets.GetEnumerator()) { $targetConfig = $targetEntry.Value $targetType = if ($targetConfig.ContainsKey('Type')) { $targetConfig.Type } else { $targetEntry.Key # Fallback for legacy configurations } if ($targetType -eq $Type) { $targetsToRemove += $targetEntry.Key } } if ($targetsToRemove.Count -eq 0) { Write-Warning "No logging targets of type '$Type' found" return } if ($targetsToRemove.Count -gt 1) { Write-Warning "Multiple targets of type '$Type' found: $($targetsToRemove -join ', '). Use -UniqueName to remove a specific target." return } $UniqueName = $targetsToRemove[0] } # If no UniqueName provided, list available targets if (-not $UniqueName) { if ($Script:Logging.EnabledTargets.Count -eq 0) { Write-Warning 'No logging targets are currently configured' return } Write-Host 'Available logging targets:' foreach ($targetEntry in $Script:Logging.EnabledTargets.GetEnumerator()) { $targetConfig = $targetEntry.Value $targetType = if ($targetConfig.ContainsKey('Type')) { $targetConfig.Type } else { $targetEntry.Key } Write-Host " UniqueName: $($targetEntry.Key), Type: $targetType" } Write-Host 'Use -UniqueName to specify which target to remove' return } # Check if target exists if (-not $Script:Logging.EnabledTargets.ContainsKey($UniqueName)) { Write-Warning "Logging target with UniqueName '$UniqueName' not found" if ($Script:Logging.EnabledTargets.Count -gt 0) { Write-Host "Available targets: $($Script:Logging.EnabledTargets.Keys -join ', ')" } return } # Get target info for confirmation message $targetConfig = $Script:Logging.EnabledTargets[$UniqueName] $targetType = if ($targetConfig.ContainsKey('Type')) { $targetConfig.Type } else { $UniqueName } # Remove the target $removed = $Script:Logging.EnabledTargets.TryRemove($UniqueName, [ref]$null) if ($removed) { Write-Verbose "Successfully removed logging target '$UniqueName' (Type: $targetType)" # If this was the last target, inform the user if ($Script:Logging.EnabledTargets.Count -eq 0) { Write-Verbose 'No logging targets remain configured. Logging will continue but no output will be generated until targets are added.' } } else { Write-Warning "Failed to remove logging target '$UniqueName'" } } #EndRegion './Public/Remove-LoggingTarget.ps1' 147 #Region './Public/Set-LoggingCallerScope.ps1' -1 <# .SYNOPSIS Sets the scope from which to get the caller scope .DESCRIPTION This function sets the scope to obtain information from the caller .PARAMETER CallerScope Integer representing the scope to use to find the caller information. Defaults to 1 which represent the scope of the function where Write-Log is being called from .EXAMPLE PS C:\> Set-LoggingCallerScope -CallerScope 2 .EXAMPLE PS C:\> Set-LoggingCallerScope It sets the caller scope to 1 .LINK https://logging.readthedocs.io/en/latest/functions/Set-LoggingCallerScope.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Set-LoggingCallerScope.ps1 #> function Set-LoggingCallerScope { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not alter system state')] [CmdletBinding(HelpUri = 'https://logging.readthedocs.io/en/latest/functions/Set-LoggingCallerScope.md')] param( [int]$CallerScope = $Defaults.CallerScope ) Wait-Logging $Script:Logging.CallerScope = $CallerScope } #EndRegion './Public/Set-LoggingCallerScope.ps1' 39 #Region './Public/Set-LoggingCustomTarget.ps1' -1 <# .SYNOPSIS Sets a folder as custom target repository .DESCRIPTION This function sets a folder as a custom target repository. Every *.ps1 file will be loaded as a custom target and available to be enabled for logging to. .PARAMETER Path A valid path containing *.ps1 files that defines new loggin targets .EXAMPLE PS C:\> Set-LoggingCustomTarget -Path C:\Logging\CustomTargets .LINK https://logging.readthedocs.io/en/latest/functions/Set-LoggingCustomTarget.md .LINK https://logging.readthedocs.io/en/latest/functions/CustomTargets.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Set-LoggingCustomTarget.ps1 #> function Set-LoggingCustomTarget { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not alter system state')] [CmdletBinding(HelpUri = 'https://logging.readthedocs.io/en/latest/functions/Set-LoggingCustomTarget.md')] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path -Path $_ -PathType Container })] [string] $Path ) Write-Verbose 'Stopping Logging Manager' Stop-LoggingManager $Script:Logging.CustomTargets = $Path Write-Verbose 'Starting Logging Manager' Start-LoggingManager } #EndRegion './Public/Set-LoggingCustomTarget.ps1' 45 #Region './Public/Set-LoggingDefaultFormat.ps1' -1 <# .SYNOPSIS Sets a global logging message format .DESCRIPTION This function sets a global logging message format .PARAMETER Format The string used to format the message to log .EXAMPLE PS C:\> Set-LoggingDefaultFormat -Format '[%{level:-7}] %{message}' .EXAMPLE PS C:\> Set-LoggingDefaultFormat It sets the default format as [%{timestamp:+%Y-%m-%d %T%Z}] [%{level:-7}] %{message} .LINK https://logging.readthedocs.io/en/latest/functions/Set-LoggingDefaultFormat.md .LINK https://logging.readthedocs.io/en/latest/functions/LoggingFormat.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Set-LoggingDefaultFormat.ps1 #> function Set-LoggingDefaultFormat { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not alter system state')] [CmdletBinding(HelpUri = 'https://logging.readthedocs.io/en/latest/functions/Set-LoggingDefaultFormat.md')] param( [string] $Format = $Defaults.Format ) Wait-Logging $Script:Logging.Format = $Format # Setting format on already configured targets foreach ($Target in $Script:Logging.EnabledTargets.Values) { if ($Target.ContainsKey('Format')) { $Target['Format'] = $Script:Logging.Format } } # Setting format on available targets foreach ($Target in $Script:Logging.Targets.Values) { if ($Target.Defaults.ContainsKey('Format')) { $Target.Defaults.Format.Default = $Script:Logging.Format } } } #EndRegion './Public/Set-LoggingDefaultFormat.ps1' 60 #Region './Public/Set-LoggingDefaultLevel.ps1' -1 <# .SYNOPSIS Sets a global logging severity level. .DESCRIPTION This function sets a global logging severity level. Log messages written with a lower logging level will be discarded. .PARAMETER Level The level severity name to set as default for enabled targets .EXAMPLE PS C:\> Set-LoggingDefaultLevel -Level ERROR PS C:\> Write-Log -Level INFO -Message "Test" => Discarded. .LINK https://logging.readthedocs.io/en/latest/functions/Set-LoggingDefaultLevel.md .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Set-LoggingDefaultLevel.ps1 #> function Set-LoggingDefaultLevel { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not alter system state')] [CmdletBinding(HelpUri = 'https://logging.readthedocs.io/en/latest/functions/Set-LoggingDefaultLevel.md')] param() DynamicParam { New-LoggingDynamicParam -Name 'Level' -Level } End { $Script:Logging.Level = $PSBoundParameters.Level $Script:Logging.LevelNo = Get-LevelNumber -Level $PSBoundParameters.Level # Setting level on already configured targets foreach ($Target in $Script:Logging.EnabledTargets.Values) { if ($Target.ContainsKey('Level')) { $Target['Level'] = $Script:Logging.Level } } # Setting level on available targets foreach ($Target in $Script:Logging.Targets.Values) { if ($Target.Defaults.ContainsKey('Level')) { $Target.Defaults.Level.Default = $Script:Logging.Level } } } } #EndRegion './Public/Set-LoggingDefaultLevel.ps1' 62 #Region './Public/Wait-Logging.ps1' -1 <# .SYNOPSIS Wait for the message queue to be emptied .DESCRIPTION This function can be used to block the execution of a script waiting for the message queue to be emptied .EXAMPLE PS C:\> Wait-Logging .LINK https://logging.readthedocs.io/en/latest/functions/Wait-Logging.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Wait-Logging.ps1 #> function Wait-Logging { [CmdletBinding(HelpUri='https://logging.readthedocs.io/en/latest/functions/Wait-Logging.md')] param() #This variable is initiated inside Start-LoggingManager if (!(Get-Variable -Name "LoggingEventQueue" -ErrorAction Ignore)) { return } $start = [datetime]::Now Start-Sleep -Milliseconds 10 while ($Script:LoggingEventQueue.Count -gt 0) { Start-Sleep -Milliseconds 20 <# If errors occure in the consumption of the logging requests, forcefully shutdown function after some time. #> $difference = [datetime]::Now - $start if ($difference.seconds -gt 30) { Write-Error -Message ("{0} :: Wait timeout." -f $MyInvocation.MyCommand) -ErrorAction SilentlyContinue break; } } } #EndRegion './Public/Wait-Logging.ps1' 44 #Region './Public/Write-Log.ps1' -1 <# .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 Provide an optional ErrorRecord .PARAMETER Tags An array of tags to associate with the log message for target routing. Defaults to 'Default' if not specified. .EXAMPLE PS C:\> Write-Log 'Hello, World!' .EXAMPLE PS C:\> Write-Log -Level ERROR -Message 'Hello, World!' .EXAMPLE PS C:\> Write-Log -Level ERROR -Message 'Hello, {0}!' -Arguments 'World' .EXAMPLE PS C:\> Write-Log -Level ERROR -Message 'Hello, {0}!' -Arguments 'World' -Body @{Server='srv01.contoso.com'} .EXAMPLE PS C:\> Write-Log -Level INFO -Message 'Database operation completed' -Tags @('Database', 'Performance') .LINK https://logging.readthedocs.io/en/latest/functions/Write-Log.md .LINK https://logging.readthedocs.io/en/latest/functions/Add-LoggingLevel.md .LINK https://github.com/EsOsO/Logging/blob/master/Logging/public/Write-Log.ps1 #> function Write-Log { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'This is a judgement call. The argument is that if this module is loaded the user should be considered aware that this is the main cmdlet of the module.')] [CmdletBinding()] 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, [Parameter(Position = 6, Mandatory = $false)] [string[]] $Tags = @('Default') ) dynamicparam { New-LoggingDynamicParam -Level -Mandatory $false -Name 'Level' $PSBoundParameters['Level'] = 'INFO' } end { $levelNumber = Get-LevelNumber -Level $PSBoundParameters.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 (-not [string]::IsNullOrEmpty($invocationInfo.ScriptName)) { $fileName = Split-Path -Path $invocationInfo.ScriptName -Leaf } # Normalize tags to lowercase for case-insensitive matching $normalizedTags = $Tags | ForEach-Object { $_.ToLower() } $logMessage = [hashtable] @{ timestamp = [datetime]::now timestamputc = [datetime]::UtcNow level = Get-LevelName -Level $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 tags = $normalizedTags } if ($PSBoundParameters.ContainsKey('Arguments')) { $logMessage['message'] = [string] $Message -f $Arguments $logMessage['args'] = $Arguments } #This variable is initiated via Start-LoggingManager $Script:LoggingEventQueue.Add($logMessage) } } #EndRegion './Public/Write-Log.ps1' 123 #Region './suffix.ps1' -1 # The content of this file will be appended to the end of the psm1 module file. This is useful for custom procesedures after all module functions are loaded. Set-LoggingVariables Start-LoggingManager #Trigger buil #EndRegion './suffix.ps1' 7 |