ScriptLog.psm1
#Region './Enum/ScriptLogMessageSeverity.ps1' 0 # Define log message severity types enum ScriptLogMessageSeverity { Information Verbose Warning Error } #EndRegion './Enum/ScriptLogMessageSeverity.ps1' 9 #Region './Enum/ScriptLogType.ps1' 0 # Define log types enum ScriptLogType { CMTrace Memory } #EndRegion './Enum/ScriptLogType.ps1' 7 #Region './Classes/01-LogMessage.ps1' 0 # Declare class for individual log messages class LogMessage { [datetime]$DateTime [ScriptLogMessageSeverity]$Severity [string]$Source [string]$Context [int]$ProcessId [string]$Message LogMessage([datetime]$DateTime, [ScriptLogMessageSeverity]$Severity, [string]$Source, [string]$Context, [int]$ProcessId, [string]$Message) { $this.DateTime = $DateTime $this.Severity = $Severity $this.Source = $Source $this.Context = $Context $this.ProcessId = $ProcessId $this.Message = $Message } } #EndRegion './Classes/01-LogMessage.ps1' 19 #Region './Classes/02-ScriptLog.ps1' 0 # Declare class for a log object class ScriptLog { [String] $Name [String] $FilePath [ScriptLogType] $LogType [String] $Source = $null [ScriptLogMessageSeverity[]] $MessagesOnConsole [DateTime] $StartTimeStamp [System.Collections.Generic.List[LogMessage]] $Messages hidden [String] $TimeZoneOffset ScriptLog([String] $Name, [String] $Path, [String] $BaseName, [Boolean] $AppendDateTime, [ScriptLogType] $LogType, [ScriptLogMessageSeverity[]] $MessagesOnConsole) { $this.Name = $Name $this.LogType = $LogType $this.MessagesOnConsole = $MessagesOnConsole $this.StartTimeStamp = Get-Date $this.Messages = [System.Collections.Generic.List[LogMessage]]::new() $Offset = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes if ($Offset -ge 0) { $this.TimeZoneOffset = "+$Offset" } else { $this.TimeZoneOffset = [string]"$Offset" } if ($LogType -eq 'Memory') { $this.FilePath = $null } else { $ConstructedPath = $Path + '\' + $BaseName if ($AppendDateTime) { $ConstructedPath += '-' + (Get-Date -Format 'yyyyMMddHHmmss') } switch ($LogType) { CMTrace { $ConstructedPath += '.log' } } # Ensure path is not already used by another ScriptLog if ($Script:ScriptLogs.count -gt 0) { if ($ConstructedPath -in $Script:ScriptLogs.FilePath) { throw "Another active ScriptLog is already using the file '$ConstructedPath'" } } $this.FilePath = $ConstructedPath } } } #EndRegion './Classes/02-ScriptLog.ps1' 48 #Region './Private/00-Initialization.ps1' 0 $PSDefaultParameterValues.Clear() Set-StrictMode -Version 3 # Prepare value defining the default ScriptLog to log messages to $DefaultScriptLog = $null # Prepare collection to hold ScriptLog objects [System.Collections.Generic.List[ScriptLog]]$ScriptLogs = @() #EndRegion './Private/00-Initialization.ps1' 10 #Region './Public/Get-ScriptLog.ps1' 0 function Get-ScriptLog { <# .SYNOPSIS Returns active ScriptLogs. .DESCRIPTION Returns a list of all active ScriptLogs. .EXAMPLE Get-ScriptLog Returns a list of all active ScriptLogs .EXAMPLE Get-ScriptLog -Name "SomeLog" Returns the ScriptLog named "SomeLog" .EXAMPLE Get-ScriptLog -Default Returns the default ScriptLog .NOTES Author: kovergard #> [CmdletBinding(DefaultParameterSetName = 'AllLogs')] [OutputType([ScriptLog[]])] Param ( # Find ScriptLog object by name [Parameter(ValueFromPipeline, ParameterSetName = 'SpecificLog')] [String] $Name, # If specified, return only the default ScriptLog [Parameter(ParameterSetName = 'DefaultLog')] [switch] $Default ) process { if ($Default) { if ($Script:ScriptLogs.Count -eq 0) { throw 'No ScriptLogs exists, cannot return default ScriptLog' } elseif (-not $DefaultScriptLog) { throw 'No default ScriptLog has been defined' } return $DefaultScriptLog } if ($Name) { $Log = $Script:ScriptLogs | Where-Object { $_.Name -eq $Name } if (-not $Log) { throw "Log with name '$Name' not found" } Return $Log } return $ScriptLogs } } #EndRegion './Public/Get-ScriptLog.ps1' 61 #Region './Public/New-ScriptLog.ps1' 0 function New-ScriptLog { <# .SYNOPSIS Returns a new ScriptLog object .DESCRIPTION Creates a new ScriptLog object with the settings provided and returns it through the pipeline so it can be used for logging during script execution using the Out-ScriptLog cmdlet. .EXAMPLE New-ScriptLog Create a new ScriptLog object with default settings. File will be created in the temp folder, with the name ScriptLog.log and will be written in the CMTrace format. .EXAMPLE $MemoryLog = New-ScriptLog -Name "TempLog" -LogType Memory -MessagesOnConsole @("Error","Verbose") Create an in-memory SriptLog instance to allow for collection of log messages during runtime. Only errors and verbose messages will be written to the console (Warnings will not, they will only be written to the in-memory log) .EXAMPLE $CriticalFileLog = New-ScriptLog -Name "Critical" -Path "C:\Logs" -BaseName "CriticalErrors" -AppendDateTime; $VerboseLog = New-ScriptLog -Name "Verbose" -Path "C:\Logs" -BaseName "Verbose" -MessagesOnConsole "Verbose" Create two separate ScriptLog objects to log messages in different formats to two different files. .NOTES Author: kovergard #> [CmdletBinding()] [OutputType([ScriptLog])] Param ( # The name of the log [Parameter()] [string] $Name = (New-Guid).Guid, # Directory in which to create the logfile [Parameter()] [string] $Path = $env:TEMP, # Name of the log file without extension [Parameter()] [string] $BaseName = 'ScriptLog', # Indicates if a datetime should be suffixed on the log base name. [Parameter()] [switch] $AppendDateTime, # Type of log [Parameter()] [ScriptLogType] $LogType = 'CMTrace', # Determines which messages (if any) should be written to the console. [Parameter()] [ScriptLogMessageSeverity[]] $MessagesOnConsole = @('Error', 'Warning') ) process { if ($Script:ScriptLogs.Count -gt 0) { if ($Script:ScriptLogs.Name -contains $Name) { throw "A ScriptLog with the name '$Name' already exists. Active ScriptLogs must have unique names." } } $NewScriptLog = [ScriptLog]::New($Name, $Path, $BaseName, $AppendDateTime, $LogType, $MessagesOnConsole) $Script:ScriptLogs.Add($NewScriptLog) if (-not $DefaultScriptLog) { Set-Variable -Name DefaultScriptLog -Value $NewScriptLog -Scope Script -Force } Write-Output $NewScriptLog } } #EndRegion './Public/New-ScriptLog.ps1' 75 #Region './Public/Out-ScriptLog.ps1' 0 function Out-ScriptLog { <# .SYNOPSIS Adds log messages to a ScriptLog. .DESCRIPTION Adds one or more log messages to a ScriptLog object. If multiple messages are sent via the pipleline, each message will get its own message entry in the log. A single messages can have multiple lines, these will be writting to the log file with line changes. If a message is longer than 7500 characters, it will be broken into multiple messages as longer messages will break the CMTrace format. .EXAMPLE Out-ScriptLog -Message "Starting script execution" Write a log message to the information channel in the default ScriptLog instance. .EXAMPLE Out-ScriptLog -Log $VerboseLog -Message "Starting script execution" -Severity Verbose Write a log message to the verbose channel in the ScriptLog $VerboseLog .EXAMPLE $Dir = Get-ChildItem -Path c:\temp; Out-ScriptLog -Message $Dir -Log $Log Write an object with multiple lines in it to the log file. This will be writtin as a single log message, since the message is not passed through the pipeline. .EXAMPLE "One","Two","Three" | Out-ScriptLog -Severity Warning Send multiple messages to the log using the pipeline. Each message will get its own log message. .NOTES Author: kovergard #> [CmdletBinding(DefaultParameterSetName = 'ByName')] Param ( # One or more messages to add to the log [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ValueFromRemainingArguments = $false)] $Message, # The Name of the ScriptLog to add the messages to. If no ScriptLog is supplied, logging is done to the default ScriptLog. [Parameter(ParameterSetName = 'ByName')] [String] $Name, # The ScriptLog object to add the messages to. If no ScriptLog is supplied, logging is done to the default ScriptLog. [Parameter(ParameterSetName = 'ByObject')] [ScriptLog] $Log, # The severity of the messages [Parameter()] [ScriptLogMessageSeverity] $Severity = 'Information' ) process { # If no ScriptLog is specified, point to default ScriptLog. if (-not $PSBoundParameters.ContainsKey('Log') -and -not $PSBoundParameters.ContainsKey('Name')) { if (-not $DefaultScriptLog) { throw 'No default ScriptLog has been defined, please use -Name or -Log parameter to target log' } $Log = $DefaultScriptLog } else { if ($PSBoundParameters.ContainsKey('Name')) { $Log = $Script:ScriptLogs | Where-Object { $_.Name -eq $Name } if (-not $Log) { throw "Log with name '$Name' not found" } } else { #TODO: Detect if Log exists } } # Convert message to string if necessary if ($Message.GetType() -ne 'System.String') { $Message = ($Message | Out-String).TrimEnd("`r`n") } # Determine log time and source of message $LogTime = Get-Date if ($Log.Source) { $Source = $Log.Source if ($MyInvocation.ScriptLineNumber) { $Source += ":$($MyInvocation.ScriptLineNumber)" } } else { Try { If ($MyInvocation.ScriptName) { [string]$Source = "$(Split-Path -Path $MyInvocation.ScriptName -Leaf -ErrorAction 'Stop'):$($MyInvocation.ScriptLineNumber)" } Else { $Source = 'interactive' } } Catch { $Source = 'unknown' } } # Get context and PID of message $Context = [Security.Principal.WindowsIdentity]::GetCurrent().Name $ProcessId = $global:PID # Add message to in-memory log. $Log.Messages.Add([LogMessage]::New($LogTime, $Severity, $Source, $Context, $ProcessId, $Message)) # If message should be written to a file, convert to proper format and write. switch ($Log.LogType) { CMTrace { if ($Message.Length -gt 7500) { $CMMessage = $Message.Substring(0, 7500) } else { $CMMessage = $Message } $CmLogLine = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="{4}" type="{5}" thread="{6}" file="{7}">' $CmMessageType = Switch ($Severity) { Error { 3 } Warning { 2 } Default { 1 } } $CmTime = ($LogTime | Get-Date -Format 'HH\:mm\:ss.fff').ToString() + $Log.TimeZoneOffset $CmDate = ($LogTime | Get-Date -Format 'MM-dd-yyyy') $CmFile = 'ScriptLog' $CmLogLineFormat = $CMMessage, $CmTime, $CmDate, $Source, $Context, $CmMessageType, $ProcessId, $CmFile $LogLine = $CmLogLine -f $CmLogLineFormat $LogLine | Out-File -FilePath $Log.FilePath -Append -Encoding utf8 -NoClobber } } # Write output to console, if applicable if ($Severity -in $Log.MessagesOnConsole) { Switch ($Severity) { Information { Write-Information -MessageData $Message -InformationAction Continue } Verbose { $VerbosePreference = 'Continue'; Write-Verbose -Message $Message } Warning { Write-Warning -Message $Message } Error { Write-Error -Message $Message } } } } } #EndRegion './Public/Out-ScriptLog.ps1' 158 #Region './Public/Remove-ScriptLog.ps1' 0 function Remove-ScriptLog { <# .SYNOPSIS Removes a ScriptLog from memory. .DESCRIPTION Removes one (or all) active ScriptLogs from memory, without removing the log files that has been used by the log(s). .EXAMPLE Remove-ScriptLog -Name "MyLog" Removes the ScriptLog named "MyLog" .EXAMPLE Remove-ScriptLog -All Removes all ScriptLogs .NOTES Author: kovergard #> [CmdletBinding(DefaultParameterSetName = 'SpecificLog')] [OutputType()] Param ( # ScriptLog object to remove [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'SpecificLog')] [String] $Name, # If specified, removes all ScriptLog objects [Parameter(ParameterSetName = 'AllLogs')] [switch] $All ) process { # Remove all ScriptLogs if requested if ($All) { $Script:ScriptLogs | ForEach-Object { $_.Messages.Clear() } $Script:ScriptLogs.Clear() $Script:DefaultScriptLog = $null } else { $Log = $Script:ScriptLogs | Where-Object { $_.Name -eq $Name } if (-not $Log) { throw "Log with name '$Name' not found" } if ($Script:DefaultScriptLog -eq $Log) { $Script:DefaultScriptLog = $null } $Script:ScriptLogs.Remove($Log) | Out-Null $Log.Messages.Clear() } } } #EndRegion './Public/Remove-ScriptLog.ps1' 57 |