PSLogs.psm1

#Region '.\prefix.ps1' 0
# 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' 0
<#
.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' 0
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' 0
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' 10
#Region '.\Private\Get-LevelsName.ps1' 0
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' 0
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' 0
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' 0
<#
.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
 
.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,
        [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)


    [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' 97
#Region '.\Private\Set-LoggingVariables.ps1' 0
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:DEBUG = 10
    $Script:INFO = 20
    $Script:WARNING = 30
    $Script:ERROR_ = 40

    New-Variable -Name LevelNames           -Scope Script -Option ReadOnly -Value ([hashtable]::Synchronized(@{
                $NOTSET   = 'NOTSET'
                $ERROR_   = 'ERROR'
                $WARNING  = 'WARNING'
                $INFO     = 'INFO'
                $DEBUG    = 'DEBUG'
                'NOTSET'  = $NOTSET
                'ERROR'   = $ERROR_
                'WARNING' = $WARNING
                'INFO'    = $INFO
                'DEBUG'   = $DEBUG
            }))

    New-Variable -Name ScriptRoot           -Scope Script -Option ReadOnly -Value ([System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Module.Path))
    New-Variable -Name Defaults             -Scope Script -Option ReadOnly -Value @{
        Level       = $LevelNames[$LevelNames['NOTSET']]
        LevelNo     = $LevelNames['NOTSET']
        Format      = '[%{timestamp:+%Y-%m-%d %T%Z}] [%{level:-7}] %{message}'
        Timestamp   = '%Y-%m-%d %T%Z'
        CallerScope = 1
    }

    New-Variable -Name Logging              -Scope Script -Option ReadOnly -Value ([hashtable]::Synchronized(@{
                Level          = $Defaults.Level
                LevelNo        = $Defaults.LevelNo
                Format         = $Defaults.Format
                CallerScope    = $Defaults.CallerScope
                CustomTargets  = [String]::Empty
                Targets        = ([System.Collections.Concurrent.ConcurrentDictionary[string, hashtable]]::new([System.StringComparer]::OrdinalIgnoreCase))
                EnabledTargets = ([System.Collections.Concurrent.ConcurrentDictionary[string, hashtable]]::new([System.StringComparer]::OrdinalIgnoreCase))
            }))
}
#EndRegion '.\Private\Set-LoggingVariables.ps1' 52
#Region '.\Private\Start-LoggingManager.ps1' 0
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
    $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] $LoggingTarget = $targetEnum.Current.key
                        [hashtable] $TargetConfiguration = $targetEnum.Current.Value
                        $Logger = [scriptblock] $Script:Logging.Targets[$LoggingTarget].Logger

                        $targetLevelNo = Get-LevelNumber -Level $TargetConfiguration.Level

                        if ($Log.LevelNo -ge $targetLevelNo)
                        {
                            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' 117
#Region '.\Private\Stop-LoggingManager.ps1' 0
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' 0
<#
    .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' 0
<#
    .SYNOPSIS
        Enable a logging target
    .DESCRIPTION
        This function configure and enable a logging target
    .PARAMETER Name
        The name of the target to enable and configure
    .PARAMETER Configuration
        An hashtable containing the configurations for the target
    .EXAMPLE
        PS C:\> Add-LoggingTarget -Name Console -Configuration @{Level = 'DEBUG'}
    .EXAMPLE
        PS C:\> Add-LoggingTarget -Name File -Configuration @{Level = 'INFO'; Path = 'C:\Temp\script.log'}
    .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 {
        New-LoggingDynamicParam -Name 'Name' -Target
    }

    End {
        $Script:Logging.EnabledTargets[$PSBoundParameters.Name] = Merge-DefaultConfig -Target $PSBoundParameters.Name -Configuration $Configuration

        # 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($PSBoundParameters.Name -eq 'File'){
            $Script:Logging.EnabledTargets[$PSBoundParameters.Name].Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Configuration.Path)
        }

        if ($Script:Logging.Targets[$PSBoundParameters.Name].Init -is [scriptblock]) {
            & $Script:Logging.Targets[$PSBoundParameters.Name].Init $Script:Logging.EnabledTargets[$PSBoundParameters.Name]
        }
    }
}
#EndRegion '.\Public\Add-LoggingTarget.ps1' 50
#Region '.\Public\Get-LoggingAvailableTarget.ps1' 0
<#
    .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' 0
<#
    .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' 0
<#
    .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' 0
<#
    .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' 0
<#
    .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\Set-LoggingCallerScope.ps1' 0
<#
    .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' 0
<#
    .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' 0
<#
    .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' 0
<#
    .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' 0
<#
    .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' 0
<#
    .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
 
    .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'}
 
    .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
    )

    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
        }

        $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
        }

        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' 109
#Region '.\suffix.ps1' 0
# 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