JT.WriteLog.psm1

Enum errorLevel
{
    ERROR
    WARNING
    SUCCESS
    INFO
    DEBUG
}

Enum logRecycle
{
    Continue
    Daily
    Hourly
}

$_setting = @{
    'DebugMode' = $false
}

Function Write-Log {
    <#
    .SYNOPSIS
    Write-Log is a Powershell logging script.
  
    .PARAMETER ErrorLevel
    Define level of the message.
 
    .PARAMETER Message
    Log message.
  
    .PARAMETER LogRecycling
    Specific the recycling of the log file (Default value is 'continue' whcih it will write into single file.)
 
    .PARAMETER LogPath
    Specific the path of the log file (The default path is the 'Log' folder under the script locaiton).
    If the command is not call from the script, it will create a log file '_log.txt' under existing location.
    If the 'LogPath' is $null, the log message will write to console only.
  
    .PARAMETER DebugMode
    Switch the 'DebugMode' on and off.
    If debug mode is off, it will not output and message to console and log file when the errorlevel of the message is DEBUG.
 
    .PARAMETER RawData
    Direct write data into log file. (If 'RawData' contain data, it will bypass the data input from 'ErrorLevel' and 'Message')
    Remark: The Write-Log will not show any messaage on console and add any information (Timestamp, Errorlevel, Call from) when write to file.
 
    .EXAMPLE
    Write-Log -errorLevel INFO -message 'Hello World!'
  
    Output (console):
    yyyy-MM-dd HH:mm:ss.fff | INFO | <ScriptBlock> | Hello World!
 
    Output file name: _log.txt
  
    .EXAMPLE
    Write-Log -errorLevel INFO -message 'Hello World!' -logRecycling Daily
  
    Output (console):
    yyyy-MM-dd HH:mm:ss.fff | INFO | <ScriptBlock> | Hello World!
 
    Output file name: _yyyy-MM-dd_log.txt
  
    .EXAMPLE
    If wants to collect the log from 'Start-Job' or 'Start-ThreadJob', it suggest create a funciton under script block and disable write log to file temporary.
     
    Disable write log to file:
    $PSDefaultParameterValues.Add('Write-Log:LogPath', $null)
 
    Example:
    [ScriptBlock]$Test-ScriptBlock = {
        $PSDefaultParameterValues.Add('Write-Log:LogPath', $null)
 
        Function Run-ScriptBlock {
            Write-Log -ErrorLevel INFO -Message 'Log message'
        }
 
        Run-ScriptBlock
    }
 
    Start-ThreadJob -ScriptBlock $Test-ScriptBlock
    Get-Job | Wait-Job | Receive-Job 6>&1 | Write-Log
 
    .NOTES
    It suggest use '$PSDefaultParameterValues' to pre-define the 'logRecycling' and 'logPath' setting.
 
    e.g.:
    $PSDefaultParameterValues['Write-Log:logRecycling'] = 'daily'
    $PSDefaultParameterValues['Write-Log:logPath'] = 'C:\Temp\'
    $PSDefaultParameterValues['Write-Log:debugMode'] = $True
 
    #>

    [CmdletBinding()]
    param(
        [errorLevel]$ErrorLevel = 'INFO',
        [string]$Message,
        [logRecycle]$LogRecycling = 'Continue',
        [string]$LogPath = './Log',
        [switch]$DebugMode,
        [parameter(ValueFromPipeline = $true)]
        [string[]]$RawData = $null,
        [ValidatePattern("^\d+$")]
        [Parameter(DontShow)]
        [string]$LengthOfFunctionName = 20
    )

    Process {
        $_configure = @{
            'FuncitonName' = '';
            'PsScrtipRoot' = '';
            'LogFileName'  = ''
        }
        $Stack = @(Get-PSCallStack)
        If ($_setting.DebugMode) { $Global:CallStack = $Stack }

        If ($Stack.Count -eq 2) {
            # Call from console
            $_configure.FuncitonName = '<Console>'
            $_configure.PsScrtipRoot = $PWD.ToString()
        } Else {
            $_configure.FuncitonName = $Stack[1].Command
            If ($Stack[-2].FunctionName -eq '<ScriptBlock>') {
                $_configure.PsScrtipRoot = Split-Path $Stack[-2].ScriptName 
                $_configure.LogFileName = (Get-Item $Stack[-2].ScriptName).BaseName
            } Else {
                $_configure.PsScrtipRoot = $PWD.ToString()
            }
        }

        # MARK: Pre-define setting
        $_date = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
        $_logFormat = '{0} | {1, -7} | {2, -xxxx} | {3}'.Replace('xxxx', $LengthOfFunctionName.Trim()) # {yyyy-MM-dd HH:mm:ss.fff} | {ErrorLevel} | {FunctionName} | {Message}
        $_errorTab = @{
            'ERROR'   = 'Red';
            'WARNING' = 'Yellow';
            'SUCCESS' = 'Green';
            'INFO'    = 'White';
            'DEBUG'   = 'DarkYellow'
        }

        # MARK: Log file cycling
        Switch ($LogRecycling) {
            'Daily'  { $_logRecyclingValue = Get-Date -Format '_yyyy-MM-dd';    break }
            'Hourly' { $_logRecyclingValue = Get-Date -Format '_yyyy-MM-dd-HH'; break }
            Default  { $_logRecyclingValue = '' }
        }

        # MARK: Log file location
        If ([string]::IsNullOrEmpty($LogPath)) {
            $_logPath = $null
        } Else {
            If (-not $LogPath.Contains(':')) {
                $LogPath = "$($_configure.PsScrtipRoot)\$($LogPath)"
            }

            # Check the log folder and try to create it if not exist.
            If (Test-Path $LogPath -PathType Container) {
                $_logPath = $LogPath
            } Else {
                Try {
                    $_logPath = (New-Item $LogPath -ItemType Directory -Force -ErrorAction SilentlyContinue).ToString()
                }
                Catch {
                    Write-Host -ForegroundColor Red "<<<<< Error for create log folder ($($LogPath))!! >>>>>"
                }
            }
        }

        # MARK: Define log file name
        If ([string]::IsNullOrEmpty($_logPath)) {
            $_logFile = $null
        } Else {
            $_logFile = [string]::Concat(
                "$($_logPath)\",
                $_configure.LogFileName, 
                $_logRecyclingValue, 
                '_log.txt')
        }

        If ([string]::IsNullOrEmpty($RawData)) {
            ## ----------------------------
            ## MARK: Generate log message
            ## ----------------------------
            
            # Define call from
            If ($Stack[1].Command -ne '<ScriptBlock>') {
                $_functionName = $Stack[1].Command
            } Else {
                $_functionName = '<Console>'
            }

            # Prepare log message
            If (!$debugMode -and ($ErrorLevel -eq 'DEBUG')) {
                $_logMessage = $null
            } Else {
                $_logMessage = [string]::Format(
                    $_logFormat, 
                    $_date, 
                    $ErrorLevel.ToString().ToUpper(), 
                    $_functionName,
                    $Message
                )
            }

            # Write message to console
            If (![string]::IsNullOrEmpty($_logMessage)) {
                If ($DebugMode) {
                    If ($ErrorLevel -eq 'INFO') {
                        Write-Host $_logMessage
                    } Else {
                        Write-Host $_logMessage -ForegroundColor $_errorTab.($ErrorLevel.ToString())
                    }
                } Else {
                    If ($ErrorLevel -eq 'INFO') {
                        Write-Host $Message
                    } Else {
                        Write-Host $Message -ForegroundColor $_errorTab.($ErrorLevel.ToString())
                    }
                }
            }
        } Else {
            ## ----------------------------
            ## MARK: Write log from 'RawData'
            ## ----------------------------
            $_logMessage = $RawData
        }

        # MARK: Write message to log file
        If (![string]::IsNullOrEmpty($_logFile)) {
            $_logMessage | Out-File -Append $_logFile
        }
    }
}
Export-ModuleMember -Function 'Write-Log'