This project is read-only.

Write-Log: Create log file in regular format or SCCM 2012's CMTrace.exe format

Topics: Archive - Toolkit Extensions
Jul 9, 2014 at 6:56 AM
Edited Jul 15, 2014 at 6:37 PM
For some reason, the "plus" sign is being converted to "+" when I post code here. Please fix this before you use code.

Dependent function: Write-ErrorStack function is posted in another thread on this forum.

The function is too long for one post so it will be split into two posts:
Function Write-Log
{
    <#
    .SYNOPSIS
        Write messages to a log file in CMTrace.exe compatible format or regular file format.
        
    .DESCRIPTION
        Write messages to a log file in CMTrace.exe compatible format or regular file format.
        The default option is to create a log file in a CMTrace.exe compatible format.
        Messages can also be logged to file and displayed on the console at the same time.
        The default option is to only display messages on the console if there was an error logging to file.
        You can choose to suppress all error messages generated by this function so that nothing is displayed on the console.
        
    .PARAMETER LogFileDirectory
        Set the directory where the log file will be saved. Default is %PROGRAMDATA%\Logs.
        If this does not exist and cannot be created, it will default to %TEMP%
        
    .PARAMETER LogFileName
        Set the name of the log file, default is the name of the script.
        
    .PARAMETER ScriptSection
        The heading for the portion of the script that is being executed
        Use $script:ScriptSection = 'Section Name' to control the value of this variable throughout the calling script
        
    .PARAMETER Message
        The message you wish to write to the log file.
        You can pipe messages to this function but they will all have the same time stamp so
        only use to seperate multiple messages about the same script event into separate lines.
        This function will accept an array of messages. Piping/arrays can be used to add
        spacing and create nice formatting.
        
    .PARAMETER Component
        The source of the message in the script.
        This should be the name of the function where the message is being generated
        or the name of the calling script.
        
    .PARAMETER Severity
        Defines message type. When writing to console or CMTrace.exe log format,
        it allows highlighting of message type.
        Options:
                1 = Information
                2 = Warning     (highlighted in yellow)
                3 = Error       (highlighted in red)
        
    .PARAMETER Thread
        Thread where log information comes from.
        Valid only from 0 (0x0000) to 65535 (0xFFFF). Default is 1.
        It is the only variable specific to the CMTrace log type.
        
    .PARAMETER LogType
        Choose whether to write a CMTrace.exe compatible log file or a normal text log file.
        Default is to create a CMTrace.exe compatible log file.
        Options:
                CMTrace
                Text
        
    .PARAMETER WriteHost
        Write the log message to the console.
        
    .PARAMETER SuppressHostOnError
        Suppress writing log message to console on failure to write message to log file.
        
    .PARAMETER MaxLogFileSizeMB
        Maximum file size limit for log file in megabytes (MB). Default is 10 MB.
        If file size limit is set to 0, log file is never renamed.
        Archive existing log file from "<filename>.log" to "<filename>.lo_"
        Overwrites any existing "<filename>.lo_" file.
        This is the same method SCCM uses for log files.
        
    .EXAMPLE
        $script:ScriptSection = "Install Patch"
        Write-Log -Message "Installing patch MS15-031" -Component 'Add-Patch' -Severity 1
        Write-Log -Message "Installation of patch MS15-031 failed" -Component 'Add-Patch' -Severity 3
        $script:ScriptSection = "OS Check"
        Write-Log -Message "Script is running on Windows 8" -Component 'Test-ValidOS' -Severity 1
        
        Log Output Display In CMTrace.exe Format:
        
        Log Text                                                Component       Date/Time               Thread
        ________                                                _________       _________               ______
        Install Patch :: Installing patch MS15-031              Add-Patch       7/8/2014 5:45:56 PM     1(0x1)
        Install Patch :: Installation of patch MS15-031 failed  Add-Patch       7/8/2014 5:46:45 PM     1(0x1)
        OS Check :: Script is running on Windows 8              Test-ValidOS    7/8/2014 5:47:34 PM     1(0x1)
        
        NOTE: Second log line entry above would be highlighed in red because it is an error message.
        
    .EXAMPLE
        $script:ScriptSection = "Install Patch"
        Write-Log -Message "Installing patch MS15-031" -Component 'Add-Patch' -Severity 1 -LogType 'Text'
        Write-Log -Message "Installation of patch MS15-031 failed" -Component 'Add-Patch' -Severity 3 -LogType 'Text'
        $script:ScriptSection = "OS Check"
        Write-Log -Message "Script is running on Windows 8" -Component 'Test-ValidOS' -Severity 1 -LogType 'Text'
        
        Log Output Display In Regular File Format:
        
        [07-09-2014 15:18:15.7010] [Add-Patch] Install Patch :: Installing patch MS15-031
        [07-09-2014 15:18:16.7010] [Add-Patch] Install Patch :: Installation of patch MS15-031 failed
        [07-09-2014 15:18:17.7010] [Test-ValidOS] OS Check :: Script is running on Windows 8
        
    .NOTES
        Tool for viewing the CMTrace log type:
        CMTrace.exe is a log file viewer that comes with SCCM 2012.
        CMTrace.exe  - Installation directory on Configuration Manager 2012 Site Server - <Install Directory>\tools\
        If you have the SCCM 2012 client tools installed, CMTrace.exe can also be found here:
            C:\Program Files (x86)\ConfigMgr 2012 Toolkit R2\ClientTools\CMTrace.exe
    #>
    
    [CmdletBinding()]
    Param
    (
        # Set the directory where the log file will be saved
        # Set the directory path here so that we do not have to pass the path with each function call
        # If error creating log directory, then it will be set to %TEMP%
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [string]$LogFileDirectory = "$env:PROGRAMDATA\Logs",
        
        # Set the name of the log file, default is the name of the script
        # Set the file name here so that we do not have to pass the name with each function call
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [string]$LogFileName = [System.IO.Path]::GetFileNameWithoutExtension($script:MyInvocation.MyCommand.Definition) + '.log',
        
        # The information to log
        # You can pipe messages to this function but they will all have the same time stamp so
        #  only use to seperate multiple messages about the same script event into separate lines.
        # You can also pass an array of messages to this function.
        # One way to use arrays for this variable would be to add spacing or special formatting
        #  to make log output look nicer without having to make multiple calls to the function.
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string[]]$Message,
        
        # The heading for the portion of the script that is being executed
        # Use $script:ScriptSection = 'Section Name' to control the value of this variable throughout the main script
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [string]$ScriptSection = $script:ScriptSection,
        
        # The source of the message, usually the function name
        #  or the script name if used in the calling script and not in a function
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$Component,
        
        # The severity (1=Information, 2=Warning, 3=Error)
        # CMtrace.exe will highlight "Warning" messages as "Yellow" and "Error" messages as "Red"
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateRange(1,3)]
        [int16]$Severity,
        
        # Thread where log information comes from. It is the only variable specific to the CMTrace log type.
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [ValidateRange(0,65535)]
        [int32]$Thread = 1,
        
        # Choose whether to write a CMTrace.exe compatible log file or a normal "text" log file
        # Default is to create a CMTrace.exe compatible log file
        [Parameter(Mandatory=$false)]
        [ValidateSet("CMTrace","Text")]
        [string]$LogType = 'CMTrace',
        
        # Write log entry to the console
        [Parameter(Mandatory=$false)]
        [switch]$WriteHost,
        
        # Suppress writing log message to console on failure to write message to log file
        [Parameter(Mandatory=$false)]
        $SuppressHostOnError = $false,
        
        # Maximum file size limit for log file in megabytes (MB)
        # If file size limit is set to 0, log file is never renamed/archived
        # Archive existing log file from <filename>.log to <filename>.lo_
        # Overwrites any existing <filename>.lo_ file
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [decimal]$MaxLogFileSizeMB = 10
    )
