PSLogger.psm1

#requires -version 3
<#
    .SYNOPSIS
        PSLogger is a PowerShell module to enhance and simplify interacting with log files on the file system.
    .DESCRIPTION
        PSLogger provides functions to Write logs, find / enumerate recently written log files, and Read the latest lines of recent .log files.
    .NOTES
        NAME : PSLogger
        VERSION : 1.3.5
        LAST UPDATED: 11/16/2015
        AUTHOR : Bryan Dady | https://github.com/bcdady/
        Original Write-Log Author: Jeffery Hicks
        http://jdhitsolutions.com/blog
        http://twitter.com/JeffHicks
        Date: 3/3/2011
#>


# Setup necessary configs for PSLogger's Write-Log cmdlet
[string]$global:loggingPreference = 'Continue'
# set $loggingPreference to anything other than continue, to leverage write-debug or write-verbose, without writing to a log on the filesystem

# Define $loggingPath as .\WindowsPowerShell\log directory, under the user's profile Documents folder.
# To assure portability and compatability across client and server, and various OS versions, we use special Environment paths, instead of $HOME or $env:userprofile
# http://windowsitpro.com/powershell/easily-finding-special-paths-powershell-scripts
# If this path doesn't already exist, it will be created later, in Write-Log function
[string]$global:loggingPath = Join-Path -Path "$([Environment]::GetFolderPath('MyDocuments'))" -ChildPath 'WindowsPowerShell\log'
# Handle when special Environment variable MyDocuments is a mapped drive, it returns as the full UNC path.
if (([Environment]::GetFolderPath('MyDocuments')).Substring(0,2) -match '\\')
{
    $global:loggingPath = $loggingPath.Replace("$(Split-Path -Path "$([Environment]::GetFolderPath('MyDocuments'))" -Parent)"+'\',$(Get-PSDrive -PSProvider FileSystem | Where-Object -FilterScript {
                $PSItem.DisplayRoot -eq $(Split-Path -Path "$([Environment]::GetFolderPath('MyDocuments'))" -Parent)
    }).Root)

}

[string]$global:logFileDateString = Get-Date -UFormat '%Y%m%d'
# Need to keep an eye on this one, in case PowerShell sessions run for multiple days, I doubt this variable value will be refreshed / updated

New-Variable -Name LastFunction -Description "Retain 'state' of the last function name called, to streamline logging statements from the same function" -Force -Scope Global -Visibility Public
[bool]$writeIntro = $true
[string]$global:LastFunction

