Logging is perhaps not the first thing you think about when making small scripts that is only intended to be used by yourself, but the moment other people are involved, the probability of something going wrong is going up. And making scripts intended for others usually means the size and complexity goes up as well. At some point, you will get tired of feedback from others that are hard to debug, and wish you wrote logging into your script! What if logging was as simple as just adding a bunch of “Write-Log”‘s in your code? I’ll show you how.
I have created three logging functions that makes logging as easy as writing to the verbose stream. The first function (New-Log) you add one time, in the beginning of your script, and defines what kind of logging you want to do (it currently supports logging to file and to the Windows Event log), and some parameters.
Whenever you want to log something throughout your script, just use the second function (Write-Log). It works the same as all the other Write-* commands, except it has some different parameters relevant for logging. When writing to a file, it will automatically call the third function (Invoke-LogRotation) after each write, so you can keep some kind of control of your log(s).
Look at the help for New-Log and Write-Log for examples of use, and descriptions of the different parameters.
Even though I try to test out my functions thoroughly before releasing them, there are probably some bugs or stuff I haven’t thought about. So if you find anything wrong, or have any ideas for future improvement, let me know, ok?
Oh, and this is the first time I’m using a mutex (I found some excellent information about it from Boe Prox), so if I’m using it completely wrong, I’d appreciate you’d let me know!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function New-Log { | |
<# | |
.SYNOPSIS | |
Create 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 $global:PSLOG | |
.EXAMPLE | |
New-Log '.\myScript.log' -Header 'MyHeader – MyScript' -Append -Format '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: Øyvind Kallstad | |
Date: 21.11.2014 | |
Version: 1.0 | |
#> | |
[CmdletBinding(DefaultParameterSetName = 'LogFile')] | |
param ( | |
# Path to log file. | |
[Parameter(ParameterSetName = 'LogFile', Mandatory, Position = 0)] | |
[ValidateNotNullorEmpty()] | |
[string] $Path, | |
# Optionally define a header to be added when a new empty log file is created. | |
[Parameter(ParameterSetName = 'LogFile')] | |
[string] $Header, | |
# If log file already exist, append instead of creating a new empty log file. | |
[Parameter(ParameterSetName = 'LogFile')] | |
[switch] $Append, | |
# Maximum size of log file. | |
[Parameter(ParameterSetName = 'LogFile')] | |
[int64] $MaxLogSize = 1048576, # in bytes, default is 1048576 = 1 MB | |
# Maximum number of log files to keep. Default is 3. Setting MaxLogFiles to 0 will keep all log files. | |
[Parameter(ParameterSetName = 'LogFile')] | |
[ValidateRange(0,99)] | |
[int32] $MaxLogFiles = 3, | |
# 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. | |
[Parameter(ParameterSetName = 'LogFile')] | |
[ValidateSet('Minimal','PlainText','CMTrace')] | |
[string] $Format = 'PlainText', | |
# Specifies the name of the event log. | |
[Parameter(ParameterSetName = 'EventLog', Mandatory)] | |
[string] $EventLogName, | |
# Specifies the name of the event log source. | |
[Parameter(ParameterSetName = 'EventLog', Mandatory)] | |
[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 = '0', | |
# When UseGlobalVariable is True, the log object is saved in the global PSLOG variable, otherwise it's returned to the pipeline. Default value is True. | |
[Parameter()] | |
[switch] $UseGlobalVariable = $true | |
) | |
$elevated = $null | |
if ($PSCmdlet.ParameterSetName -eq 'EventLog') { | |
$logType = 'EventLog' | |
# when creating (and writing) to the event log, you need to run with elevated user rights | |
$windowsIdentity=[System.Security.Principal.WindowsIdentity]::GetCurrent() | |
$windowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($windowsIdentity) | |
$adm=[System.Security.Principal.WindowsBuiltInRole]::Administrator | |
if ($windowsPrincipal.IsInRole($adm)) { | |
$elevated = $true | |
Remove-Variable –Name Format,MaxLogSize,MaxLogFiles –ErrorAction SilentlyContinue | |
# create new event log if needed | |
try { | |
if (-not([System.Diagnostics.EventLog]::SourceExists($EventLogName))) { | |
New-EventLog –Source $EventLogSource –LogName $EventLogName | |
Write-Verbose "Created new event log (Name: $($EventLogName), Source: $($EventLogSource))" | |
} | |
else { | |
Write-Verbose "$($EventLogName) exists, skip create new event log." | |
} | |
} | |
catch { | |
Write-Warning $_.Exception.Message | |
} | |
} | |
else { | |
Write-Warning 'When creating a Windows Event Log you need to run as a user with elevated rights!' | |
$elevated = $false | |
} | |
} | |
else { | |
$logType = 'LogFile' | |
# create new log file if needed | |
if((-not $Append) -or (-not(Test-Path $Path))){ | |
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 | |
} | |
} | |
} | |
# create log object | |
$logObject = [PSCustomObject] [Ordered] @{ | |
LogType = $logType | |
LogFormat = $Format | |
Path = $Path | |
MaxLogSize = $MaxLogSize | |
MaxLogFiles = $MaxLogFiles | |
LogHeader = $Header | |
EventLogName = $EventLogName | |
EventLogSource = $EventLogSource | |
DefaultEventID = $DefaultEventID | |
Elevated = $elevated | |
} | |
# save logObject to a global variable | |
if($UseGlobalVariable){ | |
$global:PSLOG = $logObject | |
} | |
# unless UseGlobalValiable is false, then return it to the pipeline instead | |
else{ | |
Write-Output $logObject | |
} | |
} | |
function Write-Log { | |
<# | |
.SYNOPSIS | |
Write to the log. | |
.DESCRIPTION | |
The Write-Log function is used to write to the log. It is using the log object created by New-Log | |
to determine if it's going to write to a log file or to a Windows Event log. | |
.EXAMPLE | |
Write-Log 'Finished running WMI query' | |
Get the log object from $global:PSLOG and write to the log. | |
.EXAMPLE | |
$myLog | Write-Log 'Finished running WMI query' | |
Use the log object saved in $myLog and write to the log. | |
.EXAMPLE | |
Write-Log '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: Øyvind Kallstad | |
Date: 21.11.2014 | |
Version: 1.0 | |
#> | |
[CmdletBinding()] | |
param ( | |
# The text you want to write to the log. | |
[Parameter(Position = 0)] | |
[string] $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')] | |
[string] $LogType = 'Information', | |
# 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 global PSLOG variable. | |
[Parameter(ValueFromPipeline)] | |
[ValidateNotNullorEmpty()] | |
[object] $Log = $global:PSLOG, | |
# PassThru passes the log entry to the pipeline for further processing. | |
[Parameter()] | |
[switch] $PassThru | |
) | |
try { | |
# get information from log object | |
$logObject = $Log | |
# translate event types to CMTrace format | |
if ($logObject.LogFormat -eq 'CMTrace') { | |
switch ($LogType) { | |
'Error' {$cmType = '3';break} | |
'FailureAudit' {$cmType = '3';break} | |
'Information' {$cmType = '1';break} | |
'SuccessAudit' {$cmType = '1';break} | |
'Warning' {$cmType = '2';break} | |
DEFAULT {$cmType = '1'} | |
} | |
} | |
# get invocation information | |
$thisInvocation = (Get-Variable –Name 'MyInvocation' –Scope 1).Value | |
# get calling script info | |
if(-not ($thisInvocation.ScriptName)){ | |
$scriptName = $thisInvocation.MyCommand | |
$file = "$($scriptName)" | |
} | |
else{ | |
$scriptName = Split-Path –Leaf ($thisInvocation.ScriptName) | |
$file = "$($scriptName):$($thisInvocation.ScriptLineNumber)" | |
} | |
# get calling command info | |
$component = "$($thisInvocation.MyCommand)" | |
if ($logObject.LogType -eq 'EventLog') { | |
if($logObject.Elevated) { | |
# if EventID is not specified use default event id from the log object | |
if([system.string]::IsNullOrEmpty($EventID)) { | |
$EventID = $logObject.DefaultEventID | |
} | |
Write-EventLog –LogName $logObject.EventLogName –Source $logObject.EventLogSource –EntryType $LogType –EventId $EventID –Message $LogEntry | |
} | |
else { | |
Write-Warning 'When writing to the Windows Event Log you need to run as a user with elevated rights!' | |
} | |
} | |
else { | |
# create a mutex, so we can lock the file while writing to it | |
$mutex = New-Object System.Threading.Mutex($false, 'LogMutex') | |
# handle the different log file formats | |
switch ($logObject.LogFormat) { | |
'Minimal' { $logEntryString = $LogEntry; break } | |
'PlainText' { | |
# when component and file are equal | |
if($component -eq $file){ | |
$logEntryString = "$((Get-Date).ToString()) $($LogType.ToUpper()) [$($file)] $($LogEntry)" | |
} | |
# log entry when component and file are not equal | |
else{ | |
$logEntryString = "$((Get-Date).ToString()) $($LogType.ToUpper()) [$($component) – $($file)] $($LogEntry)" | |
} | |
break | |
} | |
'CMTrace' { | |
$date = Get-Date –Format 'MM-dd-yyyy' | |
$time = Get-Date –Format 'HH:mm:ss.ffffff' | |
$logEntryString = "<![LOG[$LogEntry]LOG]!><time=""$time"" date=""$date"" component=""$component"" context="""" type=""$cmType"" thread=""$pid"" file=""$file"">" | |
break | |
} | |
} | |
# write to the log file | |
[void]$mutex.WaitOne() | |
Add-Content –Path $logObject.Path –Value $logEntryString | |
$mutex.ReleaseMutex() | |
# invoke log rotation if log is file | |
if ($logObject.LogType -eq 'LogFile') { | |
Invoke-LogRotation | |
} | |
# handle PassThru | |
if ($PassThru) { | |
Write-Output $LogEntry | |
} | |
} | |
} | |
catch { | |
Write-Warning $_.Exception.Message | |
} | |
} | |
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: Øyvind Kallstad | |
Date: 21.11.2014 | |
Version: 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 = $global: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) { | |
# 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)) { | |
# 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) { | |
[int]$numTooMany = (($oldLogFiles.Count) + 1) – $log.MaxLogFiles | |
$oldLogFiles | Sort-Object 'LastWriteTime' | Select-Object –First $numTooMany | Remove-Item | |
} | |
} | |
} | |
} | |
} | |
} | |
catch { | |
Write-Warning $_.Exception.Message | |
} | |
} |