Jul 9, 2014 at 1:03 PM
Edited Jul 22, 2014 at 5:06 PM
    Begin
    {
        # First initialize the date/time variables
        $LogTime = Get-Date -Format HH:mm:ss.fff
        $LogDate = Get-Date -Format MM-dd-yyyy
        
        # Get the name of this function
        ${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
        
        # Get the parameters that this function was invoked with
        $PSParameters = New-Object -TypeName PSObject -Property $PSBoundParameters
        
        # Test to see if variable holding path to log file exists
        $LogFileVariableExists = Test-Path 'variable:script:OutLogFile'
        
        If (-not $LogFileVariableExists)
        {
            # Find the timezone bias
            $script:LogTimeZoneBias = $(Get-WmiObject -ComputerName $env:COMPUTERNAME -Query "SELECT Bias FROM Win32_TimeZone" -ErrorAction 'SilentlyContinue').Bias
            
            # Create the directory where the log file will be saved
            If (!(Test-Path $LogFileDirectory))
            {
                Try
                {
                    New-Item -Path $LogFileDirectory -Type 'Directory' -Force -ErrorAction 'Stop' | Out-Null
                }
                Catch
                {
                    # If error creating directory, then set log directory to %TEMP%
                    $LogFileDirectory = $env:TEMP
                }
            }
            
            # Assemble the fully qualified path to the log file
            [string]$LogFilePath = Join-Path -Path $LogFileDirectory -ChildPath $LogFileName
            
            # Set variable so that this "if block" is not executed again during the execution of this script
            Set-Variable -Name 'OutLogFile' -Value $LogFilePath -Scope 'Script'
        }
        
        # Check if the script section is defined
        # Use $script:ScriptSection = 'Section Name' to control the value of this variable throughout the script
        $ScriptSectionDefined = $false
        If ((Test-Path 'variable:ScriptSection') -and ((Get-Variable -Name 'ScriptSection' -Value) -ne $null))
        {
            $ScriptSectionDefined = $true
        }
        
        # Add the timezone bias to the log time
        $LogTimePlusBias = $LogTime + $script:LogTimeZoneBias
        
        # Create script block for generating CMTrace.exe compatible log entry
        $LogString = {
            Param
            (
                $lMessage,
                $lLogTimePlusBias,
                $lComponent,
                $lSeverity
            )
            "<![LOG[$lMessage]LOG]!>" +`
            "<time=`"$lLogTimePlusBias`" " +`
            "date=`"$LogDate`" " +`
            "component=`"$lComponent`" " +`
            "context=`"`" " +`
            "type=`"$lSeverity`" " +`
            "thread=`"$Thread`" " +`
            "file=`"`">"
        }
        
        # Create script block for writing log entry to the console
        $WriteLogLineToHost = {
            Param
            (
                $lTextLogLine,
                $lSeverity
            )
            If ($WriteHost)
            {
                # Only output using color options if running in a host which supports colors.
                If ($Host.UI.RawUI.ForegroundColor -ne $null)
                {
                    Switch ($lSeverity)
                    {
                        3 { Write-Host $lTextLogLine -ForegroundColor 'Red'     }
                        2 { Write-Host $lTextLogLine -ForegroundColor 'Yellow'  }
                        1 { Write-Host $lTextLogLine                            }
                    }
                }
                # If executing "powershell.exe -File <filename>.ps1 > log.txt", then all the Write-Host
                #  calls are converted to Write-Output calls so that they are included in the text log.
                Else
                {
                    Write-Output $lTextLogLine
                }
            }
        }
    }
    Process
    {
        ForEach ($Msg in $Message)
        {
            # If the Section of the script we are in is defined, prepend it to the $Message
            If ($ScriptSectionDefined)
            {
                If ($Msg -ne '')
                {
                    $Msg = $ScriptSection + ' :: ' + $Msg
                }
            }
                
            # Create a normal "Text" log entry
            $TextLogLine = "[$LogDate $LogTimePlusBias] [$Component] " + $Msg
            
            # Execute script block to create the CMTrace.exe compatible log entry
            $CMTraceLogLine = &$LogString -lMessage $Msg -lLogTimePlusBias $LogTimePlusBias -lComponent $Component -lSeverity $Severity
            
            # Choose which log type to write to file
            If ($LogType.ToLower() -eq 'cmtrace')
            {
                $LogLine = $CMTraceLogLine
            }
            Else
            {
                $LogLine = $TextLogLine
            }
            
            # Write the log entry to the log file. Out-File is the preferred method of writing
            # the log file over Add-Content because it will not read/write lock the file.
            Try
            {
                $LogLine | Out-File -FilePath $OutLogFile -Append -NoClobber -Force -Encoding default -ErrorAction 'Stop'
            }
            Catch
            {
                # If failure, then write message to console
                If (-not $SuppressHostOnError)
                {
                    Write-Host "[$LogDate $LogTimePlusBias] [${CmdletName}] Write Log :: Failed to write to the log file `n$(Write-ErrorStack)" -ForegroundColor 'Red'
                    $WriteHost = $true
                }
            }
            
            # Execute script block to write the log entry to the console if $WriteHost is $true
            &$WriteLogLineToHost -lTextLogLine $TextLogLine -lSeverity $Severity
        }
    }
    End
    {
        # Archive log file if size is greater than $MaxLogFileSizeMB and $MaxLogFileSizeMB > 0
        Try
        {
            $LogFile = Get-ChildItem $OutLogFile -ErrorAction 'Stop'
            [decimal]$LogFileSizeMB = $LogFile.Length/1MB
            If (($LogFileSizeMB -gt $MaxLogFileSizeMB) -and ($MaxLogFileSizeMB -gt 0))
            {
                # Change the file extension to "lo_"
                $ArchivedOutLogFile = [System.IO.Path]::ChangeExtension($OutLogFile, 'lo_')
                $ArchiveLogParams = @{
                    ScriptSection       = 'Write Log';
                    Component           = ${CmdletName};
                    Severity            = 2;
                    Thread              = $Thread;
                    LogFileDirectory    = $LogFileDirectory;
                    LogFileName         = $OutLogFile;
                    LogType             = $LogType;
                    MaxLogFileSizeMB    = 0;
                    WriteHost           = $WriteHost;
                    SuppressHostOnError = $SuppressHostOnError;
                }
                
                # Log message about archiving the log file
                $ArchiveLogMessage = "Maximum log file size [$MaxLogFileSizeMB MB] reached. Renamed log file to [$ArchivedOutLogFile]."
                Write-Log -Message $ArchiveLogMessage @ArchiveLogParams
                
                # Archive existing log file from <filename>.log to <filename>.lo_
                # Overwrites any existing <filename>.lo_ file
                # This is the same method SCCM uses for log files.
                Move-Item -Path $OutLogFile -Destination $ArchivedOutLogFile -Force -ErrorAction 'Stop'
                
                # Start new log file and Log message about archiving the old log file
                $NewLogMessage     = "Previous log file was renamed to [$ArchivedOutLogFile] because maximum log file size of [$MaxLogFileSizeMB MB] was reached."
                Write-Log -Message $NewLogMessage @ArchiveLogParams
            }
        }
        Catch
        {
            # If renaming of file fails, script will continue writing to log file even if size goes over the max file size
        }
    }
}