Function Write-Log
{
<#
    .Synopsis
        Write a message to a log file.
    .Description
        Write-Log can be used to write text messages to a log file. It can be used like Write-Verbose,
        and looks for two variables that you can define in your scripts and functions. If the function
        finds $LoggingPreference with a value of "Continue", the message text will be written to the file.
        The default file is PowerShellLog.txt in your %TEMP% directory. You can specify a different file
        path by parameter or set the $logFilePref variable. See the help examples.
 
        This function also supports Write-Verbose which means if -Verbose is detected, the message text
        will be written to the Verbose pipeline. Thus if you call Write-Log with -Verbose and a the
        $loggingPreference variable is set to continue, you will get verbose messages AND a log file.
    .Parameter Message
        The message string to write to the log file. It will be prepended with a date time stamp.
    .Parameter Function
        The Function Parameter passes the name of the Function or CmdLet that invoked the Write-Log function.
        This is used to write related log messages into a topical log file, instead of writing all log messages into a common file
        If not specified, this defaults to 'PowerShell'
    .Parameter Path
        The filename and path for the log file. The default is defined as $loggingPath (above)
        If $logFilePref variable is (bound) and passed into the Write-Log function, then that override path value will be used.
 
    .EXAMPLE
        PS .\>Write-Log -Message "Test Message ... this is a test of the Write-Log function" -Function TEST
 
    .EXAMPLE
        PS .\>Write-Log -Message "Test Message ... this is another test of the Write-Log function, to a custom specified path" -Function TEST -Path $env:TEMP\testing.log
 
    .Notes
        NAME: Write-Log
        AUTHOR: Bryan Dady, adapted from original work by Jeffery Hicks
        VERSION: 1.3.5
        LASTEDIT: 11/16/2015
 
    .Link
        http://jdhitsolutions.com/blog/2011/03/powershell-automatic-logging/
 
    .Link
        Write-Verbose
#>

    [cmdletbinding()]
    Param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'The message string to write to the log file. It will be prepended with a date time stamp.'
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false,
            Position = 1,
            HelpMessage = 'The Function Parameter passes the name of the Function or CmdLet that invoked this Write-Log function'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('Action', 'Source')]
        [String]$Function,

        [Parameter(
            Position = 2,
                ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
                ValueFromRemainingArguments = $false,
            HelpMessage = 'The optional Path parameter specifies the path of the log file to write the message to.'
        )]
        [ValidateScript({Test-Path -Path $PSItem -PathType Any})]
        [string]$Path
    )

    # Assign Function variable to 'PowerShell' if blank or null
    if (($Function -eq $null) -or ($Function -eq ''))
    {
        $Function = 'PowerShell'
    }

    # Detect if this Function is the same as the $LastFunction. If not, verbosely log which new Function is active
    if ($Function -eq $LastFunction)
    {
        # Retain 'state' of the last function name called, to streamline logging statements from the same function
        $writeIntro = $false
    }
    else
    {
        Set-Variable -Name LastFunction -Value $Function -Force -Scope Global
        $writeIntro = $true
    }

    # Detect -debug mode:
    # http://blogs.msdn.com/b/powershell/archive/2009/04/06/checking-for-bound-parameters.aspx
    # https://kevsor1.wordpress.com/2011/11/03/powershell-v2-detecting-verbose-debug-and-other-bound-parameters/
    if ($PSBoundParameters['Debug'].IsPresent)
    {
        [bool]$script:testMode = $true
        $logFilePref = Join-Path -Path "$loggingPath\test" -ChildPath "$("$Function", "$logFileDateString" -join '_').log"
    }
    else
    {
        [bool]$testMode = $false
        $logFilePref = Join-Path -Path "$loggingPath" -ChildPath "$("$Function", "$logFileDateString" -join '_').log"
    }

    # Pass on the message to Write-Debug cmdlet if -Debug parameter was used
    if ($testMode)
    {
        Write-Debug -Message $Message
        if ($writeIntro -and ( $Message -notlike 'Exit*'))
        {
            Write-Output -InputObject "Logging [Debug] to $logFilePref`n"
        }
    }
    elseif ($PSBoundParameters['Verbose'].IsPresent)
    {
        #Pass on the message to Write-Verbose cmdlet if -Verbose parameter was used
        Write-Verbose -Message $Message
        if ($writeIntro -and ( $Message -notlike 'Exit*'))
        {
            Write-Output -InputObject "Logging to $logFilePref`n"
        }
    }

    # Only write to the log file if the $LoggingPreference variable is set to Continue
    if ($loggingPreference -eq 'Continue')
    {

        # Before writing a copy of $Message to an output file, strip line breaks and/or other formatting that could interfere with clear/standardized logging
        $Message = $Message -replace "`n", ' '
        $Message = $Message -replace '\s{2,}', ' '

        #if $Path parameter was specified, then use it, otherwise use the derived $logFilePref
        if ($Path) {
            $LogFile=$Path
        } else {
            $LogFile=$logFilePref
        }

        # Confirm or create LogFile path, otherwise Out-File throws DirectoryNotFoundException;
        # Only need to do this once per unique $LogFile path, so use $writeIntro as that flag
        if ($writeIntro -and (-not (Test-Path -Path $(Split-Path -Path $LogFile -Parent) -PathType Container) ) )
        {
            Write-Output -InputObject "$(Get-Date) Creating logging path: $(Split-Path -Path $LogFile)" -NoEnumerate | Out-Host
            New-Item -Path $(Split-Path -Path $LogFile) -Force -ItemType Directory -ErrorAction Ignore
        }

        if ($testMode)
        {
            Write-Output -InputObject "$(Get-Date) [Debug] $Message" -NoEnumerate | Out-File -FilePath $LogFile -Append
        }
        elseif ($PSBoundParameters['Verbose'].IsPresent)
        {
            Write-Output -InputObject "$(Get-Date) [Verbose] $Message" -NoEnumerate | Out-File -FilePath $LogFile -Append
        }
        else
        {
            Write-Output -InputObject "$(Get-Date) $Message" -NoEnumerate | Out-File -FilePath $LogFile -Append
        }
    }
} #end function

