Frank Detzer

why i wrote a Power Shell LogEngine and ended up not using it

why i wrote a Power Shell LogEngine and ended up not using it

When I started working with Power Shell 2 years ago I slowly but surely came to the realization that I wanted to build a functionality that log everything you do. So, I did exactly that and build one.

I started out piecing everything together and while I intended to just have one function that you can integrate in the begin{}-block of your script or dot source it the need arose to build multiple functions for this single purpose and grow it into a bigger module. In the module itself you either need a $global:-variable or return and pipe everything to the next module. There is nothing wrong about using a $global:-variable but you should avoid using these whenever you can. With every $global:- & $script:-variable you create every Power Shell Session will load these and they will take the name space and additional resources. That might not seem something worth considering about but especially in a case like this, where you have a script that automates a workload that fills a lot of resources dedicated to these variables it surely will slow down all other Power Shell sessions you are running. Imagine this, you execute a script on a server, and it will produce a lot of output (therefore will consume a lot of resources) and a colleague just want to fire up a script that does a simple task like renaming the computer. Does this Power Shell session really need all that info? No, it does not. There is maybe a point about that do not begin true for Power Shell remoting, which I had not the chance to test.

After working on for this module now for a while I came to point where I wanted to utilize the [Start/Stop]-Transcript-Cmdlets. Surely, they would make everything easier because they already do what I wanted to build. Not really. I have the feeling the usage of the Transcript-Cmdlets would limit the ability of the log-functionality. They do what they are designed to do in the way they are intended to do. So, if you do not trigger them at the very beginning or for in a specific context Power Shell ether collect to much data or maybe miss something you wanted to keep.

That led me investigating the Power Shell debugger which led me directly to the $Error-variable. $Error is a collection of all the error(s) that your script/line of code produces - therefore the name. The more I investigated this the more I liked it because it contains everything you need. I am a big fan of the KISS principle (“keep it short and simple”) and would argue that almost never run into situations where you need a full console log (that outputs everything into a file afterwards) in Power Shell. If Power Shell comes across an error, it really depends on the setting of the $ErrorActionPreference which defaults to continue. So, if there is something that is not a terminating error it simply skips that and moves on and outputs the error in the console. This is fine until you produce so many errors that the screen buffer is filled or close your terminal.


#method 1, using the $Error-variable

To counter the effect of a lost $Error-log you can simply export the variable to a file at the end of your script. The following function snippet contains everything you need for this in a very tidy way:

function Use-ExampleErrorVariableOutput {
    param (
        [string]$WorkingDirectory = $PSScriptRoot,
        [...]
    )

    begin {
        $RunDateFileDateTimeUniversal = Get-Date -Format FileDateTimeUniversal
        $ScriptFiles = New-Item -Path $WorkingDirectory -ItemType Directory -Name 'ScriptFiles'
        [...]
    }

    process {
        $ErrorContainer = @()

        # Examples that will produce an error
        Get-ChildItem -Path '//a' -ErrorVariable lserror
        $ErrorContainer += $lserror

        Get-ChildItem -Path '//b' -ErrorVariable lserror
        $ErrorContainer += $lserror
        
        Get-ChildItem -Path '//c' -ErrorVariable lserror
        $ErrorContainer += $lserror
    }

    end {
        $ErrorContainer | Out-File -Path "$($ScriptFiles)\ErrorContainer_$($RunDateFileDateTimeUniversal).txt" -Encoding UTF8
    }

#method 2, using the-ErrorVariable parameter You can also pipe the single error of the Cmdlet you are using into a variable, you just need to define a namespace. If you add the Parameter ‘-ErrorVariable SingleError’ you would call the variable by using (Get-Variable Error).Value or $SingleError. If you want to collect the Errors of certain Cmdlet(s) you need to define an Array Collection and add them to the array. The example below shows how that is done.

function Use-ExampleErrorVariableOutput {
    param (
        [string]$WorkingDirectory = $PSScriptRoot,
        [...]
    )

    begin {
        $RunDateFileDateTimeUniversal = Get-Date -Format FileDateTimeUniversal
        $ScriptFiles = New-Item -Path $WorkingDirectory -ItemType Directory -Name 'ScriptFiles'
        [...]
    }

    process {
        $ErrorContainer = @()

        # Examples that will produce an error
        Get-ChildItem -Path '//a' -ErrorVariable lserror
        $ErrorContainer += $lserror

        Get-ChildItem -Path '//b' -ErrorVariable lserror
        $ErrorContainer += $lserror
        
        Get-ChildItem -Path '//c' -ErrorVariable lserror
        $ErrorContainer += $lserror
    }

    end {
        $ErrorContainer | Out-File -Path "$($ScriptFiles)\ErrorContainer_$($RunDateFileDateTimeUniversal).txt" -Encoding UTF8
    }

Using the -ErrorVariable parameter method gives us one advantage that I wish that would be already included in method #1. We can add a time stamp :).

function Use-ExampleErrorVariableOutputWithTimeStamp {
    param (
        [string]$WorkingDirectory = $PSScriptRoot,
        [...]
    )

    begin {
        $RunDateFileDateTimeUniversal = Get-Date -Format FileDateTimeUniversal
        $ScriptFiles = New-Item -Path $WorkingDirectory -ItemType Directory -Name 'ScriptFiles'
        [...]
    }

    process {
        $ErrorContainer = @()

        # Examples that will produce an error
        Get-ChildItem -Path '//a' -ErrorVariable lserror
        $lserror | Add-Member -MemberType NoteProperty -Name 'TimeStamp' -Value ((Get-Date).ToString())
        $ErrorContainer += $lserror
        
        Get-ChildItem -Path '//b' -ErrorVariable lserror
        $lserror | Add-Member -MemberType NoteProperty -Name 'TimeStamp' -Value ((Get-Date).ToString())
        $ErrorContainer += $lserror
        
        Get-ChildItem -Path '//c' -ErrorVariable lserror
        $lserror | Add-Member -MemberType NoteProperty -Name 'TimeStamp' -Value ((Get-Date).ToString())
        $ErrorContainer += $lserror
    }

    end {
        $ErrorContainer | Out-File -Path "$($ScriptFiles)\ErrorContainer_$($RunDateFileDateTimeUniversal).txt" -Encoding UTF8
    }

So, all this talk and I did not even come to my Log Engine that I wrote. Well, I am releasing it on GitHub and linking to it. There is maybe someone who can use the code maybe even improve it. The state of the Log Engine is working but would need an update here and there. You can find the code here.


Photo by Anthony Riera on Unsplash

why i wrote a Power Shell LogEngine and ended up not using it
Prev post

pwsh quick tip Check for Weekday/Weekend