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

<#
    .SYNOPSIS
        Gets the name of a logging level from its numeric value

    .DESCRIPTION
        This function retrieves the string name of a logging level based on its numeric value.
        If the level number is not found in the defined levels, it returns a generic 'Level X' format.

    .PARAMETER Level
        The numeric value of the logging level to get the name for

    .EXAMPLE
        PS C:\> Get-LevelName -Level 20
        INFO

        Gets the name for logging level 20, which returns 'INFO'

    .EXAMPLE
        PS C:\> Get-LevelName -Level 999
        Level 999

        Gets the name for an undefined logging level, which returns 'Level 999'

    .OUTPUTS
        System.String
        Returns the string name of the logging level

    .NOTES
        This is an internal function used by the PSLogs module to convert numeric
        logging levels to their string representations.
#>

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' 49
#Region './Private/Get-LevelNumber.ps1' -1

<#
    .SYNOPSIS
        Gets the numeric value of a logging level from its name or validates a numeric level

    .DESCRIPTION
        This function converts a logging level name to its numeric value, or validates that
        a numeric level is valid. It accepts both string level names (like 'INFO', 'ERROR')
        and integer level numbers. If the level is invalid, it throws an exception.

    .PARAMETER Level
        The logging level to convert or validate. Can be either a string name (e.g., 'INFO')
        or an integer number (e.g., 20)

    .EXAMPLE
        PS C:\> Get-LevelNumber -Level 'INFO'
        20

        Converts the string level name 'INFO' to its numeric value 20

    .EXAMPLE
        PS C:\> Get-LevelNumber -Level 30
        30

        Validates that numeric level 30 is valid and returns it

    .EXAMPLE
        PS C:\> Get-LevelNumber -Level 'INVALID'

        Throws an exception because 'INVALID' is not a valid level name

    .OUTPUTS
        System.Int32
        Returns the numeric value of the logging level

    .NOTES
        This is an internal function used by the PSLogs module to normalize logging
        levels to their numeric representations for comparison and filtering.

        Valid level names include: NOTSET, SQL, DEBUG, VERBOSE, INFO, NOTICE, SUCCESS,
        WARNING, ERROR, CRITICAL, ALERT, EMERGENCY
#>

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' 61
#Region './Private/Get-LevelsName.ps1' -1

<#
    .SYNOPSIS
        Gets all available logging level names

    .DESCRIPTION
        This function retrieves all available logging level names from the PSLogs module.
        It returns only the string names of the levels (not the numeric values) in
        alphabetical order.

    .EXAMPLE
        PS C:\> Get-LevelsName
        ALERT
        CRITICAL
        DEBUG
        EMERGENCY
        ERROR
        INFO
        NOTICE
        NOTSET
        SQL
        SUCCESS
        VERBOSE
        WARNING

        Returns all available logging level names in sorted order

    .OUTPUTS
        System.String[]
        Returns an array of string level names

    .NOTES
        This is an internal function used by the PSLogs module to provide a list of
        valid level names for validation and dynamic parameter creation.

        The function filters out numeric keys from the LevelNames hashtable to return
        only the string representations.
#>

function Get-LevelsName
{
    [CmdletBinding()]
    param()

    return $Script:LevelNames.Keys | Where-Object { $_ -isnot [int] } | Sort-Object
}
#EndRegion './Private/Get-LevelsName.ps1' 45
#Region './Private/Initialize-LoggingTarget.ps1' -1

<#
    .SYNOPSIS
        Discovers and initializes all available logging targets

    .DESCRIPTION
        This function scans for and loads all logging target plugins from both the built-in
        targets directory and any custom targets directory. It processes each target's
        configuration schema, validates requirements, and registers them in the module's
        target registry for use with Add-LoggingTarget.

    .EXAMPLE
        PS C:\> Initialize-LoggingTarget

        Loads all available logging targets from the include directory and any custom paths

    .NOTES
        This is an internal function called during module initialization and when the
        consumer runspace starts. It performs the following operations:

        - Scans the built-in targets in the 'include' directory
        - Scans custom targets if a CustomTargets path is configured
        - Loads each target script and extracts its configuration schema
        - Registers targets in the $Script:Logging.Targets hashtable
        - Identifies required vs optional parameters for each target
        - Makes targets available for use with Add-LoggingTarget

        Each target plugin must return a hashtable with:
        - Name: Target identifier
        - Init: Initialization script block (optional)
        - Logger: Main logging script block
        - Description: Target description (optional)
        - Configuration: Parameter schema with types and defaults

        The function is idempotent and can be called multiple times safely.
#>

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' 60
#Region './Private/Merge-DefaultConfig.ps1' -1

