Public/Logging/New-LogSpectre.ps1

using namespace Spectre.Console
using namespace System.Collections.Generic
using namespace System.Text.RegularExpressions
using namespace System.Management.Automation

class SpectreColorsFM : ArgumentCompleterAttribute {
    SpectreColorsFM() : base({
        param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
        $options = [Spectre.Console.Color] | Get-Member -Static -Type Properties | Select-Object -ExpandProperty Name
        return $options | Where-Object { $_ -like "$wordToComplete*" }
    }){}
}

function New-LogSpectre {
    <#
    .SYNOPSIS
    Logs messages with various levels and formats using Spectre.Console.
 
    .DESCRIPTION
    The `New-LogSpectre` function logs messages to the console or a file with specified colors and formats. It supports different log levels such as ERROR, WARNING, INFO, SUCCESS, and DEBUG. The function can also include caller information and handle error logging with detailed information about the error context.
 
    .PARAMETER Message
    Specifies the message to log. Supports string, hashtable, PSCustomObject, and Software types.
 
    .PARAMETER Level
    Specifies the log level. Valid values are ERROR, WARNING, INFO, SUCCESS, and DEBUG. Default is INFO.
 
    .PARAMETER IncludeCallerInfo
    Includes caller function information in the log if specified.
 
    .PARAMETER NoConsole
    Prevents logging to the console if specified.
 
    .PARAMETER PassThru
    Returns the log message or object instead of writing it to the console.
 
    .PARAMETER AsObject
    Returns the log details as a PSCustomObject when used with PassThru.
 
    .PARAMETER OverwriteLogFile
    Overwrites the existing log file if specified.
 
    .PARAMETER LogFilePath
    Specifies the path to the log file where the message should be written.
 
    .PARAMETER TimestampColor
    Specifies the color for the timestamp in the log message.
 
    .PARAMETER DefaultTextColor
    Specifies the default text color for the log message.
 
    .PARAMETER DebugColor
    Specifies the color for DEBUG level messages.
 
    .PARAMETER ErrorColor
    Specifies the color for ERROR level messages.
 
    .PARAMETER InfoColor
    Specifies the color for INFO level messages.
 
    .PARAMETER SuccessColor
    Specifies the color for SUCCESS level messages.
 
    .PARAMETER WarningColor
    Specifies the color for WARNING level messages.
 
    .PARAMETER InternalErrorColor
    Specifies the color for internal error messages.
 
    .OUTPUTS
    PSCustomObject if AsObject is specified; otherwise, writes formatted log messages to the console or file.
 
    .EXAMPLE
    # **Example 1**
    # This example demonstrates how to log an informational message to the console.
    New-LogSpectre -Message "The process completed successfully." -Level "INFO"
 
    .EXAMPLE
    # **Example 2**
    # This example demonstrates how to log a warning message to a file.
    New-LogSpectre -Message "Disk space is running low." -Level "WARNING" -LogFilePath "C:\Logs\system.log"
 
    .EXAMPLE
    # **Example 3**
    # This example demonstrates how to log an error message with caller information.
    try {
        Get-ChildItem -Path "C:\nonexistentpath" -ErrorAction Stop
    } catch {
        New-LogSpectre -Message $_ -Level "ERROR" -IncludeCallerInfo
    }
 
    .EXAMPLE
    # **Example 4**
    # This example demonstrates how to return a log message as an object.
    $logObject = New-LogSpectre -Message "Debugging mode enabled." -Level "DEBUG" -PassThru -AsObject
    Write-Output $logObject
 
    .NOTES
    Author: Futuremotion
    Website: https://github.com/futuremotiondev
    Date: 11-14-2024
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,Position=0)] $Message,
        [ValidateSet("ERROR", "WARNING", "INFO", "SUCCESS", "DEBUG", IgnoreCase=$true)]
        [string] $Level = "INFO",
        [switch] $IncludeCallerInfo = $false,
        [switch] $NoConsole,
        [switch] $PassThru,
        [switch] $AsObject,
        [switch] $OverwriteLogFile,
        [string] $LogFilePath,
        [SpectreColorsFM()]
        [String] $TimestampColor="#dde1e6",
        [SpectreColorsFM()]
        [String] $DefaultTextColor="#a0a4ab",
        [SpectreColorsFM()]
        [String] $DebugColor="#dfe4eb",
        [SpectreColorsFM()]
        [String] $ErrorColor="#f57a88",
        [SpectreColorsFM()]
        [String] $InfoColor="#c8d1df",
        [SpectreColorsFM()]
        [String] $SuccessColor="#8cddb9",
        [SpectreColorsFM()]
        [String] $WarningColor="#eab077",
        [SpectreColorsFM()]
        [String] $InternalErrorColor="#f0a2a2"
    )

    begin {
        $levelColors = @{
            "ERROR"   = $ErrorColor
            "WARNING" = $WarningColor
            "SUCCESS" = $SuccessColor
            "DEBUG"   = $DebugColor
            "INFO"    = $InfoColor
        }
        try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 }
        catch { Write-SpectreHost "[#DCDFE5]Notice:[/] [#ABB1BC]Unable to set console encoding to [#FFFFFF]UTF8[/][/]" }
    }

    process {

        if ($null -eq $Message -and $Level -ne "ERROR") { return }

        $sEscapeL = Get-SpectreEscapedText -Text "["
        $sEscapeR = Get-SpectreEscapedText -Text "]"

        try {
            @('exceptionMessage', 'failedCode', 'scriptLines', 'lineInfo') |
                ForEach-Object { Set-Variable -Name $_ -Value $null }

            if ($Message -is [hashtable]) { $Message = [pscustomobject]$Message }

            # Check for unsupported message types
            $validTypes = [HashSet[string]]::new()
            $validTypes.Add("PSCustomObject") | Out-Null
            $validTypes.Add("Hashtable") | Out-Null
            $validTypes.Add("String") | Out-Null
            $validTypes.Add("Software") | Out-Null

            if ($Message -and -not $validTypes.Contains($Message.GetType().Name)) {
                $UnsupportedMsg = "Must be PSCustomObject, Hashtable, String, or Software"
                New-Log "Unsupported message type: $($Message.GetType().Name). $UnsupportedMsg" -ForegroundColor Red
                return
            }

            # Initialize variables
            $logSentToConsole = $false
            $logMessage = ''
            $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
            $callerInfo = (Get-PSCallStack)[1]
            $originalMessage = $Message
            $levelColor = $levelColors[$Level]

            $headerPrefix = "$sEscapeL[$TimestampColor]$timestamp[/]$sEscapeR $sEscapeL[$levelColor]$Level[/]$sEscapeR"
            $headerPrefixFile = "[$timestamp] [$Level]"

            # Format message if not a string
            if ($Message -isnot [string]) { $Message = ($Message | Format-List | Out-String).Trim() }

            # Include caller info if necessary
            $includeFunctionInfo = $callerInfo.FunctionName -ne '<ScriptBlock>' -and ($IncludeCallerInfo.IsPresent -or $Level -eq "ERROR")
            $functionInfo = if ($includeFunctionInfo) { "$sEscapeL[$TimestampColor]Function:[/] $($callerInfo.FunctionName)$sEscapeR" } else { "" }
            $functionInfoFile = if ($includeFunctionInfo) { "[Function: $($callerInfo.FunctionName)]" } else { "" }

            $messageLines = if ($Message) {
                $Message -split "`n" | ForEach-Object { "$headerPrefix [$DefaultTextColor]$_[/]" }
            } else {
                "$headerPrefix${_}"
            }

            $messageLinesFile = if ($Message) {
                $Message -split "`n" | ForEach-Object { "$headerPrefixFile $_" }
            } else {
                "$headerPrefixFile${_}"
            }

            $logMessage += ($messageLines -join "`n") + $functionInfo
            $logMessageFile += ($messageLinesFile -join "`n") + $functionInfoFile

            # Handle error logging
            if ($Level -eq "ERROR" -and $Error[0]) {
                $errorRecord = $Error[0]
                $invocationInfo = $errorRecord.InvocationInfo

                try {
                    $scriptPath = $errorRecord.InvocationInfo.PSCommandPath ?? $errorRecord.InvocationInfo.ScriptName
                    if ($scriptPath -and (Test-Path -Path $scriptPath)) {
                        $scriptLines = Get-Content -Path $scriptPath -ErrorAction Stop
                    }
                } catch {
                    Write-SpectreHost "$sEscapeL[$TimestampColor]$timestamp[/]$sEscapeR$sEscapeL[$InternalErrorColor]INTERNAL_ERROR[/]$sEscapeR An error occurred in New-Log function."
                    Write-SpectreHost "[$InternalErrorColor]$($_.Exception.Message)[/]"
                }

                $functionName = $callerInfo.Command
                $failedCode = $invocationInfo.Line?.Trim()
                [int]$errorLine = $errorRecord.InvocationInfo.ScriptLineNumber ?? $invocationInfo.ScriptLineNumber

                if ($scriptLines) {
                    [int]$functionStartLine = ($scriptLines | Select-String -Pattern "function\s+$functionName" | Select-Object -First 1).LineNumber
                    $lineNumberInFunction = $errorLine - $functionStartLine
                    $lineInfo = "($lineNumberInFunction,$errorLine) (Function,Script)"
                    if ($callerInfo.FunctionName -eq '<ScriptBlock>') {
                        $lineInfo = "$errorLine (Script)"
                    }
                } else {
                    $lineNumberInFunction = $errorLine - ([int]$callerInfo.ScriptLineNumber - [int]$invocationInfo.OffsetInLine) - 1
                    $lineInfo = "($lineNumberInFunction,$errorLine) (Function,Script)"
                    if ($callerInfo.FunctionName -eq '<ScriptBlock>') {
                        $lineInfo = "$errorLine (Script)"
                    }
                }

                $exceptionMessage = $errorRecord.Exception.Message
                $logMessage += "$sEscapeL[$InternalErrorColor]CodeRow:[/] $lineInfo$sEscapeR"
                $logMessage += "$sEscapeL[$InternalErrorColor]FailedCode:[/] $failedCode$sEscapeR"
                $logMessage += "$sEscapeL[$InternalErrorColor]ExceptionMessage:[/] [$ErrorColor]$exceptionMessage[/]$sEscapeR"
                $logMessageFile += "CodeRow: $lineInfo"
                $logMessageFile += "FailedCode: $failedCode"
                $logMessageFile += "ExceptionMessage: $exceptionMessage"
            }

            function Write-MessageToConsole {
                if ($LogSentToConsole -eq $true) { return }
                if (-not($NoConsole.IsPresent)) { Write-SpectreHost $logMessage }
                return $true
            }

            # Log to console if conditions are met
            if (!($NoConsole.IsPresent) -and !($PassThru.IsPresent) -and !($AsObject.IsPresent) -and !$LogFilePath) {
                $LogSentToConsole = Write-MessageToConsole
            }

            # Handle log file writing
            if ($LogFilePath) {
                $LogSentToConsole = Write-MessageToConsole
                $parentDir = Split-Path -Path $LogFilePath -Parent
                if (-not (Test-Path -Path $parentDir)) {
                    New-Item -Path $parentDir -ItemType Directory -Force
                }
                if ($OverwriteLogFile.IsPresent) {
                    Remove-Item -Path $LogFilePath -Force -ErrorAction SilentlyContinue
                    Set-Content -Value $logMessageFile -Path $LogFilePath -Force -Encoding utf8
                } else {
                    Add-Content -Value $logMessageFile -Path $LogFilePath -Encoding utf8
                }
            }

            $object = [PSCustomObject]@{
                Timestamp      = $timestamp
                Level          = $Level
                Message        = if ($originalMessage -is [string]) { $Message } else { $Message | Out-String }
                Exception      = if (-not [string]::IsNullOrEmpty($exceptionMessage)) { $exceptionMessage } else { $null }
                CallerFunction = if ($callerInfo.FunctionName -eq '<ScriptBlock>') { $null } else { $callerInfo.FunctionName }
                CodeRow        = if (-not [string]::IsNullOrEmpty($lineInfo)) { $lineInfo } else { $null }
                FailedCode     = if (-not [string]::IsNullOrEmpty($FailedCode)) { $FailedCode } else { $null }
            }

            if ($PassThru.IsPresent) {
                $LogSentToConsole = Write-MessageToConsole
                return if ($AsObject.IsPresent) { $object } else { $logMessage }
            } elseif (!$NoConsole.IsPresent -and $AsObject.IsPresent) {
                $object | Out-Host
            }
        }
        catch {
            Write-SpectreHost "$sEscapeL[$TimestampColor]$timestamp[/]$sEscapeR$sEscapeL[$ErrorColor]ERROR[/]$sEscapeR [$DefaultTextColor]An error occurred in New-Log function.[/] [$ErrorColor]$($_.Exception.Message)[/]"
        }
    }
}