Write-Log Function with Context
Along the same lines as my previous post, I wanted to share the latest version of my logging function. I find this function particularly useful when writing verbose log entries that need to be searched in an automated fashion. It’s a good practice to implement full tracking of every automated action such as a server reboot, user modifications, etc. This function will output the data to a file which can be read in as a CSV and converted into objects. These logging entries can then be searched for without having to manually search text files.
function Write-Log {
param(
[string]$Message,
[string]$EntryType = "Info",
[string]$ObjectContext,
[string]$CodeContext,
[string]$Details,
$LogName = "output.log",
$LogPath
)
try {
if ([string]::IsNullOrEmpty($LogPath)) {
$LogPath = $pwd.path
}
if (!(Test-Path $LogPath\$LogName -ErrorAction SilentlyContinue)) {
"Timestamp,EntryType,Message,ObjectContext,CodeContext,Details" | Out-File -FilePath $LogPath\$LogName
}
foreach ($paramInfo in $MyInvocation.BoundParameters.Keys) {
#If parameter passed in contains a comma, wrap the entry in quotes to not break CSV delimiters.
$parameterData = $MyInvocation.BoundParameters[$paramInfo].ToString()
if ($parameterData.Contains(",")) {
Set-Variable "$paramInfo" -Value $parameterData.Replace(
"$parameterData","`"$parameterData`"")
}
}
"$([DateTime]::Now.ToString("MM-dd HH:mm:ss")),$EntryType,$Message,$ObjectContext,$CodeContext,$Details".Replace("\n",";") | `
Out-File -Append -FilePath "$LogPath\$LogName"
}
catch {
Write-Warning "Unexpected error occured while executing $((Get-PSCallStack)[0].Command) with exception: $($_.Exception)"
Write-Warning "Command: `'$($_.InvocationInfo.Line.Trim())`'"
}
}
The overall concept is we have a few key properties that make up the log type. We will of course need a Message property, as well as a Timestamp when the message was written. Key to large scale scripts is an ObjectContext and a CodeContext which store contextual information about executing code and objects that code is operating against. The intent of the ObjectContext property should be a string representation of the object you are making a log entry about. Having an object context makes searching the logs easy for something that happened to a particular user account for example – just load the log into memory and execute a search against ObjectContext and you can see every log entry against that user account. Once these entries are logged, it’s easy to read them back into your PowerShell session and execute queries.
Writing to the log is simple – the function will automatically create the file and initialize header information if the file did not exist. It will also automatically encapsulate log entries with quotes if the value contains a comma which is our CSV delimiter.
Here are a couple examples of searching the log data we just created. First, we want to see any log entries that mention the w32time service as the ObjectContext. In the results, we can see the service starting and stopping. The next example will search for Messages of a certain type, in this case any time a service was stopped. Alternatively if we were logging CodeContext, we could look for a function name or something similar.
This may not be the best logging solution for every scenario, but it works especially well when there are hundreds or thousands of log events being created in one file.