ScriptLog.psm1

#Region './Enum/ScriptLogMessageSeverity.ps1' 0
# Define log message severity types
enum ScriptLogMessageSeverity
{
    Information
    Verbose
    Warning
    Error
}
#EndRegion './Enum/ScriptLogMessageSeverity.ps1' 9
#Region './Enum/ScriptLogType.ps1' 0
# Define log types
enum ScriptLogType
{
    CMTrace
    Memory
}
#EndRegion './Enum/ScriptLogType.ps1' 7
#Region './Classes/01-LogMessage.ps1' 0
# Declare class for individual log messages
class LogMessage {
    [datetime]$DateTime
    [ScriptLogMessageSeverity]$Severity
    [string]$Source
    [string]$Context
    [int]$ProcessId
    [string]$Message

    LogMessage([datetime]$DateTime, [ScriptLogMessageSeverity]$Severity, [string]$Source, [string]$Context, [int]$ProcessId, [string]$Message) {
        $this.DateTime = $DateTime
        $this.Severity = $Severity
        $this.Source = $Source
        $this.Context = $Context
        $this.ProcessId = $ProcessId
        $this.Message = $Message
    }
}
#EndRegion './Classes/01-LogMessage.ps1' 19
#Region './Classes/02-ScriptLog.ps1' 0
# Declare class for a log object
class ScriptLog {
    [String] $FilePath
    [ScriptLogType] $LogType
    [String] $Source = $null
    [ScriptLogMessageSeverity[]] $MessagesOnConsole
    [DateTime] $StartTimeStamp
    [System.Collections.Generic.List[LogMessage]] $Messages
    hidden [String] $TimeZoneOffset

    ScriptLog([String] $Path, [String] $BaseName, [Boolean] $AppendDateTime, [ScriptLogType] $LogType, [ScriptLogMessageSeverity[]] $MessagesOnConsole) {
        $this.LogType = $LogType
        $this.MessagesOnConsole = $MessagesOnConsole
        $this.StartTimeStamp = Get-Date
        $this.Messages = [System.Collections.Generic.List[LogMessage]]::new()
        $Offset = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes
        if ($Offset -ge 0) {
            $this.TimeZoneOffset = "+$Offset"
        }
        else {
            $this.TimeZoneOffset = [string]"$Offset"
        }
        if ($LogType -eq 'Memory') {
            $this.FilePath = $null
        }
        else {
            $ConstructedPath = $Path + '\' + $BaseName
            if ($AppendDateTime) {
                $ConstructedPath += '-' + (Get-Date -Format 'yyyyMMddHHmmss')
            }
            switch ($LogType) {
                CMTrace {
                    $ConstructedPath += '.log'
                }
            }
            $this.FilePath = $ConstructedPath
        }
    }
}
#EndRegion './Classes/02-ScriptLog.ps1' 40
#Region './Private/00-Initialization.ps1' 0

$PSDefaultParameterValues.Clear()
Set-StrictMode -Version 3

# Prepare value defining the default ScriptLog to log messages to
$DefaultScriptLog = $null

