/Posh-Logger

Logging module written in Power Shell

Primary LanguagePowerShell

Posh-Logger

Logging module written in Power Shell

Originally, this solution was conceived to meet the need for "logging" the execution of any procedure, and it can be imported into any solution where PowerShell code can be used. This solution was originally written during free time between projects to keep my mind occupied and to continue using my development skills. The entire module was designed and written in 4 full days of work, and this represents the initial release of the solution as a product.

Considering Microsoft's significant effort towards integrating with Linux systems, we can confidently state that this solution can be imported into various platforms and solutions, supporting a wide range of languages and architectures.

You can obtain it by cloning the repository: https://github.com/ottogori/Posh-Logger.git

Along with the code, an extensive "how to" has been written through comments formalized in the "help" section of each of the complex functions. Therefore, I will focus on demonstrating the functionality of its main modules without delving too much into the explanation of the encapsulated and/or secondary procedures. These "how to" guides can be accessed using the get-help function for each of the procedures. An example of this is shown below:

    <#Write-OPSLogInput
    .SYNOPSIS
        Writes a log input to a stream based on the log level.

    .DESCRIPTION
        This function chooses the stream to write to based on the log level parameter and writes the input to the chosen stream.

    .PARAMETER logInput
        Input to write to the stream.
        
    .PARAMETER logLevel
        Level of the log input to determine which stream to write to.
        - Info and Verbose writes to the Verbose stream.
        - Debug writes to the Debug stream
        - Warning writes to the Warning stream
        - Error writes to the Error stream
        
        This is an optional parameter. If not set, it will write to the verbose stream.
        
    .NOTES
        Author: Otto Gori
        Data: 06/2017
        testVersion: 0.0
        Should be run on systems with PS >= 3.0
        
    .INPUT EX
        Add-OPSLogInput -logInput "Initiating foobar"
        Add-OPSLogInput -logInput "Error on foobar" -logLevel Error
        
    .OUTPUTS
        Null
        
    #>
    function Write-OPSLogInput{
    [CmdletBinding()]
    param(
        [parameter(Position=0, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()][string]$logInput = $(throw "logInput is mandatory and was not set."),
        [ValidateSet('verbose','info','debug','warning','error')][string]$logLevel = "verbose"
    )
        process {
            switch ($logLevel) {
                warning{Write-Warning $logInput}
                error{Write-Error $logInput}
                debug{Write-Debug $logInput}
                {(($_ -eq 'verbose') -or ($_ -eq 'info'))}{Write-Verbose $logInput}
                Default{Write-Verbose $logInput}
            }
        }
    }

That said, let's move on to the initialization of the logging procedure, which requires the parameters demonstrated below and is initiated on the last line of the code snippet.

    # Set power shell stream handling preferences
    $VerbosePreference = "Continue"
    $DebugPreference = "SilentlyContinue"
    $ErrorActionPreference = "Stop"

    # Define global variables
    $global:stepInitialize = 1
    $global:stepExecCmd = 2
    $global:stepValidate = 3
    $global:totalSteps = 3

    # Initialize log
    $global:logLevel = "Debug"   # Possible values: Error, Warning, Info and Debug
    New-OPSLogger -logPath "$PSScriptRoot\logs" -actionName 'Automation' | Out-Null

The architecture of this solution allows you to create logging with different complexities simultaneously.

This is achieved by providing the option to log in "debug" or "info" mode, which allows for detailed logging as well as a simpler and more user-friendly log, which can be attached to an email, for example, at the end of a deployment procedure or environment preparation. A third option is the error state log, which, in addition to the developer's comments, includes the original error message, enabling an accurate diagnosis of the problem.

The debug-level log includes everything from the execution step listed by the developer to the function where the error originated. On the other hand, the informational-level log is more descriptive and easier to interpret, making it suitable for business reporting, if required (though in rare situations).

The solution even includes a log rotation procedure, filtering the logs by date and name.

    function Delete-OPSOldFiles {
        [CmdletBinding()]
        Param(
            [parameter(Position = 0,
                    ValueFromPipeline=$true,
                    ValueFromPipelineByPropertyName=$true
                    )][Alias('FullName')]
            [ValidateNotNullOrEmpty()][string]$path = $(throw "path is mandatory and was not set."),
            [parameter(Mandatory=$true)]
            [int]$days,
            [string]$filter = "*"
        )
        begin {
            $limit = (Get-Date).AddDays(-$days)
        }
        process {
            Add-OPSLoggerInput "Deleting files from $path\$filter older then $limit ($days)..." -logLevel Info -invocation $MyInvocation
            Get-ChildItem "$path" -filter $filter | `
                        Where-Object { -not $_.PSIsContainer -and $_.CreationTime -lt $limit } | `
                        Add-OPSLoggerInput -format "Deleting file {0}" -logLevel Debug -invocation $MyInvocation -passthru | `
                        Remove-Item
        }
    }

    #Call for this function
    Delete-OPSOldFiles -path "$PSScriptRoot\logs" -days 90 -filter *.log -ErrorAction $DebugPreference

The execution of the log files initialization:

    <#New-OPSLogger
    .SYNOPSIS
        Create a new pair of summary and detailed log files.

    .DESCRIPTION
        Creates two new log files on $logPath directory, one for detailed log and one for summary log, following the naming convention below.
        [Current date as dd-MM-yyyy] [$actionName] detailed.log
        [Current date as dd-MM-yyyy] [$actionName] summary.log
        Examples:
            11-02-2016 AllinOne_5.0.3.5_Update detailed.log
            11-02-2016 AllinOne_5.0.3.5_Update summary.log
        
        If any of the log files already exists, it will rename the existing file with a number version at the end
        unless explicitly requesting to replace any existing file with the alwaysReplace parameter.

    .PARAMETER logPath
        Path to where the log file will be created
        
    .PARAMETER actionName
        Name of the package to use as part of the file name to identify which package processing created the log file
        
    .PARAMETER alwaysReplace
        This is a switch parameter. If set, will always replace log file if one exists with the same name.
        
    .PARAMETER alwaysReplace
        This is a switch parameter. If set, The logger object stored in $global:logger will be returned.
        
    .NOTES
        Author: Otto Gori
        Data: 06/2017
        testVersion: 0.0
        Application user must have permition to create and rename files on the directory specified by $logPath parameter
        Should be run on systems with PS >= 3.0

    .INPUT EX
        New-OPSLogger -logPath "C:\logs" -actionName "AllinOne_5.0.3.5_Update"
        New-OPSLogger -logPath "C:\logs" -actionName "AllinOne_5.0.3.5_Update" -alwaysReplace
        
    .OUTPUTS
        If passthru is set, a Dictionary with a SummaryLogFile member containg the log path, the full path to the newly created summary log file
        and a DetailedLogFile member containg the full path to the newly created detailed log file will be returned.
        The returned object is also stored in $global:logger
        If passthru is not set, the Dictionary will just be stored in $global:logger and not be returned.
        
    #>
    function New-OPSLogger{
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()][string]$logPath = $(throw "logPath is mandatory and was not set."),
        [ValidateNotNullOrEmpty()][string]$actionName = $(throw "actionName is mandatory and was not set."),
        [switch]$alwaysReplace,
        [switch]$passthru
    )
        $summaryLogFile = New-OPSLogFile -logPath $logPath -actionName $actionName -logType summarized -alwaysReplace:$alwaysReplace
        $detailedLogFile = New-OPSLogFile -logPath $logPath -actionName $actionName -logType detailed -alwaysReplace:$alwaysReplace
        
        $global:logger = @{
            LogPath = $logPath
            SummaryLogFile = $summaryLogFile
            DetailedLogFile = $detailedLogFile
        }
        
        if ($passthru) {
            Write-Output $global:logger
        }
    }

It will create two logs following the standard nomenclature described below: [Current date as dd-MM-yyyy] [$actionName] detailed.log [Current date as dd-MM-yyyy] [$actionName] summary.log

Example: 11-02-2016 AllinOne_5.0.3.5_Update detailed.log 11-02-2016 AllinOne_5.0.3.5_Update summary.log

With the contents shown below, where a call to the nonexistent function "asd" was included to illustrate the third case, an error.

To include more data in the logs, simply use the following function calls:

Add-OPSLoggerInput -logInput "Initiating foobar" -logLevel Info -silent -invocation $MyInvocation

Or for errors:

Add-OPSLoggerException "Error on foobar" -step "foobar" -invocation $MyInvocation