Logging in Powershell

13 May 2020

I am always a big fan to have some logging in the software/scripts I develop. A log gives details what happened inside the software/script and it can be useful for debugging or validation purposes. So I decided to write a Powersehll logging script as part of my Powershell library scripts, below the logging script.


function Write-LogEntry{
[CmdletBinding()]
param(
        [Parameter (Mandatory = $True, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$FullLogFile
        ,
        [Parameter (Mandatory = $True, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("Information", "Warning", "Error")]
        [string]$Severity
        ,        
        [Parameter (Mandatory = $True, Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]$Message
    );

    [pscustomobject]@{
        Time = (Get-Date -Format "dd-MM-yyyy HH:mm:ss:ms")
        Function = $([string]$(Get-PSCallStack)[1].Command)
        Severity = $Severity
        Message = $Message
    } | Export-Csv -Path $FullLogFile -Append -NoTypeInformation -Delimiter "," -Encoding UTF8;

};

function Get-FullLogFileName{
[CmdletBinding()]
param(
        [Parameter (Mandatory = $false, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [bool]$CreateLogFolder = $false
    );

    try{
        # script root folder
        [string]$Path = $PWD.Path;

        [string]$LogFileName = $([System.IO.Path]::GetFileNameWithoutExtension($Global:MyInvocation.MyCommand.Name));

        if ($CreateLogFolder){
            $Path = Join-Path -Path $Path -ChildPath "Log";
        };

        [string]$Log = Join-Path -Path $Path -ChildPath "$($LogFileName)_$(Get-Date -format "yyyyMMdd_HHmm").txt";

        if (-not(Test-Path -Path $Path)){
            New-Item -Path $Path -ItemType Directory | Out-Null;
            Write-LogEntry -FullLogFile $Log -Severity Information -Message "Created Log Folder: $($Log)";
        };

        Write-LogEntry -FullLogFile $Log -Severity Information -Message "Created Log File: $($Log)";
        Write-GeneralLogProperty -FullLogFile $Log;
    } catch{
        Write-LogEntry -FullLogFile $Log -Severity Error -Message $($_);
        $Log = "";
    };

    return ($Log);

};

function Write-GeneralLogProperty{
[CmdletBinding()]
param(
        [Parameter (Mandatory = $True, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$FullLogFile
    );

    Write-LogEntry -FullLogFile $FullLogFile -Severity Information -Message "UserName: $($env:USERNAME)";
    Write-LogEntry -FullLogFile $FullLogFile -Severity Information -Message "ClientName: $($env:CLIENTNAME)";
    Write-LogEntry -FullLogFile $FullLogFile -Severity Information -Message "UserDomain: $($env:USERDOMAIN)"; 

};

The main objective was to get a clean and easy to use logging script. Below a bit more detail about my requirements.

Not a module
I decided not to write a real module because the script should easily be re-usable in different kind of situations e.g. when I am on-site and do not want (or even not allowed) to install a module. The downside is the main script needs always a reference to the logging script.

Log file and its place
The name of the log file should be the same as the main script included a timestamp (date and time). With $Global:MyInvocation.MyCommand.Name you will get the script name and Get-Date -format "yyyyMMdd_HHmm gives you the required timestamp. The log file itself is just a UTF 8 text file with the four columns: Time, Function, Severity and Message. In the log file those columns are comma-separated. As as text file I can use e.g. notepad to open it. As an option the log files can be created in the folder Log and this folder will be created automatically. The default location is the main script folder.

Severity Levels
Only the severity levels Information, Warning and Error are needed because I only need basic logging in my Powershell scripts. Other severity levels like Critical are not needed. For some severity level inspiration have a look on this one or here.

Only 2 functions
I need only two functions. One in the beginning of the main script, the function Get-FullLogFileName creates the log file and optional the log folder and it returns a fully qualified file name. During the execution of the main script it uses the function Write-LogEntry to log a particular message with a severity level (Information, Warning or Error). The value of the function field in the log file is taken from Powershell using Get-PSCallStack. For the severity levels Information and Warning you should provide a message, so you can use those levels everywhere in your script. Only in the case of an error the message is taken from the Powershell framework. The Error severity level is always a part of the catch block.


Below a Powershell script how you can use the logging script.


Clear-Host;

# reference logging script
Push-Location $PSScriptRoot;
."$PSScriptRoot\LogFunctions.ps1";

# Log folder is created
#[string]$FullLogFile = Get-FullLogFileName -CreateLogFolder $true;

# Log file is created in the root of the script
# Parameter -CreateLogFolder is optional, default values is false
[string]$FullLogFile = Get-FullLogFileName;

try{
    $res = 10/0##10.5;
    $threshold = 1;

    Write-LogEntry -FullLogFile $FullLogFile -Severity Information -Message "Result: $($res)";

    if($res -lt $threshold){
        Write-LogEntry -FullLogFile $FullLogFile -Severity Warning -Message "Result is below the threshold value of $($threshold)";
    };

} catch{
    Write-LogEntry -FullLogFile $FullLogFile -Severity Error -Message $($_);
};

[System.GC]::Collect();

Have fun!