YetAnotherCMLogger.psm1



#region FUNCTION: Check if running in ISE
Function Test-IsISE {
    # trycatch accounts for:
    # Set-StrictMode -Version latest
    try {
        return ($null -ne $psISE);
    }
    catch {
        return $false;
    }
}
#endregion

#region FUNCTION: Check if running in Visual Studio Code
Function Test-VSCode{
    if($env:TERM_PROGRAM -eq 'vscode') {
        return $true;
    }
    Else{
        return $false;
    }
}
#endregion

#region FUNCTION: Find script path for either ISE or console
Function Get-ScriptPath {
    <#
        .SYNOPSIS
            Finds the current script path even in ISE or VSC
        .LINK
            Test-VSCode
            Test-IsISE
    #>

    param(
        [switch]$Parent
    )

    Begin{}
    Process{
        Try{
            if ($PSScriptRoot -eq "")
            {
                if (Test-IsISE)
                {
                    $ScriptPath = $psISE.CurrentFile.FullPath
                }
                elseif(Test-VSCode){
                    $context = $psEditor.GetEditorContext()
                    $ScriptPath = $context.CurrentFile.Path
                }Else{
                    $ScriptPath = (Get-location).Path
                }
            }
            else
            {
                $ScriptPath = $PSCommandPath
            }
        }
        Catch{
            $ScriptPath = '.'
        }
    }
    End{

        If($Parent){
            Split-Path $ScriptPath -Parent
        }Else{
            $ScriptPath
        }
    }

}
#endregion


Function Restore-YaCMLogFileName {
    <#
    .SYNOPSIS
        Gets current log path
    .DESCRIPTION
       Gets current log path from global variable: $Global:LogFilePath
    #>

    # Use function to get paths because Powershell ISE & other editors have differnt results
    $scriptPath = Get-ScriptPath
    [string]$scriptName = [IO.Path]::GetFileNameWithoutExtension($scriptPath)
    #specify path of log
    $Global:LogFilePath = "$env:Windir\Logs\$($scriptName)_$(Get-Date -Format 'yyyy-MM-dd_Thh-mm-ss-tt').log"
    Return $Global:LogFilePath
}


Function Set-YaCMLogFileName {
    <#
    .SYNOPSIS
       Sets a log file path and name
 
    .DESCRIPTION
       Sets a log file path and name for Write-YaCMLogEntry to use
       Sets a global variable: $Global:LogFilePath
 
    .PARAMETER ParentPath
        Defaults to the $env:Windir\Logs
 
    .PARAMETER CallerName
        Overwrites the script name as caller. Useful when Intune generates file using a random caller file name.
 
    .PARAMETER Appendix
        Defaults to the (Get-Date -Format 'yyyy-MM-dd_Thh-mm-ss-tt')
 
    .PARAMETER Passthru
        Output new log path. This DOES NOT update Write-YaCMLogEntry OutputLogFile location
 
    .EXAMPLE
        Set-YaCMLogFileName
 
    .EXAMPLE
        Set-YaCMLogFileName -ParentPath c:\Logs
 
    .EXAMPLE
        Set-YaCMLogFileName -ParentPath c:\Windows\Logs -CallerName 'MyPoshScript' -Appendix ''
 
    .EXAMPLE
        Set-YaCMLogFileName -ParentPath c:\Logs -Appendix ''
 
    .EXAMPLE
        Set-YaCMLogFileName -Passthru
    #>

    Param(
        [Parameter(Mandatory=$false)]
        [string]$ParentPath = "$env:Windir\Logs",

        [Parameter(Mandatory=$false)]
        [string]$CallerName,

        [Parameter(Mandatory=$false)]
        [string]$Appendix = (Get-Date -Format 'yyyy-MM-dd_Thh-mm-ss-tt'),

        [switch]$Passthru
    )

    #Overwrite name with specified caller name
    If($CallerName){
        [string]$scriptName = $CallerName
    }
    Else{
        # Attempt to get path from PScommandpath (only works when called within script)
        $scriptPath = Try{Split-Path $MyInvocation.PSCommandPath -Leaf}Catch{Get-ScriptPath}
        [string]$scriptName = [IO.Path]::GetFileNameWithoutExtension($scriptPath)
    }

    #tes path of parent location
    If(Test-Path $ParentPath)
    {
        #build path
        If($Appendix){$AddAppendix= "_$($Appendix).log"}Else{$AddAppendix= ".log"}
        $NewLogFilePath = Join-Path $ParentPath -ChildPath ($scriptName + $AddAppendix)

        If($Passthru){
            Return $NewLogFilePath
        }
        Else{
            $Global:LogFilePath = $NewLogFilePath
        }
    }
    Else{
        Write-Error ("Path does not exist [{0}]. Create path or specify a new one!" -f $ParentPath)
    }
}

Function Get-YaCMLogFileName{
    <#
    .SYNOPSIS
        Gets current log path
    .DESCRIPTION
       Gets current log path from global variable: $Global:LogFilePath
    #>

    Return $Global:LogFilePath
}

