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 the [Write-Log] is called under [Start-Job] / [Start-ThreadJob], the write to log function will be disabled and the output will changed to detail (DebugMode) format.
     
    Example:
    [ScriptBlock]$Test-ScriptBlock = {
        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 {
        # MARK: Define log file location and function name
        $Stack = @(Get-PSCallStack)
        If ($_setting.DebugMode) { $Global:CallStack = $Stack }

        $_configure = @{ 'FunctionName' = $Stack[1].Command }

        # The write log function will be disabled if it call from [Start-Job] / [Start-ThreadJob]
        If (($Host.Name -eq 'Default Host') -or ($Host.Name -eq 'ServerRemoteHost')) { 
            $_threadJob = $true
            $LogPath = $null
            If ($Stack.Count -eq 1) {
                $_configure.FunctionName = '<Thread Job>'
            } Else {
                $_configure.FunctionName += ' <Job>'
            }
        }
        
        If ($Stack[-1].ScriptName) {
            # Use command call the PS1 file
            $_configure['PsScrtipRoot'] = Split-Path $Stack[-1].ScriptName
            $_configure['LogFileName']  = (Get-Item $Stack[-1].ScriptName).BaseName
        } ElseIf ($Stack.Count -eq 2) {
            # Use [Write-Log] cmdlet from <Console>
            If (!$_threadJob) { $_configure.FunctionName = '<Console>' }
            $_configure['PsScrtipRoot'] = $PWD.ToString()
            $_configure['LogFileName']  = 'Console'
        } ElseIf ($Stack[-2].ScriptName) {
            # Use Powershell console call the PS1 file
            $_configure['PsScrtipRoot'] = Split-Path $Stack[-2].ScriptName
            $_configure['LogFileName']  = (Get-Item $Stack[-2].ScriptName).BaseName
        } Else {
            # Call from PowerShell console
            $_configure['PsScrtipRoot'] = $PWD.ToString()
            $_configure['LogFileName']  = 'Console'
        }
        If ($_configure.FunctionName -eq '<ScriptBlock> <Job>') {
            $_configure.FunctionName = '<Thread Job>'
        }

        # MARK: Define log format
        $_date = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
        If ($_configure.FunctionName.Length -gt $_setting["$($_configure.PsScrtipRoot):$($_configure.LogFileName)"]) {
            $_setting["$($_configure.PsScrtipRoot):$($_configure.LogFileName)"] = $_configure.FunctionName.Length
        }
        If ($_setting["$($_configure.PsScrtipRoot):$($_configure.LogFileName)"] -gt $LengthOfFunctionName) {
            $LengthOfFunctionName = $_setting["$($_configure.PsScrtipRoot):$($_configure.LogFileName)"]
        } Else {
            $_setting["$($_configure.PsScrtipRoot):$($_configure.LogFileName)"] = $LengthOfFunctionName
        }
        $_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 ($LogPath) {
            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))!! >>>>>"
                }
            }
        } Else {
            $_logPath = $null
        }

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

        If ($RawData) {
            ## ----------------------------
            ## MARK: Write log from 'RawData'
            ## ----------------------------
            $_logMessage = $RawData
        } Else {
            ## ----------------------------
            ## MARK: Generate log message
            ## ----------------------------

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

            # Write message to console
            If ($_logMessage) {
                If ($DebugMode -or $_threadJob) {
                    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())
                    }
                }
            }
        }

        # MARK: Write message to log file
        If ($_logFile) {
            $_logMessage | Out-File -Append $_logFile -Encoding utf8
        }

        If ($_setting.DebugMode) {
            $_setting['LastRecord'] = @{
                'LogPath'      = $_logPath
                'LogFile'      = $_logFile
                'LogFormat'    = $_logFormat
                'LogMessage'   = $_logMessage
                'LogRecycling' = $LogRecycling
                'ErrorLevel'   = $ErrorLevel.ToString()
                'InputMessage' = $Message

            }
        }
    }
}
Export-ModuleMember -Function 'Write-Log'

function Get-WriteLogData {
    If ($_setting.ContainsKey('LastRecord')) {
        $_setting.'LastRecord'
    } Else {
        Write-Host -ForegroundColor Yellow 'No data found!'
    }
}
If ($_setting.DebugMode) { Export-ModuleMember -Function Get-WriteLogData }