<#
    .SYNOPSIS
        Merges user configuration with default target configuration values

    .DESCRIPTION
        This function combines user-provided configuration parameters with the default
        configuration for a specific logging target. It validates that required parameters
        are provided, checks parameter types, and fills in default values for missing
        optional parameters.

    .PARAMETER Target
        The name of the logging target to merge configuration for (e.g., 'File', 'Console')

    .PARAMETER Configuration
        A hashtable containing user-provided configuration parameters for the target

    .EXAMPLE
        PS C:\> $config = @{ Path = 'C:\logs\app.log'; Level = 'INFO' }
        PS C:\> Merge-DefaultConfig -Target 'File' -Configuration $config

        Merges the user configuration with File target defaults, filling in missing
        values like Format, Encoding, etc.

    .EXAMPLE
        PS C:\> $config = @{ Level = 'DEBUG' }
        PS C:\> Merge-DefaultConfig -Target 'Console' -Configuration $config

        Merges the user configuration with Console target defaults

    .OUTPUTS
        System.Collections.Hashtable
        Returns a complete configuration hashtable with all required and optional
        parameters filled in

    .NOTES
        This is an internal function used by Add-LoggingTarget to ensure that all
        target configurations have the necessary parameters with appropriate default
        values and correct types.

        The function will throw exceptions if:
        - Required parameters are missing
        - Parameter types don't match the expected types for the target
#>

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' 82
#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
{
    <#
    .SYNOPSIS
        Initializes the core PSLogs module variables and data structures

    .DESCRIPTION
        This function sets up all the essential variables and data structures used by the
        PSLogs module. It creates logging levels, level name mappings, default configurations,
        and the main logging hashtable with thread-safe collections for targets and
        enabled targets. This function is called during module initialization and ensures
        idempotent operation by checking if variables are already set up.

    .EXAMPLE
        PS C:\> Set-LoggingVariables

        Initializes all PSLogs module variables if they haven't been set up already

    .NOTES
        This is an internal initialization function called during module import.

        The function creates:
        - Logging level constants (NOTSET=0, DEBUG=10, INFO=20, etc.)
        - LevelNames hashtable for bidirectional level/name mapping
        - ScriptRoot variable pointing to the module directory
        - Defaults hashtable with default level, format, and caller scope
        - Main Logging hashtable with thread-safe concurrent dictionaries

        The function is idempotent - it will return early if variables are already initialized.
        All collections use thread-safe implementations to support the async logging architecture.
#>

    [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' 102
#Region './Private/Start-LoggingManager.ps1' -1

<#
    .SYNOPSIS
        Starts the asynchronous logging consumer runspace

    .DESCRIPTION
        This function initializes and starts the background consumer runspace that processes
        log messages from the queue and dispatches them to configured targets. It sets up
        a separate PowerShell runspace with its own session state, imports necessary variables
        and functions, and starts the consumer loop that processes messages until the queue
        is marked as completed.

    .PARAMETER ConsumerStartupTimeout
        The maximum time to wait for the consumer runspace to initialize before timing out.
        Defaults to 10 seconds.

    .EXAMPLE
        PS C:\> Start-LoggingManager

        Starts the logging manager with default 10-second startup timeout

    .EXAMPLE
        PS C:\> Start-LoggingManager -ConsumerStartupTimeout '00:00:30'

        Starts the logging manager with a 30-second startup timeout

    .NOTES
        This is an internal function that is automatically called when the first Write-Log
        command is executed. The consumer runspace:

        - Runs in a separate thread for non-blocking logging operations
        - Processes messages from a BlockingCollection queue
        - Applies level and tag filtering before sending to targets
        - Handles target initialization and error handling
        - Supports graceful shutdown when the module is unloaded

        The function creates thread-safe collections and synchronization primitives
        to coordinate between the main thread and the consumer runspace.
#>

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' 218
#Region './Private/Stop-LoggingManager.ps1' -1

<#
    .SYNOPSIS
        Stops the asynchronous logging consumer runspace and cleans up resources

    .DESCRIPTION
        This function gracefully shuts down the background consumer runspace by marking
        the message queue as completed, waiting for the consumer to finish processing
        remaining messages, and then disposing of all resources including the runspace,
        queue, and event handlers. It ensures proper cleanup to prevent resource leaks.

    .EXAMPLE
        PS C:\> Stop-LoggingManager

        Stops the logging manager and cleans up all resources

    .NOTES
        This is an internal function that is automatically called when:
        - The PSLogs module is being unloaded
        - PowerShell is exiting
        - Manual cleanup is requested

        The function performs the following cleanup steps:
        1. Marks the message queue as complete (no new messages accepted)
        2. Waits for the consumer runspace to finish processing remaining messages
        3. Disposes the PowerShell runspace and associated resources
        4. Unregisters event handlers and module removal callbacks
        5. Removes script-scoped variables

        After this function completes, a new Start-LoggingManager call would be
        required to resume logging operations.
#>

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' 50
#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