# Prepare collection to hold ScriptLog objects
[System.Collections.Generic.List[ScriptLog]]$ScriptLogs = @()
#EndRegion './Private/00-Initialization.ps1' 10
#Region './Public/Get-ScriptLog.ps1' 0
function Get-ScriptLog {
    <#
        .SYNOPSIS
            Returns active ScriptLogs.

        .DESCRIPTION
            Returns a list of all active ScriptLogs.

        .EXAMPLE
            Get-ScriptLog

            Returns a list of all active ScriptLogs

        .EXAMPLE
            Get-ScriptLog -Default

            Returns the default ScriptLog

        .NOTES
            Author: kovergard
    #>

    [CmdletBinding()]
    [OutputType([ScriptLog[]])]
    Param (
        # If specified, return only the default ScriptLog
        [Parameter()]
        [switch]
        $Default
    )

    process {
        if ($Default) {
            if (-not $DefaultScriptLog) {
                Write-Warning 'No ScriptLogs exists, cannot return default ScriptLog'
                break
            }
            return $DefaultScriptLog
        }
        return $ScriptLogs
    }
}
#EndRegion './Public/Get-ScriptLog.ps1' 42
#Region './Public/New-ScriptLog.ps1' 0
function New-ScriptLog {
    <#
        .SYNOPSIS
            Returns a new ScriptLog object

        .DESCRIPTION
            Creates a new ScriptLog object with the settings provided and returns it through the pipeline so it can be used for logging during script execution using the Out-ScriptLog cmdlet.

        .EXAMPLE
            New-ScriptLog

            Create a new ScriptLog object with default settings. File will be created in the temp folder, with the name ScriptLog.log and will be written in the CMTrace format.

        .EXAMPLE
            $MemoryLog = New-ScriptLog -LogType Memory -MessagesOnConsole @("Error","Verbose")

            Create an in-memory SriptLog instance to allow for collection of log messages during runtime. Only errors and verbose messages will be written to the console (Warnings will not, they will only be written to the in-memory log)

        .EXAMPLE
            $CriticalFileLog = New-ScriptLog -Path "C:\Logs" -BaseName "CriticalErrors" -AppendDateTime; $VerboseLog = New-ScriptLog -Path "C:\Logs" -BaseName "Verbose" -MessagesOnConsole "Verbose"

            Create two separate ScriptLog objects to log messages in different formats to two different files.

        .NOTES
            Author: kovergard
    #>

    [CmdletBinding()]
    [OutputType([ScriptLog])]
    Param (
        # Directory in which to create the logfile
        [Parameter()]
        [string]
        $Path = $env:TEMP,

        # Name of the log file without extension
        [Parameter()]
        [string]
        $BaseName = 'ScriptLog',

        # Indicates if a datetime should be suffixed on the log base name.
        [Parameter()]
        [bool]
        $AppendDateTime = $false,

        # Type of log
        [Parameter()]
        [ScriptLogType]
        $LogType = 'CMTrace',

        # Determines which messages (if any) should be written to the console.
        [Parameter()]
        [ScriptLogMessageSeverity[]]
        $MessagesOnConsole = @('Error', 'Warning')
    )

    process {
        $NewScriptLog = [ScriptLog]::New($Path, $BaseName, $AppendDateTime, $LogType, $MessagesOnConsole)
        $Script:ScriptLogs.Add($NewScriptLog)
        if (-not $DefaultScriptLog) {
            Set-Variable -Name DefaultScriptLog -Value $NewScriptLog -Scope Script -Force
        }
        Write-Output $NewScriptLog
    }
}
#EndRegion './Public/New-ScriptLog.ps1' 65
#Region './Public/Out-ScriptLog.ps1' 0
function Out-ScriptLog {

    <#
        .SYNOPSIS
            Adds log messages to a ScriptLog.

        .DESCRIPTION
            Adds one or more log messages to a ScriptLog object. If multiple messages are sent via the pipleline, each message will get its own message entry in the log.

            A single messages can have multiple lines, these will be writting to the log file with line changes. If a message is longer than 7500 characters, it will be broken into multiple messages as longer messages will break the CMTrace format.

        .EXAMPLE
            Out-ScriptLog -Message "Starting script execution"

            Write a log message to the information channel in the default ScriptLog instance.

        .EXAMPLE
            Out-ScriptLog -Log $VerboseLog -Message "Starting script execution" -Severity Verbose

            Write a log message to the verbose channel in the ScriptLog $VerboseLog

        .EXAMPLE
            $Dir = Get-ChildItem -Path c:\temp; Out-ScriptLog -Message $Dir -Log $Log

            Write an object with multiple lines in it to the log file. This will be writtin as a single log message, since the message is not passed through the pipeline.

        .EXAMPLE
            "One","Two","Three" | Out-ScriptLog -Severity Warning

            Send multiple messages to the log using the pipeline. Each message will get its own log message.

        .NOTES
            Author: kovergard
    #>

    [CmdletBinding()]
    Param (
        # One or more messages to add to the log
        [Parameter(Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false)]
        $Message,

        # The ScriptLog object to add the messages to. If no ScriptLog is supplied, logging is done to the default ScriptLog.
        [Parameter()]
        [ScriptLog]
        $Log,

        # The severity of the messages
        [Parameter()]
        [ScriptLogMessageSeverity]
        $Severity = 'Information'
    )

    process {
        # Fail if no ScriptLogs exists
        if ($ScriptLogs.Count -eq 0) {
            throw 'Use New-ScriptLog to create a ScriptLog before using Out-ScriptLog'
        }

        # If no ScriptLog is specified, point to default ScriptLog.
        if (-not $PSBoundParameters.ContainsKey('Log')) {
            $Log = $DefaultScriptLog
        }

        # Convert message to string if necessary
        if ($Message.GetType() -ne 'System.String') {
            $Message = ($Message | Out-String).TrimEnd("`r`n")
        }

        # Determine log time and source of message
        $LogTime = Get-Date
        if ($Log.Source) {
            $Source = $Log.Source
            if ($MyInvocation.ScriptLineNumber) {
                $Source += ":$($MyInvocation.ScriptLineNumber)"
            }
        }
        else {
            Try {
                If ($MyInvocation.ScriptName) {
                    [string]$Source = "$(Split-Path -Path $MyInvocation.ScriptName -Leaf -ErrorAction 'Stop'):$($MyInvocation.ScriptLineNumber)"
                }
                Else {
                    $Source = 'interactive'
                }
            }
            Catch {
                $Source = 'unknown'
            }
        }

        # Get context and PID of message
        $Context = [Security.Principal.WindowsIdentity]::GetCurrent().Name
        $ProcessId = $global:PID

        # Add message to in-memory log.
        $Log.Messages.Add([LogMessage]::New($LogTime, $Severity, $Source, $Context, $ProcessId, $Message))

        # If message should be written to a file, convert to proper format and write.
        switch ($Log.LogType) {
            CMTrace {
                if ($Message.Length -gt 7500) {
                    $CMMessage = $Message.Substring(0, 7500)
                }
                else {
                    $CMMessage = $Message
                }
                $CmLogLine = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="{4}" type="{5}" thread="{6}" file="{7}">'
                $CmMessageType = Switch ($Severity) {
                    Error { 3 }
                    Warning { 2 }
                    Default { 1 }
                }
                $CmTime = ($LogTime | Get-Date -Format 'HH\:mm\:ss.fff').ToString() + $Log.TimeZoneOffset
                $CmDate = ($LogTime | Get-Date -Format 'MM-dd-yyyy')
                $CmFile = 'ScriptLog'
                $CmLogLineFormat = $CMMessage, $CmTime, $CmDate, $Source, $Context, $CmMessageType, $ProcessId, $CmFile
                $LogLine = $CmLogLine -f $CmLogLineFormat
                $LogLine | Out-File -FilePath $Log.FilePath -Append -Encoding utf8 -NoClobber
            }
        }

        # Write output to console, if applicable
        if ($Severity -in $Log.MessagesOnConsole) {
            Switch ($Severity) {
                Information {
                    Write-Information -MessageData $Message -InformationAction Continue
                }
                Verbose {
                    $VerbosePreference = 'Continue'; Write-Verbose -Message $Message
                }
                Warning {
                    Write-Warning -Message $Message
                }
                Error {
                    Write-Error -Message $Message
                }
            }
        }
    }
}
#EndRegion './Public/Out-ScriptLog.ps1' 144