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