Function Read-Log
{
<#
    .Synopsis
        Reads the latest log file, optionally displaying only the latest number of specified lines
    .Description
        Intended as a complement to the Write-Log function provided within the PSLogger module, Read-Log will find the latest log file in the
        defined $loggingPath directory, and return some basic stats of that file, as well as get it's contents for review
    .Parameter MessageSource
        The message source is an optional parameter that specifies which module, function, or script wrote the log file to be retrieved.
        If this parameter is not specified, function returns the latest available log file, regardless of message source.
        The value of this parameter becomes a filter to the search of .log files within the $loggingPath directory.
        .Parameter lineCount
        The most recent number of lines from the log file in question.
        unless the $logFilePref variable is found. If so, then this value will be used.
    .Example
        PS .\> Read-Log
 
        Returns basic file properties, and last 10 lines, of the latest / newest log file found in $loggingPath directory
 
    .Example
        PS .\> Read-Log -MessageSource Get-Profile -lineCount 30
 
        Returns latest log file, reading the latest 30 lines, specific to function Get-Profile
 
    .Notes
        NAME: Read-Log
        AUTHOR: Bryan Dady
        VERSION: 1.0
        LASTEDIT: 04/15/2015
 
    .Output
        Matches default properties return the same as Get-Item:
        * Name
        * LastWriteTime
        * Length
        * Path
#>

    [cmdletbinding()]
    Param(
        [Parameter(
            Mandatory = $false,
            Position = 0,
            HelpMessage = 'The message source typically matches a log filename string, which represents the name of the function or module which wrote the log file.'
        )]
        [Alias('function','source','f','m')]
        [string]$MessageSource,

        [Parameter(
            Mandatory = $false,
            Position = 1,
            HelpMessage = 'Provide an integer of line numbers to read from the bottom of the log file.'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('lines', 'l')]
        [int16]$lineCount = 10
    )
    # Use write-output instead of write-log, so that the function of reading log files does not write new log files to be read
    Write-Output -InputObject "Selecting latest log file to read last $lineCount lines"

Write-Output -InputObject "[Debug] Looking for log files with `$MessageSource is $MessageSource" -Debug
    # Select the newest (1) file (most recent LastWriteTime), with an optional filter, based on MessageSource parameter
    $latestLogFile = Get-ChildItem -Path $loggingPath -Filter *$MessageSource* -File |
    Sort-Object -Property LastWriteTime -Descending |
    Select-Object -First 1

Write-Output -InputObject "[Debug] `$latestLogFile is $latestLogFile" -Debug

    if (Test-Path -Path $latestLogFile -PathType Leaf -IsValid -ErrorAction Ignore)
    {
        Write-Output -InputObject "Selected $latestLogFile"
        $latestLogFile |
        Select-Object -Property Name, LastWriteTime |
        Format-List
        if ($lineCount -gt 0)
        {
            Write-Output -InputObject "`n ... "
            $latestLogFile | Get-Content -Tail $lineCount
            Write-Output -InputObject "`n[EOF]`n"
        }
    } else
    {
        Write-Warning -Message "Could not open $latestLogFile for reading"
    }
} #end function

Function Get-LatestLogs
{
    Get-ChildItem -Path $loggingPath |
    Sort-Object -Descending -Property LastWriteTime |
    Select-Object -First 10
}

# Export-ModuleMember -Function Backup-Logs, Get-StringHash, Write-Log, Read-Log, Get-LatestLogs, Show-Progress -Alias Archive-Logs