ShoutOut.psm1
# START: source\_buildBasicFileLogger.ps1 function _buildBasicFileLogger { param( [string]$FilePath ) return { param($Record) if (-not (Test-Path $FilePath -PathType Leaf)) { New-Item -Path $FilePath -ItemType File -Force -ErrorAction Stop | Out-Null } $Record | Out-File $FilePath -Encoding utf8 -Append -Force }.GetNewClosure() } # END: source\_buildBasicFileLogger.ps1 # START: source\_ensureShoutOutLogFile.ps1 function _ensureShoutOutLogFile { param( [string]$logFile, [string]$MsgType = "*" ) if (!(Test-Path $logFile -PathType Leaf)) { try { return new-Item $logFile -ItemType File -Force -ErrorAction Stop } catch { "Unable to create log file '{0}' for '{1}'." -f $logFile, $msgType | shoutOut -MsgType Error "Messages marked with '{0}' will be redirected." -f $msgType | shoutOut -MsgType Error shoutOut $_ Error throw ("Unable to use log file '{0}', the file cannot be created." -f $logFile) } } return gi $logFile } # END: source\_ensureShoutOutLogFile.ps1 # START: source\_injectLogHandler.ps1 <# .SYNOPSIS Injects a log handler into the callstack at the specified scope #> function _injectLogHandler { param( [Parameter(Mandatory=$true)] [scriptblock]$Handler, [Parameter(Mandatory=$false)] [ValidateNotNull()] [System.Management.Automation.CallStackFrame]$InjectionFrame, [Parameter(Mandatory=$false)] [String]$MessageType="*", [Parameter(Mandatory=$false)] [bool]$StopPropagation=$false ) # $script:logRegistry | Out-String | Write-Host -ForegroundColor DarkRed <# $callStack = Get-PSCallStack $callStack | Write-Host -ForegroundColor DarkCyan $InjectionFrame = $callStack | Where-Object { "-" * 80 | Write-Host -ForegroundColor DarkGreen $_.GetFrameVariables().Keys | Sort-Object | Write-Host -ForegroundColor Magenta $_.GetFrameVariables().ContainsKey($script:hashCodeAttribute) } | Select-Object -First 1 $InjectionFrame | Write-Host -ForegroundColor Cyan #> # Generate GUID $logId $logid = [guid]::newGuid().guid # Generate a record $logRecord of the handler: $record = @{ Id = $logId Handler = $Handler MessageType = $MessageType StopPropagation = $StopPropagation } if ($InjectionFrame) { $hash = $InjectionFrame.GetFrameVariables().$script:hashCodeAttribute.value.getHashCode() } else { $hash = 'global' } $record.frame = $hash if (-not $script:logRegistry.containsKey($hash)) { $script:logRegistry[$hash] = New-Object System.Collections.ArrayList } # "Adding handler {0} for frame {1}" -f $logId, $hash | Write-Host -ForegroundColor Cyan $script:logRegistry[$hash].Add($record) | Out-Null return $logId } # END: source\_injectLogHandler.ps1 # START: source\_removeLogHandler.ps1 <# .SYNOPSIS Attempts to remove the log handler with the specified Id from the callstack. Returns $true if a handler was removed, $false otherwise. #> function _removeLogHandler { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$LogId ) foreach($handlers in $script:logRegistry.Values) { $handler = $handlers | Where-Object { $_.Id -eq $LogId } if ($null -ne $handler) { $handlers.remove($handler) return $true } } return $false } # END: source\_removeLogHandler.ps1 # START: source\_resolveLogHandler.ps1 <# .SYNOPSIS Scans the current callstack for ShoutOut log handers macthing the provided MessageType (wildcard expression). #> function _resolveLogHandler { [CmdletBinding(DefaultParameterSetName="All")] param( [Parameter(Mandatory=$false)] [string]$MessageType, [Parameter(Mandatory=$false, ParameterSetName="TargetFrame")] [System.Management.Automation.CallStackFrame]$TargetFrame ) # $PSCmdlet.ParameterSetName | Write-Host -ForegroundColor DarkYellow # $script:logRegistry | Out-String | Write-Host -ForegroundColor DarkGreen $callstack = if ($TargetFrame) { @($TargetFrame) } else { Get-PSCallStack } $foundHandlers = New-Object System.Collections.Queue $liveHashes = new-Object System.Collections.ArrayList $liveHashes.Add('global') | Out-Null foreach($frame in $callstack){ # "-" * 80 | Write-Host -ForegroundColor Magenta $fv = $frame.GetFrameVariables() # $fv | Out-String | Write-Host if (-not $fv.ContainsKey($script:hashCodeAttribute)) { continue } $hash = $fv.$script:hashCodeAttribute.value.getHashCode() $liveHashes.Add($hash) | Out-Null # "Looking for handler on frame '{0}'..." -f $hash | Write-Host -ForegroundColor Gray if ($handlers = $script:logRegistry[$hash]) { foreach ($handler in $handlers) { if (($null -eq $Messagetype) -or ($MessageType -like $handler.MessageType)) { # "Found handler {1} on frame '{0}'" -f $hash, $handler.Id | Write-Host -ForegroundColor Green $foundHandlers.Enqueue($handler) if ($handler.StopPropagation) { return $foundHandlers } } } } } if (-not $PSBoundParameters.containsKey('TargetFrame')) { foreach($handler in $script:logRegistry['global']) { # "{0} -like {1} => {2}" -f $MessageType, $handler.MessageType, ($MessageType -like $handler.MessageType) | Write-Host if (($null -eq $Messagetype) -or ($MessageType -like $handler.MessageType)) { # "Found handler {1} on frame '{0}'" -f $hash, $handler.Id | Write-Host -ForegroundColor Green $foundHandlers.Enqueue($handler) if ($handler.StopPropagation) { return $foundHandlers } } } } if ($PSCmdlet.ParameterSetName -eq 'All') { # Garbage collection $hashes = [array]$script:logRegistry.Keys foreach ($hash in $hashes) { if ($hash -notin $liveHashes) { $script:logRegistry.remove($hash) } } } return $foundHandlers } # END: source\_resolveLogHandler.ps1 # START: source\_validateShoutOutLogHandler.ps1 function _validateShoutOutLogHandler { param( [scriptblock]$LogHandler, [string]$msgType = "*" ) # Valid/recognizable parameters and their expected typing: $validParams = @{ '$Message' = [Object] '$Record' = [String] '$details' = [hashtable] } $params = $LogHandler.Ast.ParamBlock.Parameters if ($params.count -eq 0) { "Invalid handler, no parameters found: {0}" -f $LogHandler | shoutOut -MsgType Error "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error throw "No parameters declared by the given handler." } $recognizedParams = $params | Where-Object { $_.Name.Extent.Text -in $validParams.Keys } if ($null -eq $recognizedParams) { "Invalid handler, none of the expeted parameters found (expected any of {0}): {1}" -f ($paramNames -join ', '), $LogHandler | shoutOut -MsgType Error "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error throw ("None of {0} parameters declared by the given handler." -f ($paramNames -join ', ')) } foreach ($param in $recognizedParams) { $paramType = $validParams[$param.Name.Extent.Text] if (($t = $param.StaticType) -and !($t.IsAssignableFrom($paramType)) ) { "Invalid handler, the '{0}' parameter should accept values of type '{1}' (found '{2}' which is not assignable from '{1}')." -f $param.Name, $paramType.Name, $t.Name | shoutOut -MsgType Error "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error throw ("'{0}' parameter on the given handler is of invalid type (not assignable from [string])." -f $paramNames) } } return $LogHandler } # END: source\_validateShoutOutLogHandler.ps1 # START: source\Add-ShoutOutLog.ps1 function Add-ShoutOutLog { [CmdletBinding()] param( [parameter(Mandatory=$true, Position=1, HelpMessage="Message type to redirect.")] [Alias('MsgType')] [string]$MessageType, [Parameter(ParameterSetName="FilePath", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")] [string]$LogFilePath, [Parameter(ParameterSetName="FileInfo", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="FileInfo object.")] [System.IO.FileInfo]$LogFile, [Parameter(ParameterSetName="Scriptblock", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="ScriptBlock to use as log handler.")] [scriptblock]$LogHandler, [Parameter(Mandatory=$false, HelpMessage="Causes the log handler to be registered on the global frame.")] [switch]$Global, [Parameter(Mandatory=$false, HelpMessage="Clear all log handlers for this message type on the current frame. If used with -Global this will remove all log handlers for the message type up to and including the global frame.")] [switch]$Reset ) switch ($PSCmdlet.ParameterSetName) { "FileInfo" { try { _ensureShoutOutLogFile $LogFile.FullName $MessageType | Out-Null $LogHandler = _buildBasicFileLogger $LogFile.FullName } catch { return $_ } } "FilePath" { try { _ensureShoutOutLogFile $LogFilePath $MessageType | Out-Null $LogHandler = _buildBasicFileLogger $LogFilePath } catch { return $_ } } } try { $cs = Get-PSCallStack $logHandler = _validateShoutOutLogHandler $LogHandler $MessageType $injectArgs = @{ InjectionFrame = $cs[1] MessageType = $MessageType Handler = $logHandler } if ((Get-PSCallStack)[1].Command -in 'Set-ShoutOutDefaultLog', 'Set-ShoutOutRedirect') { $injectArgs.InjectionFrame = $cs[2] } if ($PSBoundParameters.ContainsKey('Global')) { $injectArgs.remove('InjectionFrame') } if ($PSBoundParameters.ContainsKey('Reset')) { $handlers = Get-ShoutOutLog if (-not $PSBoundParameters.ContainsKey('Global')) { $hash = if ($InjectArgs.InjectionFrame) { $InjectArgs.InjectionFrame.GetFrameVariables().$script:hashCodeAttribute.value.getHashCode() } else { 'global' } $handlers = $handlers | Where-Object { $_.frame -eq $hash } } $handlers | Where-Object { $_.MessageType -eq $MessageType } | ForEach-Object { _removeLogHandler $_.Id | Out-Null } } _injectLogHandler @injectArgs } catch { return $_ } } # END: source\Add-ShoutOutLog.ps1 # START: source\Clear-ShoutOutLog.ps1 <# .SYNOPSIS Removes all log handlers in the current scope for the given message type. If the '-Global' switch is specified, all log handlers for the given message type will be removed #> function Clear-ShoutOutLog { param( [Parameter(Mandatory=$true, Position=1, HelpMessage="Mesage Type to remove handlers for.")] [Alias('MsgType')] [string]$MessageType, [Parameter(Mandatory=$false)] [Switch]$Global ) $resolveArgs = @{ MessageType = $MessageType } if (-not $Global) { $resolveArgs.TargetFrame = (Get-PSCallStack)[1] } _resolveLogHandler @resolveArgs | ForEach-Object { _removeLogHandler $_.Id } } # END: source\Clear-ShoutOutLog.ps1 # START: source\Get-ShoutOutConfig.ps1 function Get-ShoutOutConfig { return $_ShoutOutSettings } # END: source\Get-ShoutOutConfig.ps1 # START: source\Get-ShoutOutDefaultLog.ps1 function Get-ShoutOutDefaultLog { param( [Parameter(HelpMessage="Specifies that log handlers in all context should be removed, instead of just the current context.")] [switch]$Global ) $getArgs = @{ MessageType = '*' } if ($Global) { $getArgs.Global = $true } return Get-ShoutOutLog @getArgs } # END: source\Get-ShoutOutDefaultLog.ps1 # START: source\Get-ShoutOutLog.ps1 function Get-ShoutOutLog { [CmdletBinding(DefaultParameterSetName="MessageType")] param( [Parameter(Mandatory=$false, ParameterSetName="MessageType", HelpMessage="Message Type to retrieve log handlers for.")] [Alias('MsgType')] [string]$MessageType, [Parameter(Mandatory=$true, ParameterSetName="LogId", HelpMessage="ID of the log handler to retrieve")] [guid]$LogId ) switch ($PSCmdlet.ParameterSetName) { MessageType { $foundHandlers = New-Object System.Collections.ArrayList foreach ($context in $script:logRegistry.Keys) { $handlers = $script:logRegistry[$context] $handlers | Where-Object { ('' -eq $MessageType) -or ($_.MessageType -eq $MessageType) } | ForEach-Object { $foundHandlers.Add($_) | Out-Null } } return $foundHandlers } LogId { foreach ($context in $script:logRegistry.Keys) { $handlers = $script:logRegistry[$context] $id = $LogId.Guid foreach ($handler in $handlers) { if ($handler.id -eq $id) { return $handlers[$id] } } } } } } # END: source\Get-ShoutOutLog.ps1 # START: source\Invoke-ShoutOut.ps1 <# .WISHLIST - Update so that that output is fed to shoutOut as it is generated rather than using the result output. The goal is to generate logging data continuously so that it's clear whether the script has hung or not. [Done] .SYNOPSIS Helper function to execute commands (strings or blocks) with error-handling/reporting. .DESCRIPTION Helper function to execute commands (strings or blocks) with error-handling/reporting. Invokes a command in a new context so that all output and errors can be captured and recorded by ShoutOut. If a scriptblock is passed as the operation, the function will attempt make any variables referenced by the scriptblock available to the scriptblock when it is resolved (using variables available in the scope that called Run-Operation). The variables used in the command are identified using [scriptblock].Ast.FindAll method, and are imported from the parent scope using $PSCmdlet.SessionState.PSVariable.Get. The following variable-names are restricted and may cause errors if they are used in the operation: - $__thisOperation: The operation being run. - $__inputVariables: List of the variables being imported to run the operation. .NOTES - Transforms ScriptBlocks to Strings prior to execution because of a quirk in iex where it will not allow the evaluation of ScriptBlocks without an input (a 'param' statement in the block). iex is used because it yields output as each line is evaluated, rather than waiting for the entire $OPeration to complete as would be the case with <ScriptBlock>.Invoke(). #> function Invoke-ShoutOut { param( [parameter(ValueFromPipeline=$true, position=1, HelpMessage="Operation to perform. Should be a scripblock or a string describing a command to run.")] $Operation, [parameter(HelpMessage="Suppreses all output from the call.")] [Switch] $OutNull, [parameter(HelpMessage="Do not treat exceptions as fatal.")] [Switch] $NotStrict, [parameter(HelpMessage="Only log errors.")] [Switch] $LogErrorsOnly, [parameter(HelpMessage="Suppress output to console.")] [Switch] $Quiet ) $shoutOutArgs = @{ Quiet = $PSBoundParameters.ContainsKey('Quiet') } if (!$NotStrict) { # Switch error action preference to catch any errors that might pop up. # Works so long as the internal operation doesn't also change the preference. $OldErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop } if ($Operation -is [string]) { $Operation = [scriptblock]::create($Operation) } if (!$LogErrorsOnly) { $msg = "Invoke-ShoutOut: '$Operation'..." $msg | shoutOut -MsgType Invocation -ContextLevel 2 @shoutOutArgs } $r = try { # Step 1: Get any variables in the parent scope that are referenced by the operation. if ($Operation -is [scriptblock]) { $variableNames = $Operation.Ast.FindAll( {param($o) $o -is [System.Management.Automation.Language.VariableExpressionAst]}, $true ) | ForEach-Object { $_.VariablePath.UserPath } $variables = foreach ($vn in $variableNames) { $PSCmdlet.SessionState.PSVariable.Get($vn) } } # Step 2: Convert the scriptblock if necessary. if ($Operation -is [scriptblock]) { # Under certain circumstances the iex cmdlet will not allow # the evaluation of ScriptBlocks without an input. However it will evaluate strings # just fine so we perform the transformation before evaluation. $Operation = $Operation.ToString() } # Step 3: inject the operation and the variables into a new isolated scope and resolve # the operation there. & { param( $thisOperation, $inputVariables ) $__thisOperation = $thisOperation $__inputVariables = $inputVariables Remove-Variable "thisOperation" Remove-Variable "inputVariables" $__ = $null foreach ( $__ in $__inputVariables ) { if ($null -ne $__) { if ($v = Get-Variable -Name $__.Name -Scope 0 -ea SilentlyContinue) { if (-not $v.Options.HasFlag([System.Management.Automation.ScopedItemOptions]::Constant)) { Set-Variable $__.Name $__.Value } } else { Set-Variable $__.Name $__.Value } } } Remove-Variable "__" # Invoke-Expression allows us to receive # and handle output as it is generated, # rather than wait for the operation to finish # as opposed to <[scriptblock]>.invoke(). Invoke-Expression $__thisOperation | ForEach-Object { if (!$LogErrorsOnly) { shoutOut "`t| $_" "Result" -ContextLevel 2 @shoutOutArgs } return $_ } } $Operation $variables } catch { "An error occured while executing the operation:" | shoutOUt -MsgType Error -ContextLevel 2 @shoutOutArgs $_ | shoutOut -MsgType Error -ContextLevel 2 @shoutOutArgs $_ } if (!$NotStrict) { $ErrorActionPreference = $OldErrorActionPreference } if ($OutNull) { return } return $r } # END: source\Invoke-ShoutOut.ps1 # START: source\Remove-ShoutOutLog.ps1 function Remove-ShoutOutLog { [CmdletBinding()] param( [Parameter(Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, HelpMessage="Id of the log handler to remove")] [guid[]]$LogId ) process { foreach ($id in $logId) { foreach ($context in $script:logRegistry.Keys) { $context | Write-Host -Fore Magenta $handlers = $script:logRegistry[$context] foreach ($handler in $handlers) { if ($handler.id -eq $id) { $handlers.remove($handler) break } } } } } } # END: source\Remove-ShoutOutLog.ps1 # START: source\Set-ShoutOutConfig.ps1 function Set-ShoutOutConfig { param( [Parameter(HelpMessage="The default Message Type that ShoutOut should apply to messages.")] [string]$DefaultMsgType, [Parameter(HelpMessage="Enable/Disable Context logging.")] [Alias("LogContext")] [boolean]$EnableContextLogging, [Parameter(HelpMessage="Disable/Enable ShoutOut.")] [Alias("Disabled")] [boolean]$DisableLogging ) if ($PSBoundParameters.ContainsKey("DefaultMsgType")) { $_shoutOutSettings.DefaultMsgType = $DefaultMsgType } if ($PSBoundParameters.ContainsKey("LogContext")) { $_shoutOutSettings.LogContext = $LogContext } if ($PSBoundParameters.ContainsKey("Disabled")) { $_shoutOutSettings.Disabled = $Disabled } } # END: source\Set-ShoutOutConfig.ps1 # START: source\Set-ShoutOutDefaultLog.ps1 function Set-ShoutOutDefaultLog { param( [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogFilePath", HelpMessage="Path to log file.")] [String]$LogFilePath, [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogFile", HelpMessage="FileInfo object.")] [System.IO.FileInfo]$LogFile, [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogHandler", HelpMessage="ScriptBlock to use as log handler.")] [scriptblock]$LogHandler, [Parameter(Mandatory=$false, HelpMessage="Causes the log handler to be registered on the global frame.")] [switch]$Global ) $redirectArgs = @{ MsgType = '*' Reset = $true } $PSBoundParameters.GetEnumerator() | ForEach-Object { $redirectArgs[$_.Key] = $_.Value } return Add-ShoutOutLog @redirectArgs } # END: source\Set-ShoutOutDefaultLog.ps1 # START: source\Set-ShoutOutRedirect.ps1 function Set-ShoutOutRedirect { [CmdletBinding()] param( [parameter(Mandatory=$true, Position=1, HelpMessage="Message type to redirect.")] [Alias('MsgType')] [string]$MessageType, [Parameter(ParameterSetName="FilePath", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")] [string]$LogFilePath, [Parameter(ParameterSetName="FileInfo", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="FileInfo object.")] [System.IO.FileInfo]$LogFile, [Parameter(ParameterSetName="Scriptblock", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="ScriptBlock to use as log handler.")] [scriptblock]$LogHandler, [Parameter(Mandatory=$false, HelpMessage="Causes the log handler to be added to the global frame.")] [switch]$Global ) $redirectArgs = @{ Reset = $true } $PSBoundParameters.GetEnumerator() | ForEach-Object { $redirectArgs[$_.Key] = $_.Value } return Add-ShoutOutLog @redirectArgs } # END: source\Set-ShoutOutRedirect.ps1 # START: source\ShoutOut.ps1 # ShoutOut.ps1 # First-things first: Logging function (is the realest, push the message and let the harddrive feel it.) <# .SYNOPSIS Pushes a message of the given $MessageType to the appropriate log handlers. .DESCRIPTION Logging function, used to push a message to a corresponding log handlers. The default log handler type is a Record handler, in thich The message is prepended with meta data about the invocation to shoutOut as: <MessageType>|<Computer name>|<PID>|<calling Context>|<Date & time>|<Message object type>|<$Message as a string> The other types of log handlers are 'Message' (just receives the raw message object) and 'Details' (the raw message along with the same metadata that is summarized for the 'Record' type). If an ErrorRecord or Exception object is passed to shoutout, will attempt to expand the object to make the output of 'Record' type handlers more detailed. The default values for the parameters can be set using the Set-ShoutOutConfig, Set-ShoutOutDefaultLog, Set-ShotOutRedirect and Add-ShoutOutLog functions. .PARAMETER Message Message object to log. .PARAMETER MessageType The type of message to log. By default ShoutOut is intended to handle the following types: - Success: Indicating a positive outcome. - Error: Indicating that the message is or is related to an Error (typically an [ErrorRecord] object). Comparable with Write-Error. - Exception: Indicates that the message is or is related to an Exception. - Warning: Indicates that the message relates to a non-fatal irregularity in the system. - Info: Indicates that the message is purely informational. This is the standard default message type. Comparable with Write-Host. - Result: Indicates that the message is related to an output value from an operation. Comparable with write output. Each of these types have standard output color presets. In practice ShoutOut will accept any given string. .PARAMETER Log Overrides the standard log-selection process and forces shoutout to use the provided log. If this parameter is a string it will be interpreted as the path to a file where log records should be written. If this is a ScriptBlock, it should have one of the accepted parameters: - Message - Details - Record See the overall ShoutOut README.md for details on Log Handlers. .PARAMETER ContextLevel The number of steps to climb up the callstack when reporting context. 0: Include the call to shoutout. 1: Include the callstack from the call to the context where shoutout was called. Default is 1. When using a Record-type log handler the last element in the stack will be reported as the calling context. .PARAMETER LogContext If set to $false, no context information will be included in the log (no 'Callstack' for Details, no calling context for Record handlers). .PARAMETER NoNewLine Omits the newline when writing to console/host. .PARAMETER Quiet Disables all writing to console/host. #> function shoutOut { [CmdletBinding()] param( [Alias('Msg')] [parameter(Mandatory=$false, position=1, ValueFromPipeline=$true, ParameterSetName="Message", HelpMessage="Message object to log")] [Object]$Message, [Alias("ForegroundColor")] [Alias("MsgType")] [parameter(Mandatory=$false, position=2, HelpMessage="The type of message log. If this is not specified it will be calculated based on the input type and shoutout configuration.")] [String]$MessageType=$null, [parameter(Mandatory=$false, position=3, HelpMessage="Path to a file or a Scriptblock to use to log the message.")] $Log=$null, [parameter(Mandatory=$false, position=4, HelpMessage="How many levels to remove from the callstack when reporting the caller context. 0 will include the call to ShoutOut. Default is 1")] [Int32]$ContextLevel=1, # The number of levels to proceed up the call # stack when reporting the calling script. [parameter(Mandatory=$false, HelpMessage="Determines if context information should be logged.")] [bool] $LogContext=$true, [parameter(Mandatory=$false, HelpMessage="If set, omits the newline when writing to console/host.")] [Switch] $NoNewline, [parameter(Mandatory=$false, HelpMessage="If set, no output will be printed to console/host.")] [Switch] $Quiet ) begin { $settings = $_ShoutOutSettings # If shoutOut is disabled, return to caller. if ($settings.ContainsKey("Disabled") -and ($settings.Disabled)) { Write-Debug "Call to Shoutout, but Shoutout is disabled. Turn back on with 'Set-ShoutOutConfig -Disabled `$false'." return } <# Applying global variables #> if (!$PSBoundParameters.ContainsKey('LogContext') -and $_ShoutOutSettings.ContainsKey("LogContext")) { $LogContext = $_ShoutOutSettings.LogContext } } process { $details = @{ Message = $Message Computer = $env:COMPUTERNAME LogTime = [datetime]::Now PID = $pid } $msgObjectType = if ($null -ne $Message) { $Message.GetType() } else { $null } $details.ObjectType = if ($null -ne $msgObjectType) { $msgObjectType.Name } else { "NULL" } if ( (-not $PSBoundParameters.ContainsKey("MessageType")) -or ($null -eq $PSBoundParameters["MessageType"]) ) { switch ($details.ObjectType) { "ErrorRecord" { $MessageType = "Error" } default { if ([System.Exception].IsAssignableFrom($msgObjectType)) { $MessageType = "Exception" } else { $MessageType = $script:_ShoutOutSettings.DefaultMsgType } } } } $details.MessageType = $MessageType $logHandlers = if ($null -eq $Log) { _resolveLogHandler -MessageType $MessageType | ForEach-Object Handler } else { Switch ($log.GetType().Name) { String { @{ Handler = @(_buildBasicFileLogger $Log) } } ScriptBlock { @{ Handler = $Log } } } } $recurseArgs = @{} $PSBoundParameters.Keys | Where-Object { $_ -notin "Message", "MsgType", "MessageType" } | ForEach-Object { $recurseArgs[$_] = $PSBoundParameters[$_] } if ($recurseArgs.ContainsKey('ContextLevel')) { $recurseArgs.ContextLevel += 1 } else { $recurseArgs.ContextLevel = 2 } $messageString = $null # Apply formatting to make output more readable. switch ($details.ObjectType) { "String" { # No transformation necessary. $messageString = $details.Message } "NULL" { # No Transformation necessary. $messageString = "" } "ErrorRecord" { if ($null -ne $details.Message.Exception) { shoutOut -Message $details.Message.Exception @recurseArgs } $m = $details.Message $MessageString = 'Exception', 'CategoryInfo', 'InvocationInfo', 'ScriptStackTrace' | ForEach-Object { $m.$_ } | Out-string | ForEach-Object Split "`n`r" | Where-Object { $_ } $MessageString = $MessageString -join "`n" } default { $t = $details.Message.GetType() if ([System.Exception].IsAssignableFrom($t)) { if ($null -ne $details.Message.InnerException) { shoutOut $details.Message.InnerException @recurseArgs } $m = $details.Message $MessageString = 'message', 'Source', 'Stacktrace', 'TargetSite' | ForEach-Object { $m.$_ } | Out-string | ForEach-Object Split "`n`r" | Where-Object { $_ } $MessageString = $MessageString -join "`n`r" } else { $messageString = $Message | Out-String | ForEach-Object TrimEnd "`n`r" } } } $details.MessageString = $MessageString # Print to console if necessary if ([Environment]::UserInteractive -and !$Quiet) { if ($settings.containsKey("MsgStyles") -and ($settings.MsgStyles -is [hashtable]) -and $settings.MsgStyles.containsKey($details.MessageType)) { $msgStyle = $settings.MsgStyles[$details.MessageType] } if (!$msgStyle) { if ($details.MessageType -in [enum]::GetNames([System.ConsoleColor])) { $msgStyle = @{ ForegroundColor=$details.MessageType } } else { $msgStyle = @{ ForegroundColor="White" } } } $p = @{ Object = $details.MessageString NoNewline = $NoNewline } if ($msgStyle.ForegroundColor) { $p.ForegroundColor = $msgStyle.ForegroundColor } if ($msgStyle.BAckgroundColor) { $p.BackgroundColor = $msgStyle.BackgroundColor } Write-Host @p } # Calculate parent/calling context $details.Caller = if ($LogContext) { # Calculate the callstack. $cs = Get-PSCallStack # Adjust ContextLevel if it is greater than the total size of the callstack: if ($cs.Length -le $ContextLevel) { $ContextLevel = $cs.Length - 1 } $cs = $cs[$ContextLevel..($cs.length - 1)] # Record the callstack on details: $details.CallStack = $cs # Calculate caller context: $l = if ($null -eq $cs[0].ScriptName) { "<No file>" } else { '{0}:{1}' -f $cs[0].ScriptName, $cs[0].ScriptLineNumber } "[{0}]{1}" -f ($cs.length), $l } else { "[context logging disabled]" } $createRecord = { param($details) "{0}|{1}|{2}|{3}|{4}|{5}|{6}" -f @( $details.MessageType, $details.Computer, $details.PID, $details.Caller, $details.LogTime.toString('o'), $details.ObjectType, $details.MessageString ) } foreach ($handler in $logHandlers) { try { $handlerArgs = @{} $handler.Ast.ParamBlock.Parameters | ForEach-Object { $n = $_.Name.Extent.Text.TrimStart('$') switch ($n) { Message { $handlerArgs.$n = $details.Message } Record { $handlerArgs.$n = & $createRecord $details } Details { $handlerArgs.$n = $details } } } & $handler @handlerArgs } catch { "Failed to log: {0}" -f ($handlerArgs | Out-String) | Write-Error "using log handler: '{0}'" -f $handler | Write-Error $_ | Out-string | Write-Error } } } } # END: source\ShoutOut.ps1 # ShoutOut..setup # START: source\.setup\.bootstrap.ps1 # Default Configuration: $script:_ShoutOutSettings = @{ DefaultMsgType="Info" MsgStyles=@{ Success = @{ ForegroundColor="Green" } Exception = @{ ForegroundColor="Red"; BackgroundColor="Black" } Error = @{ ForegroundColor="Red" } Warning = @{ ForegroundColor="Yellow"; BackgroundColor="Black" } Info = @{ ForegroundColor="Cyan" } Result = @{ ForegroundColor="White" } } LogContext=$true Disabled=$false } $script:logRegistry = @{ global = New-Object System.Collections.ArrayList } $script:hashCodeAttribute = 'MyInvocation' New-Alias 'Get-ShoutOutRedirect' -Value 'Get-ShoutOutLog' New-Alias 'Clear-ShoutOutRedirect' -Value 'Clear-ShoutOutLog' # Setting up default logging: $defaultLogFilename = "{0}.{1}.{2:yyyyMMddHHmmss}.log" -f $env:COMPUTERNAME, $pid, [datetime]::Now $defaultLogFile = "{0}\AppData\local\ShoutOut\{1}" -f $env:USERPROFILE, $defaultLogFilename $script:DefaultLog = _buildBasicFileLogger $defaultLogFile Set-ShoutOutDefaultLog -LogHandler $script:DefaultLog -Global # $script:logRegistry.values | Out-String | Write-Host # END: source\.setup\.bootstrap.ps1 |