PSJobLogger.psm1

using namespace System.Collections
using namespace System.Collections.Concurrent

class PSJobLogger {
    <#
    Constant values used as an output stream identifier
    #>

    static [int]$StreamSuccess = 0
    static [int]$StreamError = 1
    static [int]$StreamWarning = 2
    static [int]$StreamVerbose = 3
    static [int]$StreamDebug = 4
    static [int]$StreamInformation = 5
    static [int]$StreamProgress = 6
    <#
    A list of available output streams
    #>

    static [int[]]$LogStreams = @(
        [PSJobLogger]::StreamSuccess,
        [PSJobLogger]::StreamError,
        [PSJobLogger]::StreamWarning,
        [PSJobLogger]::StreamVerbose,
        [PSJobLogger]::StreamDebug,
        [PSJobLogger]::StreamInformation,
        [PSJobLogger]::StreamProgress
    )

    # The name of the logger; used to construct a "prefix" that is prepended to each message
    [String]$Name
    # A thread-safe dictionary that holds thread-safe collections for each output stream
    [ConcurrentDictionary[int, ICollection]]$MessageTables
    # The logger prefix string; it is prepended to each message
    [String]$Prefix
    # Indicates that the class has been initialized and should not be initialized again
    [Boolean]$Initialized = $false

    PSJobLogger([String]$Name = 'PSJobLogger') {
        $this.SetName($Name)
        if ($Name -eq '') {
            $this.SetName('PSJobLogger')
        }
        $this.initializeMessageTables()
    }

    [void]
    initializeMessageTables() {
        if ($this.Initialized) {
            return
        }
        $this.MessageTables = [ConcurrentDictionary[int, ICollection]]::new()
        foreach ($stream in [PSJobLogger]::LogStreams) {
            if ($stream -eq [PSJobLogger]::StreamProgress) {
                $this.MessageTables.$stream = [ConcurrentDictionary[String, Hashtable]]::new()
                continue
            }
            $this.MessageTables.$stream = [ConcurrentQueue[String]]::new()
        }
        $this.Initialized = $true
    }

    [void]
    SetName([String]$Name) {
        $this.Name = $Name
        $this.Prefix = "${Name}: "
    }

    [void]
    Output([String]$Message) {
        $this.EnqueueMessage([PSJobLogger]::StreamSuccess, $Message)
    }

    [void]
    Error([String]$Message) {
        $this.EnqueueMessage([PSJobLogger]::StreamError, $Message)
    }

    [void]
    Warning([String]$Message) {
        $this.EnqueueMessage([PSJobLogger]::StreamWarning, $Message)
    }

    [void]
    Verbose([String]$Message) {
        $this.EnqueueMessage([PSJobLogger]::StreamVerbose, $Message)
    }

    [void]
    Debug([String]$Message) {
        $this.EnqueueMessage([PSJobLogger]::StreamDebug, $Message)
    }

    [void]
    Information([String]$Message) {
        $this.EnqueueMessage([PSJobLogger]::StreamInformation, $Message)
    }

    [void]
    EnqueueMessage([int]$Stream, [String]$Message) {
        [ConcurrentQueue[String]]$messageTable = $this.MessageTables.$Stream
        $messageTable.Enqueue($Message)
    }

    [void]
    Progress([String]$Id, [Hashtable]$ArgumentMap) {
        [ConcurrentDictionary[String, Hashtable]]$progressTable = $this.MessageTables.$([PSJobLogger]::StreamProgress)
        if ($null -eq $progressTable.$Id) {
            $progressTable.$Id = @{ }
        }
        $progressArgs = $progressTable.$Id
        foreach ($argumentKey in $ArgumentMap.Keys) {
            if ($null -eq $ArgumentMap.$argumentKey) {
                $progressArgs.Remove($argumentKey)
                continue
            }
            $progressArgs.$argumentKey = $ArgumentMap.$argumentKey
        }
    }

    [void]
    FlushStreams() {
        foreach ($stream in [PSJobLogger]::LogStreams) {
            $this.FlushOneStream($stream)
        }
    }

    [void]
    FlushOneStream([int]$Stream) {
        if ($null -eq $this.MessageTables.$Stream) {
            return
        }
        $messages = @()
        # drain the queue, unless it's the Progress stream
        if ($Stream -eq [PSJobLogger]::StreamProgress) {
            [ConcurrentDictionary[String, Hashtable]]$progressQueue = $this.MessageTables.$Stream
            foreach ($queueKey in $progressQueue.Keys) {
                $messages += @($progressQueue.$queueKey)
            }
        }
        else {
            [ConcurrentQueue[String]]$messageQueue = $this.MessageTables.$Stream
            $dequeuedMessage = ''
            while ($messageQueue.Count -gt 0) {
                if (-not($messageQueue.TryDequeue([ref]$dequeuedMessage))) {
                    break
                }
                $messages += @($dequeuedMessage)
            }
        }
        # write messages to the desired stream
        foreach ($message in $messages) {
            switch ($Stream) {
                # $message is a [String] unless it's the Progress stream
                ([PSJobLogger]::StreamSuccess) {
                    Write-Output "$( $this.Prefix )${message}"
                }
                ([PSJobLogger]::StreamError) {
                    Write-Error "$( $this.Prefix )${message}"
                }
                ([PSJobLogger]::StreamWarning) {
                    Write-Warning "$( $this.Prefix )${message}"
                }
                ([PSJobLogger]::StreamVerbose) {
                    Write-Verbose "$( $this.Prefix )${message}"
                }
                ([PSJobLogger]::StreamDebug) {
                    Write-Debug "$( $this.Prefix )${message}"
                }
                ([PSJobLogger]::StreamInformation) {
                    Write-Information "$( $this.Prefix )${message}"
                }
                # $message is a [Hashtable] for the Progress stream
                ([PSJobLogger]::StreamProgress) {
                    Write-Progress @message
                }
                default {
                    Write-Error "$( $this.Prefix )unexpected stream ${Stream}"
                    return
                }
            }
        }
    }
}

<#
.SYNOPSIS
    Return a newly-initialized PSJobLogger class
.PARAMETER Name
    The name of the logger; defaults to 'PSJobLogger'
.EXAMPLE
    PS> $jobLog = Initialize-PSJobLogger -Name MyLogger
#>

function Initialize-PSJobLogger {
    [OutputType([PSJobLogger])]
    param(
        [String]$Name = 'PSJobLogger'
    )
    return [PSJobLogger]::new($Name)
}