private/write-loghandler.ps1

function write-loghandler {
    [CmdletBinding(DefaultParameterSetName='Default')]
    param (
        # Message to log
        [Parameter(Mandatory)]
        [String]$message,

        # Parameter help description
        [Parameter()]
        [ValidateSet("Debug","Verbose","Info","Warning","Error")]
        [String]
        $Level,

        [Parameter(ParameterSetName="ShouldProcess", Mandatory)]
        [parameter(ParameterSetName="Default")]
        $Target,

        # Stopwatch object for advanced logging. If provided, timestamp will be since initialization of stopwatch
        [Parameter()]
        [System.Diagnostics.Stopwatch]
        $stopwatch,

        # Normally the calling function script and line are appended to log. This suppresses that.
        [Parameter()]
        [switch]
        $SuppressCaller,

        # Omit the timestamp from output. If absent, timestamp will be added to start of line
        [Parameter()]
        [switch]
        $suppressTimestamp,

        # Adds indentation after timestamp to visually represent sub-tasks
        [Parameter()]
        [int]
        $IndentLevel,

        # Outputs an array that can be consumed by PSCmdlet.shouldProcess.invoke(), to avoid double messages but still allow formatting
        [Parameter(ParameterSetName="ShouldProcess", Mandatory)]
        [Switch]
        $Passthru
    )

    begin {
        $hostWidth = $(if ([Console]::WindowWidth) {[Console]::WindowWidth} else {100})
        $LogLevels = [ordered]@{
            'debug' = @{
                Index = 0
                Name = "debug"
                Friendly = "DEBUG"
                Style = $psstyle.foreground.FromConsoleColor("DarkGreen")
                Enabled = $DebugPreference.toString().toLower() -ne "silentlyContinue"
                AllowConsoleCallerTrim = $False
            }
            'verbose' = @{
                Index = 1
                Name = "verbose"
                Friendly = "VERB"
                Style = $psstyle.foreground.FromConsoleColor("Cyan")
                Enabled = $VerbosePreference.toString().toLower() -ne "silentlyContinue"
                AllowConsoleCallerTrim = $true
            }
            'whatif' = @{
                Index = 2
                Name = "whatif"
                Friendly = "WHATIF"
                Style = $psstyle.foreground.FromConsoleColor("Blue")
                Enabled = $WhatIfPreference.IsPresent
                AllowConsoleCallerTrim = $True
            }
            'confirm' = @{
                Index = 3
                Name = "confirm"
                Friendly = "CONFRM"
                Style = $PSStyle.Formatting.Warning
                Enabled = (-not ($ConfirmPreference -eq "high" -or $ConfirmPreference -eq "None"))
                AllowConsoleCallerTrim = $True
            }
            'info' = @{
                Index = 4
                Name = "info"
                Friendly = ""
                Style = $PSStyle.reset
                Enabled = $true
                AllowConsoleCallerTrim = $True
            }
            'warning' = @{
                Index = 5
                Name = "warning"
                Friendly = "WARN"
                Style = $psstyle.foreground.FromConsoleColor("Magenta")
                Enabled = $WarningPreference.toString().toLower() -ne "silentlycontinue"
                AllowConsoleCallerTrim = $True
            }
            'error' = @{
                Index = 6
                Name = "error"
                Friendly = "ERROR"
                Style = $PSStyle.Formatting.error
                Enabled = $ErrorActionPreference.toString().toLower() -ne "silentlycontinue"
                AllowConsoleCallerTrim = $True
            }
        }
        # Find chattiest log level enabled. We'll output to any at that level and above.
        $LowestPreferedLogLevel = @($LogLevels.values.enabled.GetEnumerator()).indexOf($true)
        if ($LowestPreferedLogLevel -ge $logLevels['info'].index) {
            # ShouldProcess doesn't display anything here, so we may want to still write a console message out (depending on level )
            $ShouldProcessWillOutput = $false
        } else {
            $ShouldProcessWillOutput = $true
        }
        $indent = " . "
        $ConfirmLevels = @(
            "low"
            "medium"
            "high"
        )
    }

    process {
        if ($target) {
            if ($target.getType() -eq [string]) {
                $targetString = $target
            } else {
                $targetString = $target.getString()
            }
        } else {
            $targetString = ""
        }
          if (-not $PSCmdlet.MyInvocation.psCommandPath -or $PSCmdlet.MyInvocation.CommandOrigin -eq "Runspace") {
            $CallerInfo = @{
                Line = $PSCmdlet.MyInvocation.ScriptLineNumber
                ScriptFullName = "InsideTheMatrix"
                ScriptRelativePath = "InsideTheMatrix"
                ScriptName = "Matrix"
                FullPath = "InsideTheMatrix"
                ModuleBase = ""
                Runspace = $true
            }

            $id = 20
        } else {
            $CallerInfo = @{
                Line = $PSCmdlet.MyInvocation.ScriptLineNumber
                ScriptFullName = $PSCmdlet.MyInvocation.Scriptname
                ScriptRelativePath = $PSCmdlet.MyInvocation.Scriptname.replace($PSCmdlet.myinvocation.mycommand.module.modulebase,'.')
                ScriptName = $PSCmdlet.MyInvocation.Scriptname.replace($PSCmdlet.myinvocation.PSScriptRoot,"") -replace "^[/\\]",""
                FullPath = $PSCmdlet.myinvocation.PSScriptRoot
                ModuleBase = $PSCmdlet.myinvocation.mycommand.module.modulebase
                Runspace = $false
            }
            $id = 9
        }


        $operativeLevel = if ($passthru) {
            if ($callerInfo['Runspace']) {
                # Passthrough Checking call stack can break things, and they're non-interactive anyways
                $CallerConfirmImpact = 'none'
            } else {
                $CallerConfirmImpact = [system.management.automation.CommandMetadata]::new($(get-command (get-pscallstack)[1].Command)).confirmImpact.toString().toLower()
            }
            if ($WhatIfPreference.IsPresent) {
                $LogLevels["whatif"]
            } elseif ($CallerConfirmImpact -ne "none" -and
                    $Loglevels["confirm"].enabled -and
                    $ConfirmLevels.IndexOf($CallerConfirmImpact) -ge $confirmLevels.IndexOf($confirmPreference.toString().toLower() )) {
                        $LogLevels["confirm"]
            } elseif ($level) {
                $LogLevels[$($level.toLower())]
            } else {
                $LogLevels["verbose"]
            }
        } elseif ($level) {
            $LogLevels[$($level.toLower())]
        } else {
            $LogLevels["info"]
        }




        # We'll come up with a log level from whatif verbose etc, assign it a number, and compare to the index of the selected level


        if ($ProgressPreference -ne "SilentlyContinue") {
            $ProgressCaller = "{0}:{1}" -f $CallerInfo['ScriptRelativePath'], $CallerInfo['Line']
            $progressMsg = $message -replace "[`r`n]",""
            write-progress -id $id -Activity $ProgressCaller  -Status $progressMsg -CurrentOperation $targetString
        }
        # This may need to be changed if file logging is on.
        if ($operativeLevel['Index'] -ge $LowestPreferedLogLevel -or $passthru) {
            $msgTime = $(
                if (-not $suppressTimestamp ) {
                    $timePrefix = if ($stopwatch.IsRunning){
                        $sw.elapsed.toString("hh\:mm\:ss\.fff")
                    } else {
                        [System.DateTime]::now.toString("HH:mm:ss.fff")
                    }
                    "{0,12} " -f $timePrefix
                }else {
                    ""
                }
            )
            $msgindent = $(for ($i=1; $i -le $indentLevel; $i++) {$indent}) -join ""

            # If there are no control characters at beginning of the line, we use a carriage return to allow rewriting 'verbose' etc messages.
            # If there are control characters at beginning or end of the message, we'll bump them before / after our prefix / suffix
            # This gets overwritten if there are start-of-line chars.
            $eol = ""
            $sol = ""
            $FixedMsg = $message
            if ($FixedMsg -match '[\r\n]') {
                # Cut \r and \n sequences at beginning of line for later use
                if ($FixedMsg -match '^([\r\n]+)') {
                    $sol=$matches[0]
                    $FixedMsg = $FixedMsg -replace '^([\r\n]+)',''
                }

                # Cut \r and \n sequences at end of line for later use
                if ($FixedMsg -match '([\r\n]+)$') {
                    # TODO: This seems to work badly, so disabling for now-- table-formatted output generates a ton of EOLs
                    #$eol=$matches[0]
                    $FixedMsg = $FixedMsg -replace '([\r\n]+)$',''
                }
            } else {
                if ($passthru -and $operativeLevel.name -ne "confirm" -and $target ) {
                    # Confirm levels on passthru already get displayed, but otherwise lets stick this in the string.
                    $FixedMsg = "{0} @ {1}" -f $fixedMsg, $targetString
                }
            }


            $startOfConsoleLine = "$sol`r"
            $LinePrefix = "{0,13}{1,-6} {2}" -f $msgTime, $operativeLevel['Friendly'], $msgIndent
            $FullCaller = " ({0}:{1})" -f $CallerInfo['ScriptRelativePath'], $CallerInfo['Line']
            if ($suppressCallerInfo) {
                $msgCall = ""
            } else {
                $msgCall = $FullCaller
            }


            #This is the basic line without eol / sol characters
            $LogLineNoCRLF = "{0}{1}{2}" -f $linePrefix, $fixedMsg, $msgCall

            # For console only: try to fix-up the length of messages so it doesn't wrap. Don't do this if there are still control characters
            $ConsoleLengthDeficit = $logLineNoCRLF.length - $HostWidth
            $consoleCaller = $msgCall
            if ($ConsoleLengthDeficit -gt 0 -and ($fixedMsg -notMatch '[\r\n]')) {
                $SpaceAfterLinePrefix = ($HostWidth) - $linePrefix.length
                if ($operativeLevel['AllowConsoleCallerTrim'] -and -not $suppressCaller -and $msgCall.length -gt 0) {
                    # Start by trimming the caller
                    $ShortCaller = " ({0}:{1})" -f $CallerInfo['ScriptName'], $CallerInfo['Line']
                    $ShortenedCallerSavings = $msgCall.length - $ShortCaller.length
                    $MaxAllowedSpaceForCaller = [math]::Floor($SpaceAfterLinePrefix * 0.30)
                    if ( $ShortenedCallerSavings -gt 0 -and $shortCaller.length -le $MaxAllowedSpaceForCaller) {
                        $consoleCaller = $shortCaller
                    } else {
                        # It's too long, remove it entirely.
                        $consoleCaller = ""
                    }
                    $ConsoleLengthDeficit = $ConsoleLengthDeficit - ($msgCall.length - $ConsoleCaller.Length)
                }
                if ($ConsoleLengthDeficit -gt 0) {
                    # "Using a shortened caller Didn't solve the problem, we will need to trim the message"
                    $maxMsgWidth = $SpaceAfterLinePrefix - $consoleCaller.length -3
                    if ($fixedMsg.length -gt $maxMsgWidth -and $maxMsgWidth -gt 0) {
                        $fixedMsg = "{0}..." -f $fixedMsg.substring(0,$maxMsgWidth)
                    }
                }
            }
            if ($consoleCaller.length -gt 0) {
                $ConsoleCallerIndent = $HostWidth - ($linePrefix.length + $fixedMsg.length)
                $ConsoleCaller = "{0,$consoleCallerIndent}" -f $consoleCaller
            }

            $ConsolePrefix = "{0}{1}{2}" -f $startOfConsoleLine, $operativeLevel['Style'], $linePrefix

            # Some return types don't need / want caller (e.g. error), and sometimes its not easy to tell ahead of time (e.g. is there an error object)
            $ConsoleShortMessage = "{0}{1}{2}" -f $consolePRefix, $fixedMsg, $consoleEOL, $eol,$PSStyle.Reset
            $ConsoleMessage = "{0}{1}{2}{3}" -f $consolePRefix, $fixedMsg, $consoleCaller, $eol,$PSStyle.Reset

            if (-not $ShouldProcessWillOutput -and $operativeLevel['Index'] -ge $LowestPreferedLogLevel ) {
                if (-not $operativeLevel['Enabled']) {
                # Sometimes our 'lowest prefered level' is e.g. debug, but if verbose is not specified write-verbose is suppressed.
                # This makes it still show up via write-host, without tampering with verbosepreference
                    write-host $ConsoleMessage
                } else {
                    Switch ($operativeLevel['Name']) {
                        'debug' { write-Debug $ConsoleMessage; break }
                        'verbose' { write-Verbose $ConsoleMessage; break }
                        'info' {
                            if ($indentLevel -gt 0) {
                                $color = "White"
                            } else {
                                $color = "White"
                            }
                            write-Host -ForegroundColor $color $ConsoleMessage; break
                        }
                        'warning' { write-Warning $ConsoleMessage; break }
                        'error' {

                            if ($target.getType() -eq [System.Management.Automation.ErrorRecord]) {
                                write-host $ConsoleMessage
                                $target
                            } else {
                                write-error $ConsoleShortMessage
                            }
                            break
                        }
                        Default {
                            Write-warning "This shouldnt happen."
                            write-host ("Levels: {0}`r`nSelectedIndex: {1}; selectedText: {2}; currentLow = {3}" -f $(@($LogLevels.Values.name.getEnumerator()) -join ";"), $operativeLevel['Index'], $operativeLevel['Name'], $LowestPreferedLogLevel)
                        }
                    }
                }
            }
            if ($passthru) {
                # output an array for consumption by PSCmdlet.shouldProcess.invoke($output)
                $targetIndent = $linePrefix.length -2

                return @($ConsoleMessage, ("{0}{1,-$targetIndent}: {2}{3}" -f $operativeLevel['Style'], "TARGET", $targetString,$PSStyle.Reset), $ConsoleMessage)
            }
        }
        #write-progress -id 20 -Completed
    }

    end {

    }
}