CT.WriteLog.psm1

Write-Verbose 'Importing from [C:\Projects\ct-writelog\CT.WriteLog\private]'
# .\CT.WriteLog\private\Get-ModuleVariable.ps1
Function Get-ModuleVariable
{
    [cmdletbinding()]
    Param(
        # Path help description
        [Parameter(ValueFromPipeline)]
        [string]$Path = "$Script:ModuleBase\lib\Variables.csv",

        [string]$VariableName,

        [switch]$All
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($MyInvocation.MyCommand)"

        # In order to make modules easier to use, they can be pre-loaded with variables.
        # To avoid releasing confidential information, module-wide variables are stored
        # in a CSV file in the .\lib directory.
        # The Variables.csv file should contain the name, value, and scope for each variable
        # used by the commands within the module.
        # If the variable needs to be exposed to the user, the Scope should be set to global,
        # otherwise the variable will just be available to the module.
        # In my testing, there was no difference between variables in the local scope, versus
        # the script scope, but I didn't test that much.
        if (Test-Path -Path $Path) {
            $VariablesInCSV = Import-Csv -Path $Path
            foreach ($Item in $VariablesInCSV) {
                Write-Verbose "Parsing variables in $Path"

                # Remove the variable if it exists, to prevent re-creating globally scoped variables
                if (Get-Variable -Name ExpandedValue -ErrorAction SilentlyContinue) {
                    Remove-Variable -Name ExpandedValue -ErrorAction SilentlyContinue
                }

                # Convert string versions of true and false to boolean versions if needed
                if ($ExecutionContext.InvokeCommand.ExpandString($Item.Value) -in 'true','false') {
                    [boolean]$ExpandedValue = [System.Convert]::ToBoolean($ExecutionContext.InvokeCommand.ExpandString($Item.Value))
                } else {
                    $ExpandedValue = $ExecutionContext.InvokeCommand.ExpandString($Item.Value)
                }

                if (!$Item.Scope) {
                    $Scope = 'Script'
                } else {
                    $Scope = $Item.Scope
                }

                if (! (Get-Variable -Name $Item.VariableName -ErrorAction SilentlyContinue) ) {
                    Write-Verbose "Creating variable"
                    New-Variable -Name $Item.VariableName -Value $ExpandedValue -Scope $Scope
                }
            }

            if ($VariableName) {
                Write-Verbose "Found $VariableName"
                Get-Variable -Name $VariableName
            }

            if ($All) {
                Get-Variable | Where-Object {$_.Name -in $VariablesInCSV.VariableName}
            }
        }

    } #begin
} #close Get-ModuleVariable
# .\CT.WriteLog\private\Invoke-LogRotation.ps1
function Invoke-LogRotation {
    <#
        .SYNOPSIS
            Handle log rotation.
        .DESCRIPTION
            Invoke-LogRotation handles log rotation, using the log parameters defined in the log object.
            This function is called within the Write-Log function so that log rotation are invoked after
            each write to the log file.
        .NOTES
            Author: CleverTwain
            Date: 4.8.2018
            Version: 0.1.0
    #>

    [CmdletBinding()]
    param (
        # The log object created using the New-Log function. Defaults to reading the global PSLOG variable.
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullorEmpty()]
        [object] $Log = $Script:PSLOG
    )

    try {

        # get current size of log file
        $currentSize = (Get-Item $Log.Path).Length

        # get log name
        $logFileName = Split-Path $Log.Path -Leaf
        $logFilePath = Split-Path $Log.Path
        $logFileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($logFileName)
        $logFileNameExtension = [System.IO.Path]::GetExtension($logFileName)

        # if MaxLogFiles is 1 just keep the original one and let it grow
        if (-not($Log.MaxLogFiles -eq 1)) {
            if ($currentSize -ge $Log.MaxLogSize) {
                Write-Verbose 'We have hit the max log size'

                # construct name of archived log file
                $newLogFileName = $logFileNameWithoutExtension + (Get-Date -Format 'yyyyMMddHHmmss').ToString() + $logFileNameExtension

                # copy old log file to new using the archived name constructed above
                Copy-Item -Path $Log.Path -Destination (Join-Path (Split-Path $Log.Path) $newLogFileName)

                # set new empty log file
                if ([string]::IsNullOrEmpty($Log.Header)) {
                    Set-Content -Path $Log.Path -Value $null -Encoding 'UTF8' -Force
                }

                else {
                    Set-Content -Path $Log.Path -Value $Log.Header -Encoding 'UTF8' -Force
                }

                # if MaxLogFiles is 0 don't delete any old archived log files
                if (-not($Log.MaxLogFiles -eq 0)) {

                    Write-Verbose 'We shouldnt need to delete old log files...'

                    # set filter to search for archived log files
                    $archivedLogFileFilter = $logFileNameWithoutExtension + '??????????????' + $logFileNameExtension

                    # get archived log files
                    $oldLogFiles = Get-Item -Path "$(Join-Path -Path $logFilePath -ChildPath $archivedLogFileFilter)"

                    if ([bool]$oldLogFiles) {
                        # compare found log files to MaxLogFiles parameter of the log object, and delete oldest until we are
                        # back to the correct number
                        if (($oldLogFiles.Count + 1) -gt $Log.MaxLogFiles) {
                            Write-Verbose "Okay... maybe we do need to delete some old logs"
                            [int]$numTooMany = (($oldLogFiles.Count) + 1) - $log.MaxLogFiles
                            $oldLogFiles | Sort-Object 'LastWriteTime' | Select-Object -First $numTooMany | Remove-Item
                        }
                    }
                }
            }
        }
    }

    catch {
        Write-Warning $_.Exception.Message
    }
}
# .\CT.WriteLog\private\Set-ModuleVariable.ps1
Function Set-ModuleVariable
{
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Low')]
    Param(
        # VariableName help description
        [Parameter(ValueFromPipeline,Mandatory)]
        [string]$VariableName,

        [Parameter(Mandatory)]
        [string]$Value,

        # I don't know of a use case for a 'local' variable, so I didn't include it
        # We scope to script-level variables by default, as they will be available to functions within
        # the module, but they are not exposed to the user.
        # If there is a variable that needs to be exposed to the user, you should set the scope to global
        [ValidateSet("Global","Script")]
        [string]$Scope = 'Script',

        [string]$Path = "$Script:ModuleBase\lib\Variables.csv",

        [switch]$Force
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($MyInvocation.MyCommand)"

        $ExistingVariables = @()

        # If the file already exists...
        if ( Test-Path -Path $Path) {

            # Get a list of all the variables already in the CSV
            $ExistingVariables = Import-Csv -Path $Path
        } else {
            Write-Debug 'Variable file not found'
        }

        # Check if the variable already exists
        if ($ExistingVariables | Where-Object {$_.VariableName -eq $VariableName}) {
            Write-Debug "Updating existing variable"
            Write-Debug "$VariableName : $Value : $Scope"
            ($ExistingVariables | Where-Object {$_.VariableName -eq $VariableName}).Value = $Value
            ($ExistingVariables | Where-Object {$_.VariableName -eq $VariableName}).Scope = $Scope
        } else {
            Write-Debug "Creating new variable:"
            Write-Debug "$VariableName : $Value : $Scope"
            $ExistingVariables += [PSCustomObject]@{
                VariableName = $VariableName
                Value = $Value
                Scope = $Scope
            }
        }

        Write-Verbose "Trying to export variables to $Path"

        Try {
            $ExistingVariables | ConvertTo-Csv -NoTypeInformation | Out-File -FilePath $Path -Force:$Force -ErrorAction Stop
        } Catch {
            $_
        }

    } #begin
} #close Set-ModuleVariable
# .\CT.WriteLog\private\Write-MessageToHost.ps1
Function Write-MessageToHost
{
    [cmdletbinding()]
    Param(
        # LogEntry help description
        [Parameter(ValueFromPipeline)]
        [object]$LogEntry,

        [Parameter()]
        [ValidateSet('Error', 'FailureAudit', 'Information', 'SuccessAudit', 'Warning', 'Verbose', 'Debug')]
        [Alias('Type')]
        [string] $LogType = 'Information',

        $NoHostWriteBack
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"

        switch ($LogType) {
            'Error' {$cmType = '3'}
            'FailureAudit' {$cmType = '3'}
            'Information' {$cmType = '6'}
            'SuccessAudit' {$cmType = '4'}
            'Warning' {$cmType = '2'}
            'Verbose' {$cmType = '4'}
            'Debug' {$cmType = '5'}
            DEFAULT {$cmType = '1'}
        }



    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $LogEntry "

            switch ($cmType) {
                2 {
                    # Write the warning message back to the host
                    $WarningPreference = $PSCmdlet.GetVariableValue('WarningPreference')
                    Write-Warning -Message "$LogEntry"
                }

                3 {
                    if ($PSCmdlet.GetVariableValue('ErrorActionPreference') -ne 'SilentlyContinue' ) {
                        $ErrorActionPreference = $PSCmdlet.GetVariableValue('ErrorActionPreference')
                        $Host.Ui.WriteErrorLine("ERROR: $([String]$LogEntry.Exception.Message)")
                        Write-Error $LogEntry -ErrorAction ($PSCmdlet.GetVariableValue('ErrorActionPreference'))
                    }
                }

                4 {
                    # Write the verbose message back to the host
                    $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
                    Write-Verbose -Message "$LogEntry"
                }

                5 {
                    # Write the debug message to the Host.
                    $DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference')
                    Write-Debug -Message "$LogEntry"
                }

                default {
                    # Write the informational message back to the host.
                    if ($PSVersionTable.PSVersion -gt 5.0.0.0){
                        $InformationPreference = $PSCmdlet.GetVariableValue('InformationPreference')
                        Write-Information -MessageData "INFORMATION: $LogEntry"
                    } else {
                        # The information stream was introduced in PowerShell v5.
                        # We have to use Write-Host in earlier versions of PowerShell.
                        Write-Debug "We are using an older version of PowerShell. Reverting to Write-Output"
                        Write-Output "INFORMATION: $LogEntry"
                    }
                }#Information
            }

    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"



    } #end
} #close Write-LogToHost
Write-Verbose 'Importing from [C:\Projects\ct-writelog\CT.WriteLog\public]'
# .\CT.WriteLog\public\New-Log.ps1
function New-Log {
    <#
        .SYNOPSIS
            Creates a new log
        .DESCRIPTION
            The New-Log function is used to create a new log file or Windows Event log. A log object is also created
            and either saved in the global PSLOG variable (default) or sent to the pipeline. The latter is useful if
            you need to write to different log files in the same script/function.
        .EXAMPLE
            New-Log '.\myScript.log'
            Create a new log file called 'myScript.log' in the current folder, and save the log object in $PSLOG
        .EXAMPLE
            New-Log '.\myScript.log' -Header 'MyHeader - MyScript' -Append -CMTrace
            Create a new log file called 'myScript.log' if it doesn't exist already, and add a custom header to it.
            The log format used for logging by Write-Log is the CMTrace format.
        .EXAMPLE
            $log1 = New-Log '.\myScript_log1.log'; $log2 = New-Log '.\myScript_log2.log'
            Create two different logs that can be written to depending on your own internal script logic. Remember to pass the correct log object to Write-Log!
        .EXAMPLE
            New-Log -EventLogName 'PowerShell Scripts' -EventLogSource 'MyScript'
            Create a new log called 'PowerShell Scripts' with a source of 'MyScript', for logging to the Windows Event Log.
        .NOTES
            Author: CleverTwain
            Date: 4.8.2018
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium', DefaultParameterSetName = 'PlainText')]
    param (
        # Create or append to a Plain Text log file
        # Log Entry Example:
        # 03-22-2018 12:37:59.168-240 INFORMATION: Generic Log Entry
        [Parameter(
            ParameterSetName = 'PlainText',
            Position = 0
        )]
        [switch]$PlainText,

        # Create or append to a Minimal log file
        # Log Entry Example:
        # Generic Log Entry
        [Parameter(
            ParameterSetName = 'Minimal',
            Position = 0
        )]
        [switch]$Minimal,

        # Create or append to a CMTrace log file
        [Parameter(
            ParameterSetName = 'CMTrace',
            Position = 0
        )]
        [switch]$CMTrace,

        # Create or append to a Windows Event Log
        [Parameter(
            ParameterSetName = 'EventLog',
            Position = 0
        )]
        [switch]$EventLog,

        # Select which type of log to create or append to
        # The format of the log file. Valid choices are 'Minimal', 'PlainText' and 'CMTrace'.
        # The 'Minimal' format will just pass the log entry to the log file, while the 'PlainText' includes meta-data.
        # CMTrace format are viewable using the CMTrace.exe tool.

        # Path to log file.
        [Parameter(
            ParameterSetName = 'PlainText',
            Position = 1)]
        [ValidateNotNullorEmpty()]
        [Parameter(
            ParameterSetName = 'Minimal',
            Position = 1)]
        [ValidateNotNullorEmpty()]
        [Parameter(
            ParameterSetName = 'CMTrace',
            Position = 1)]
        [string] $Path = "$env:TEMP\$(Get-Date -Format FileDateTimeUniversal).log",

        # Optionally define a header to be added when a new empty log file is created.
        # Headers only apply to files, and do not apply to CMTrace files
        [Parameter(
            ParameterSetName = 'PlainText',
            Mandatory = $false,
            Position = 2)]
        [Parameter(
            ParameterSetName = 'Minimal',
            Mandatory = $false,
            Position = 2)]
        [string]$Header,

        # If log file already exist, append instead of creating a new empty log file.
        [Parameter(
            ParameterSetName = 'PlainText')]
        [Parameter(
            ParameterSetName = 'Minimal')]
        [Parameter(
            ParameterSetName = 'CMTrace')]
        [switch] $Append,

        # Maximum size of log file.
        [Parameter(
            ParameterSetName = 'PlainText'
            )]
        [Parameter(
            ParameterSetName = 'Minimal'
            )]
        [Parameter(
            ParameterSetName = 'CMTrace'
            )]
        [int64] $MaxLogSize = 5242880, # in bytes, default is 5242880 = 5 MB

        # Maximum number of log files to keep. Default is 3. Setting MaxLogFiles to 0 will keep all log files.
        [Parameter(
            ParameterSetName = 'PlainText'
            )]
        [Parameter(
            ParameterSetName = 'Minimal'
            )]
        [Parameter(
            ParameterSetName = 'CMTrace'
            )]
        [ValidateRange(0,99)]
        [int32] $MaxLogFiles = 3,

        # Specifies the name of the event log.
        [Parameter(
            ParameterSetName = 'EventLog',
            Position = 3)]
        [string] $EventLogName = 'CT.WriteLog',

        # Specifies the name of the event log source.
        [Parameter(
            ParameterSetName = 'EventLog')]
        [string] $EventLogSource,

        # Define the default Event ID to use when writing to the Windows Event Log.
        # This Event ID will be used when writing to the Windows log, but can be overrided by the Write-Log function.
        [Parameter(
            ParameterSetName = 'EventLog')]
        [string] $DefaultEventID = '1000',

        # When UseLocalVariable is True, the log object is not saved in the global PSLOG variable,
        # otherwise it's returned to the pipeline.
        [Parameter()]
        [switch] $UseLocalVariable,

        # Messages written via Write-Log are also written back to the host by default. Specifying this option, disables
        # that functionality for the log object.
        [Parameter()]
        [switch] $NoHostWriteBack,

        # When writing to the log, the data stream name (DEBUG, WARNING, VERBOSE, etc.) is not included by default.
        # Specifying this option will include the stream name in all Write-Log messages.
        [Parameter()]
        [switch] $IncludeStreamName
    )

    if ($PSCmdlet.ParameterSetName -eq 'EventLog') {
        $LogType = 'EventLog'
    } else {
        $LogType = 'LogFile'
        $LogFormat = $PSCmdlet.ParameterSetName
    }

    if ($LogType -eq 'EventLog') {

        if (!$EventLogSource) {
            if ( (Get-PSCallStack)[1].FunctionName ) {
                $EventLogSource = (Get-PSCallStack)[1].FunctionName
            } else {
                $EventLogSource = (Get-PSCallStack)[1].Command
            }
            if ($EventLogSource = '<ScriptBlock>') {
                $EventLogSource = 'ScriptBlock'
            }
        }

        if ([System.Diagnostics.EventLog]::SourceExists($EventLogSource)) {
            $AssociatedLog = [System.Diagnostics.EventLog]::LogNameFromSourceName($EventLogSource,".")

            if ($AssociatedLog -ne $EventLogName) {
                Write-Warning "The eventlog source $EventLogSource is already associated with a different eventlog"
                $LogType = $null
                return $null
            }
        }

        try {
            if (-not([System.Diagnostics.EventLog]::SourceExists($EventLogSource))) {

                # In order to create a new event log, or add a new source to an existing eventlog,
                # the user must be running the command as an administrator.
                # We are checking for that here
                $windowsIdentity=[System.Security.Principal.WindowsIdentity]::GetCurrent()
                $windowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($windowsIdentity)
                $adm=[System.Security.Principal.WindowsBuiltInRole]::Administrator
                if ($windowsPrincipal.IsInRole($adm)) {
                    Remove-Variable -Name Format,MaxLogSize,MaxLogFiles -ErrorAction SilentlyContinue
                    # create new event log if needed
                    New-EventLog -Source $EventLogSource -LogName $EventLogName
                    Write-Verbose "Created new event log (Name: $($EventLogName), Source: $($EventLogSource))"
                }

                else {
                    Write-Warning 'When creating a Windows Event Log you need to run as a user with elevated rights!'
                }
            }
            else {
                Write-Verbose "$($EventLogName) exists, skip create new event log."
            }

            $logType = 'EventLog'
        }
        catch {
            Write-Warning $_.Exception.Message
        }
    }

    else {
        Remove-Variable -Name EventLogName,EventLogSource,DefaultEventID -ErrorAction SilentlyContinue

        $Counter = 0
        $HaveLog = $false
        While (-not $HaveLog) {
            $Mutex = New-Object System.Threading.Mutex($false, "LoggingMutex")
            Write-Debug "Requesting mutex to test access to log"
            [void]$Mutex.WaitOne(1000)
            Write-Debug "Received Mutex to test access to log"
            Try {
                [io.file]::OpenWrite($Path).close()
                $HaveLog = $true
            }
            Catch [System.UnauthorizedAccessException] {
                $FileName = $Path.Split("\") | Select-Object -Last 1
                $Path = "$env:TEMP\$FileName"
                Write-Warning "Current user does not have permission to write to $Path. Redirecting log to $Path"
                $HaveLog = $true
            } Catch {
                $Counter++
                Write-Debug $_
            } Finally {
                Write-Debug "Releasing Mutex to access log"
                [void]$Mutex.ReleaseMutex()
            }
            if ($Counter -gt 99) {
                $HaveLog = $false
                Write-Error "Unable to obtain lock on file"
                $logType = $null
                return $null
            }
        }

        # create new log file if needed ( we need to re-check if the file exists here because the
        # path may have changed since we last checked)
        if((-not $Append) -or (-not(Test-Path $Path))){
            Write-Verbose "Log does not currently exist, or we are overwriting an existing log"
            try {
                if($Header){
                    Set-Content -Path $Path -Value $Header -Encoding 'UTF8' -Force
                }
                else{
                    Set-Content -Path $Path -Value $null -Encoding 'UTF8' -Force
                }
                Write-Verbose "Created new log file ($($Path))"
            }
            catch{
                Write-Warning $_.Exception.Message
            }
        }
    }

    Write-Verbose "Creating Log Object"
    # create log object
    switch ($LogType) {
        'EventLog' {
            # create log object
            $logObject = [PSCustomObject]@{
                PSTypeName = 'CT.EventLog'
                Type = $logType
                Name = $EventLogName
                Source = $EventLogSource
                DefaultEventID = $DefaultEventID
                IncludeStreamName = $IncludeStreamName
                HostWriteBack = (!$NoHostWriteBack)
                MaxLogSize = $MaxLogSize
                # Limit-EventLog
                # Minimum 64KB Maximum 4GB and must be divisible by 64KB (65536)
                MaxLogRetention = $MaxLogRetention
                # Limit-EventLog
                # RetentionDays
                OverflowAction = $OverflowAction
                # Limit-EventLog
                # OverwriteOlder, OverwriteAsNeeded, DoNotOverwrite
            }
        }
        'LogFile' {
            $logObject = [PSCustomObject]@{
                PSTypeName = 'CT.LogFile'
                Type = $logType
                Path = $Path
                Format = $LogFormat
                Header = $Header
                IncludeStreamName = $IncludeStreamName
                HostWriteBack = (!$NoHostWriteBack)
                MaxLogSize = $MaxLogSize
                MaxLogFiles = $MaxLogFiles
            }
        }
        default {$logObject = $null}
    }

    # Return the log to the pipeline

    if ($UseLocalVariable) {
        Write-Output $logObject
    } else {
        if (Get-Variable PSLog -ErrorAction SilentlyContinue) {
            Remove-Variable -Name PSLOG
        }
        New-Variable -Name PSLOG -Value $logObject -Scope Script
    }
}
# .\CT.WriteLog\public\Show-Log.ps1
Function Show-Log
{
    <#
        .SYNOPSIS
            Shows a log
        .DESCRIPTION
            The Show-Log function is used to display a log file, event log, or log object.
        .EXAMPLE
            Show-Log '.\myScript.log'
            Create a new log file called 'myScript.log' in the current folder, and save the log object in $PSLOG
        .EXAMPLE
            New-Log '.\myScript.log' -Header 'MyHeader - MyScript' -Append -CMTrace
            Create a new log file called 'myScript.log' if it doesn't exist already, and add a custom header to it.
            The log format used for logging by Write-Log is the CMTrace format.
        .EXAMPLE
            $log1 = New-Log '.\myScript_log1.log'; $log2 = New-Log '.\myScript_log2.log'
            Create two different logs that can be written to depending on your own internal script logic. Remember to pass the correct log object to Write-Log!
        .EXAMPLE
            New-Log -EventLogName 'PowerShell Scripts' -EventLogSource 'MyScript'
            Create a new log called 'PowerShell Scripts' with a source of 'MyScript', for logging to the Windows Event Log.
        .NOTES
            Author: CleverTwain
            Date: 4.8.2018
    #>

    [cmdletbinding()]
    Param(
        # Log object to show
        [Parameter(ValueFromPipeline)]
        [object]$LogObject = $Script:PSLOG,

        # Event log to show
        [string]$EventLog,

        # File log to show
        [string]$Path
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"

    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $LogObject "

        if ($Path -or $LogObject.Type -eq 'LogFile') {
            Write-Verbose "Working with a logfile"
            if ($LogObject.Path) {
                $Path = $LogObject.Path
            }
            Write-Verbose "Trying to invoke-item $Path"
            Invoke-Item $Path
        }

        if ($EventLog -or $LogObject.Type -eq 'EventLog') {
            Write-Verbose "Working with event log"
            if ($LogObject.Name) {
                $EventLog = $LogObject.Name
            }
            Write-Verbose "Trying to EventVwr.exe /c:'$EventLog'"
            EventVwr.exe /c:"$EventLog"
        }

            # If the log is a file, invoke-item on it. That should just open the file with
            # the users preferred log viewer

            <#
            You can get the properties of the event log file itself by typing
            WevtUtil.exe Get-Log "$EventLogName" /format:xml
            Here is how you can get the location

            [xml]$EventLog = WevtUtil.exe Get-Log "$EventLogName" /format:xml
            $LogPath = $EventLog.Channel.Logging.LogFileName

            ---
            The log can be pulled up directly by running:
            EventVwr.exe /c:"$EventLogName"

            (Except on my machine, because it is a piece of crap!)
            #>


    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"



    } #end
} #close Show-Log
# .\CT.WriteLog\public\Write-Log.ps1
Function Write-Log {
    <#
        .SYNOPSIS
            Write to the log and back to the host by default.
        .DESCRIPTION
            The Write-Log function is used to write to the log and by default, also write back to the host.
            It is using the log object created by New-AHGLog to determine if it's going to write to a log file
            or to a Windows Event log. Log files can be created in multiple formats
        .EXAMPLE
            Write-AHGLog 'Finished running WMI query'
            Get the log object from $PSLOG and write to the log.
        .EXAMPLE
            $myLog | Write-AHGLog 'Finished running WMI query'
            Use the log object saved in $myLog and write to the log.
        .EXAMPLE
            Write-AHGLog 'WMI query failed - Access denied!' -LogType Error -PassThru | Write-Warning
            Will write an error to the event log, and then pass the log entry to the Write-Warning cmdlet.
        .NOTES
            Author: CleverTwain
            Date: 4.8.2018
            Dependencies: Invoke-AHGLogRotation
    #>


    [cmdletbinding()]
    param (
        # The text you want to write to the log.
        # Not limiting this to a string, as this function can catch exceptions as well
        [Parameter(Position = 0)]
        [Alias('Message')]
        $LogEntry,

        # The type of log entry. Valid choices are 'Error', 'FailureAudit','Information','SuccessAudit' and 'Warning'.
        # Note that the CMTrace format only supports 3 log types (1-3), so 'Error' and 'FailureAudit' are translated to CMTrace log type 3, 'Information' and 'SuccessAudit'
        # are translated to 1, while 'Warning' is translated to 2. 'FailureAudit' and 'SuccessAudit' are only really included since they are valid log types when
        # writing to the Windows Event Log.
        [Parameter()]
        [ValidateSet('Error', 'FailureAudit', 'Information', 'SuccessAudit', 'Warning', 'Verbose', 'Debug')]
        [Alias('Type')]
        [string] $LogType = 'Information',

        # Include the stream name in the log entry when writing CMTrace logs
        [Parameter()]
        [switch]$IncludeStreamName,

        # Do not write the message back to the host via the specified stream
        # By default, log entries are written back to the host via the specified data stream.
        [Parameter()]
        [switch]$NoHostWriteBack,

        # Event ID. Only applicable when writing to the Windows Event Log.
        [Parameter()]
        [string] $EventID,

        # The log object created using the New-Log function. Defaults to reading the PSLOG variable.
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullorEmpty()]
        [Alias('LogFile')]
        [object] $Log = $Script:PSLOG,

        # PassThru passes the log entry to the pipeline for further processing.
        [Parameter()]
        [switch] $PassThru
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"

        if ( (!$Log.HostWriteBack) -or ($NoHostWriteBack)) {
            $NoHostWriteBack = $true
        }

        if ( ($Log.IncludeStreamName) -or ($IncludeStreamName)) {
            $IncludeStreamName = $true
        }

        # An attribute of the Log object will be flagged if we do not have appropriate permissions.
        # If that attribute is flagged, we should still write to the screen if appropriate


    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $LogEntry "

        try {

            # get information from log object
            $logObject = $Log

            Write-Verbose "Received the log object of type $($logObject.Type)"

            if ($logObject.Format) {
                Write-Debug "LogFormat: $($LogObject.Format)"
            }

            # translate event types to CMTrace types, and gather information for error
            if ($logObject.Format -eq 'CMTrace' -or $logObject.Type -eq 'EventLog') {
                switch ($LogType) {
                    'Error' {
                        $cmType = '3'

                        #Get the info about the calling script, function etc
                        $CallingInfo = (Get-PSCallStack)[1]

                        if (!$LogEntry.Exception.Message) {
                            [System.Exception]$Exception = $LogEntry
                            [String]$ErrorID = 'Custom Error'
                            [System.Management.Automation.ErrorCategory]$ErrorCategory = [Management.Automation.ErrorCategory]::WriteError
                            $ErrorRecord = New-Object Management.automation.errorrecord ($Exception, $ErrorID, $ErrorCategory, $LogEntry)
                            $LogEntry = $ErrorRecord

                            $LogEntry =
                            "$([String]$LogEntry.Exception.Message)`r`r`n" +
                            "`nFunction: $($Callinginfo.FunctionName)" +
                            "`nScriptName: $($Callinginfo.Scriptname)" +
                            "`nLine Number: $($Callinginfo.ScriptLineNumber)" +
                            "`nColumn Number: $($Callinginfo.Position.StartColumnNumber)" +
                            "`nLine: $($Callinginfo.Position.StartScriptPosition.Line)"
                        } else {
                            $LogEntry =
                            "$([String]$LogEntry.Exception.Message)`r`r`n" +
                            "`nCommand: $($LogEntry.InvocationInfo.MyCommand)" +
                            "`nScriptName: $($LogEntry.InvocationInfo.Scriptname)" +
                            "`nLine Number: $($LogEntry.InvocationInfo.ScriptLineNumber)" +
                            "`nColumn Number: $($LogEntry.InvocationInfo.OffsetInLine)" +
                            "`nLine: $($LogEntry.InvocationInfo.Line)"
                        }

                    }
                    'FailureAudit' {$cmType = '3'}
                    'Information' {$cmType = '6'}
                    'SuccessAudit' {$cmType = '4'}
                    'Warning' {$cmType = '2'}
                    'Verbose' {$cmType = '4'}
                    'Debug' {$cmType = '5'}
                    DEFAULT {$cmType = '1'}
                }
                Write-Debug "$LogType : $cmType"
            }

            if ($logObject.Type -eq 'EventLog') {
                # if EventID is not specified use default event id from the log object
                if([system.string]::IsNullOrEmpty($EventID)) {
                    $EventID = $logObject.DefaultEventID
                }

                if ($LogType -notin ('Error','FailureAudit','SuccessAudit','Warning')) {
                    $LogType = 'Information'
                }

                $LogEntryString = $LogEntry
                Write-Verbose "LogEntryString: $LogEntryString"
                Write-Verbose "lo.name: $($logObject.Name)"
                Write-Verbose "lo.Source: $($logObject.Source)"
                Write-Verbose "EntryType: $($LogType)"
                Write-Verbose "EventId: $($EventID)"

                Write-Verbose 'Trying to write to the event log'
                Write-EventLog -LogName $logObject.Name -Source $logObject.Source -EntryType $LogType -EventId $EventID -Message $LogEntryString -Verbose
            }

            else {
                $DateTime = New-Object -ComObject WbemScripting.SWbemDateTime
                $DateTime.SetVarDate($(Get-Date))
                $UtcValue = $DateTime.Value
                $UtcOffset = $UtcValue.Substring(21, $UtcValue.Length - 21)

                $Date = Get-Date -Format 'MM-dd-yyyy'
                $Time = "$(Get-Date -Format HH:mm:ss.fff)$($UtcOffset)"

                # handle the different log file formats
                switch ($logObject.Format) {

                    'Minimal' { $logEntryString = $LogEntry}

                    'PlainText' {
                        $logEntryString = "$Date $Time"
                        if ($IncludeStreamName) {
                            $LogEntryString = "$logEntryString $($LogType.ToUpper()):"
                        }
                        $LogEntryString = "$LogEntryString $($LogEntry)"
                    }

                    'CMTrace' {

                        # Get invocation information about the script/function/module that called us
                        $thisInvocation = (Get-Variable -Name 'MyInvocation' -Scope 2).Value

                        # get calling script info
                        if(-not ($thisInvocation.ScriptName)){
                            $scriptName = $thisInvocation.MyCommand
                            $Source = "$($scriptName)"
                        }
                        else{
                            $scriptName = Split-Path -Leaf ($thisInvocation.ScriptName)
                            $Source = "$($scriptName):$($thisInvocation.ScriptLineNumber)"
                        }

                        # get calling command info
                        $component = "$($thisInvocation.MyCommand)"

                        $Context = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name

                        $Source = (Get-PSCallStack)[1].Location

                        if ( (Get-PSCallStack)[1].FunctionName ) {
                            $Component = (Get-PSCallStack)[1].FunctionName
                        } else {
                            $Component = (Get-PSCallStack)[1].Command
                        }

                        #Set Component Information
                        if ($Source -eq '<No file>') {
                            $Source = (Get-Process -Id $PID).ProcessName
                        }

                        $Line = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="{4}" type="{5}" thread="{6}" file="{7}">'
                        $LineFormat = $LogEntry, $Time, $Date, $Component, $Context, $CMType, $PID, $Source
                        $logEntryString = $Line -f $LineFormat

                        Write-Debug "$logEntryString"
                    }
                }
                $PendingWrite = $true
                $Counter = 0
                $MaxLoops = 100
                <#
                While ($PendingWrite -and ($Counter -lt $MaxLoops)) {
                    $Counter++
                    try {
                        # create a mutex, so we can lock the file while writing to it
                        $mutex = New-Object System.Threading.Mutex($false, 'LogMutex')
                        # write to the log file
                        Add-Content -Path $logObject.Path -Value $logEntryString -ErrorAction Stop
                        $PendingWrite = $false
                        $mutex.ReleaseMutex()
                    } catch {
                        [void]$mutex.WaitOne()
                    } finally {
                        if ($Counter -eq $MaxLoops) {
                            Write-Warning "Unable to gain lock on file at $($logObject.Path)"
                        }
                    }
                }
                #>

                While ($PendingWrite -and ($Counter -lt $MaxLoops)) {
                    $Mutex = New-Object System.Threading.Mutex($false, "LoggingMutex")
                    Write-Debug "Requesting mutex to write to log"
                    [void]$Mutex.WaitOne(1000)
                    Write-Debug "Received Mutex to write to log"
                    Try {
                        Add-Content -Path $logObject.Path -Value $logEntryString -ErrorAction Stop
                        $PendingWrite = $false
                    } Catch {
                        $Counter++
                        Write-Debug $_
                    } Finally {
                        Write-Debug "Releasing Mutex to access log"
                        [void]$Mutex.ReleaseMutex()
                    }
                }



                # invoke log rotation if log is file
                if ($logObject.LogType -eq 'LogFile') {
                    $logObject | Invoke-LogRotation
                }
            }
        }

        catch {
            Write-Warning $_.Exception.Message
        }

    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"

        if (!$NoHostWriteBack) {
            Write-Verbose "Writing message back to host"
            Write-MessageToHost -LogEntry $LogEntryString -LogType $LogType
        }

        # handle PassThru
        if ($PassThru) {
            Write-Output $LogEntry
        }

    } #end
} #close Write-Log
Write-Verbose 'Importing from [C:\Projects\ct-writelog\CT.WriteLog\classes]'