Function Write-YaCMLogEntry{
    <#
    .SYNOPSIS
        Creates a log file
 
    .DESCRIPTION
       Creates a log file format for cmtrace log reader
 
    .NOTES
        Allows to view log using cmtrace while being written to
 
    .PARAMETER Message
        Write message to log file
 
    .PARAMETER Source
        Defaults to the script running or another function that calls this function.
        Used to specify a different source if specified
 
    .PARAMETER Severity
        Ranges 1-5. CMtrace will highlight severity 2 as yellow and 3 as red.
        If Passthru parameter used will change host output:
        0 = Green
        1 = Gray
        2 = Yellow
        3 = Red
        4 = Verbose Output
        5 = Debug output
 
    .PARAMETER OutputLogFile
        Defaults to $Global:LogFilePath. Specify location of log file.
 
    .PARAMETER Passthru
        Output message to host as well. Great when replacing Write-Host with Write-YaCMLogEntry
 
    .EXAMPLE
        #build global log fullpath
        $Global:LogFilePath = "$env:Windir\Logs\$($scriptName)_$(Get-Date -Format 'yyyy-MM-dd_Thh-mm-ss-tt').log"
        Write-YaCMLogEntry -Message 'this is a new message' -Severity 1 -Passthru
 
    .EXAMPLE
        Function Test-output{
            ${CmdletName} = $MyInvocation.InvocationName
            Write-YaCMLogEntry -Message ('this is a new message from [{0}]' -f $MyInvocation.InvocationName) -Source ${CmdletName} -Severity 0 -Passthru
        }
        Test-output
 
        OUTPUT is in green:
        [21:07:50.476-300] [Test-output] :: this is a new message from [Test-output]
 
    .EXAMPLE
        Create entry in log with warning message and output to host in yellow
        Write-YaCMLogEntry -Message 'this is a log entry from an error' -Severity 2 -Passthru
 
    .EXAMPLE
        Create entry in log with error and output to host in red
        Write-YaCMLogEntry -Message 'this is a log entry from an error' -Severity 3 -Passthru
 
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,Position=1,ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Mandatory=$false,Position=2)]
        [string]$Source,

        [parameter(Mandatory=$false,Position=3)]
        [ValidateSet(0,1,2,3,4,5)]
        [int16]$Severity,

        [parameter(Mandatory=$false, HelpMessage="Name of the log file that the entry will written to.")]
        [ValidateNotNullOrEmpty()]
        [string]$OutputLogFile = $Global:LogFilePath,

        [parameter(Mandatory=$false)]
        [switch]$Passthru
    )
    Begin{
        #get BIAS time
        [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
        [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
        [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes
        [string]$LogTimePlusBias = $LogTime + $script:LogTimeZoneBias

        # Get the file name of the source script
        If($Source){
            [string]$ScriptSource = $Source
        }
        Else{
            Try {
                If($MyInvocation.InvocationName){
                    [string]$ScriptSource = $MyInvocation.InvocationName
                }
                ElseIf ($script:MyInvocation.Value.ScriptName) {
                    [string]$ScriptSource = Split-Path -Path $script:MyInvocation.Value.ScriptName -Leaf -ErrorAction 'Stop'
                }
                Else {
                    [string]$ScriptSource = Split-Path -Path $script:MyInvocation.MyCommand.Definition -Leaf -ErrorAction 'Stop'
                }
            }
            Catch {
                [string]$ScriptSource = ''
            }
        }

        #if -Verbose or -Debug is used, set appropriate log severity and prefix log, then reset preference.
        If( ($Severity -eq 4) -or $VerbosePreference){$Message='VERBOSE: ' + $Message;$VerbosePreference = 'Continue'}
        If( ($Severity -eq 5) -or $DebugPreference){$Message='DEBUG: ' + $Message;$DebugPreference = 'Continue'}
    }
    Process{
        #generate CMTrace log format
        $Line = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="{4}" type="{5}" thread="{6}" file="{7}">'
        $LineFormat = $Message, $LogTimePlusBias, $LogDate, $ScriptSource, $([Security.Principal.WindowsIdentity]::GetCurrent().Name),$Severity,$PID,$ScriptSource
        $LogFormat = $Line -f $LineFormat

        #when using pipeline, verbose mode output is not a string; the output needs to be encoded into utf8 then decoded into a string
        #eg. Get-Childitem $_ -Verbose 4>&1 | Write-YaCMLogEntry -Passthru
        $enc = [System.Text.Encoding]::UTF8
        $LogFormatEncoded = $enc.GetBytes($LogFormat)
        $LogFormatDecoded = [System.Text.Encoding]::UTF8.GetString($LogFormatEncoded)

        try {
            $LogFormatDecoded | Out-File -Append -NoClobber -Encoding UTF8 -FilePath $OutputLogFile -ErrorAction Stop
        }
        catch {
            Write-Error ("[{0}] [{1}] :: Unable to append log entry to [{2}], error: {3}" -f $LogTimePlusBias,$ScriptSource,$OutputLogFile,$_.Exception.ErrorMessage)
        }

        #output the message to host
        If($Passthru)
        {
            If($Source){
                $OutputMsg = ("[{0}] [{1}] :: {2}" -f $LogTimePlusBias,$Source,$Message)
            }
            Else{
                $OutputMsg = ("[{0}] [{1}] :: {2}" -f $LogTimePlusBias,$ScriptSource,$Message)
            }

            Switch($Severity){
                0       {Write-Host $OutputMsg -ForegroundColor Green}
                1       {Write-Host $OutputMsg -ForegroundColor White}
                2       {Write-Host $OutputMsg -ForegroundColor Yellow}
                3       {Write-Host $OutputMsg -ForegroundColor Red}
                4       {Write-Host $OutputMsg -ForegroundColor Yellow}
                5       {Write-Host $OutputMsg -ForegroundColor Yellow}
                default {Write-Host $OutputMsg}
            }
        }
    }
}

Restore-YaCMLogFileName

$exportModuleMemberParams = @{
    Function = @(
        'Get-YaCMLogFileName',
        'Set-YaCMLogFileName',
        'Restore-YaCMLogFileName',
        'Write-YaCMLogEntry'
    )
}

Export-ModuleMember @exportModuleMemberParams