Eigenverft.Manifested.Drydock.Logging.ps1

function Write-ConsoleLog {
<#
.SYNOPSIS
Minimal console logger for PowerShell 5 (space layout), UTC by default.
 
.DESCRIPTION
Emits: "[yyyy-MM-dd HH:mm:ss:fff LEVEL] [file.ps1] [function] message"
- Time is UTC by default. Use -LocalTime to log in local time.
- Auto-resolves caller script file and function from the call stack.
- Plain text, deterministic output (no colors).
 
.PARAMETER Message
The message to log. Mandatory and non-empty.
 
.PARAMETER Level
Severity tag. One of: TRC, DBG, INF, WRN, ERR, FTL. Defaults to INF.
 
.PARAMETER LocalTime
When present, uses local time instead of UTC.
 
.PARAMETER ContinueOnError
When present, do NOT exit on ERR/FTL. By default the function exits on ERR (code 1) and FTL (code 2).
 
.PARAMETER QuietExceptCritical
Suppress all output except ERR and FTL.
 
.PARAMETER QuietExceptWarning
Suppress all output except WRN, ERR, and FTL.
 
.EXAMPLE
function MakeDirs {
    Write-ConsoleLog -Level INF -Message 'Dirscreated.'
}
MakeDirs
# -> [2025-10-12 04:47:17:265 INF] [out.ps1] [makedirs] Dirscreated.
 
.EXAMPLE
# Use in try/catch; include the error message via $_ and keep running.
try {
    Throw "Something went wrong."
}
catch {
    # Default would exit on ERR; add -ContinueOnError to log and continue.
    # Write-ConsoleLog -Level ERR -ContinueOnError -Message ("Caught error: {0}" -f $_.Exception.Message)
    # Alternatively include the whole error record:
    # Write-ConsoleLog -Level ERR -ContinueOnError -Message ("Caught error: {0}" -f $_)
}
 
.EXAMPLE
Write-ConsoleLog -Message 'customtext.' -Level WRN -LocalTime
# -> [2025-10-12 06:47:17:265 WRN] [fileofthefunction] [functionnameinfile] customtext.
 
.NOTES
- Default: exit on ERR (1) and FTL (2). Use -ContinueOnError to keep going.
- If both quiet switches are supplied, QuietExceptCritical takes precedence.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter()]
        [ValidateSet('TRC','DBG','INF','WRN','ERR','FTL')]
        [string]$Level = 'INF',

        [Parameter()]
        [switch]$LocalTime,

        [Parameter()]
        [switch]$ContinueOnError,

        [Parameter()]
        [switch]$QuietExceptCritical,

        [Parameter()]
        [switch]$QuietExceptWarning
    )

    # Timestamp (UTC by default).
    $now = if ($LocalTime.IsPresent) { Get-Date } else { [DateTime]::UtcNow }
    $ts  = $now.ToString('yyyy-MM-dd HH:mm:ss:fff')

    # Resolve file/function from call stack.
    $self   = $MyInvocation.MyCommand.Name
    $caller = Get-PSCallStack | Where-Object { $_.FunctionName -ne $self } | Select-Object -First 1
    if (-not $caller) {
        $caller = [pscustomobject]@{
            ScriptName   = $PSCommandPath
            FunctionName = '<scriptblock>'
            Location     = $null
            Command      = $null
        }
    }

    $file =
        if ($caller.ScriptName) { Split-Path -Leaf $caller.ScriptName }
        elseif ($caller.Location -and $caller.Location -match '^(.*?):\d+') { Split-Path -Leaf $Matches[1] }
        else { 'console' }

    $func =
        if ($caller.FunctionName) { $caller.FunctionName }
        elseif ($caller.Command)  { $caller.Command }
        else { '<scriptblock>' }

    $lvl = $Level.ToUpperInvariant()

    # Severity ranking.
    $sevMap = @{ TRC=0; DBG=1; INF=2; WRN=3; ERR=4; FTL=5 }
    $sev    = $sevMap[$lvl]

    # Quieting rules (QuietExceptCritical is stricter; give it precedence).
    $shouldWrite = $true
    if ($QuietExceptCritical.IsPresent) {
        if ($sev -lt 4) { $shouldWrite = $false }
    }
    elseif ($QuietExceptWarning.IsPresent) {
        if ($sev -lt 3) { $shouldWrite = $false }
    }

    # Format the line.
    $line = "[{0} {1}] [{2}] [{3}] {4}" -f $ts, $lvl, $file, $func.ToLower(), $Message

    if ($shouldWrite) {
        Write-Host $line
    }

    # Default: stop on ERR/FTL unless -ContinueOnError is specified.
    if (-not $ContinueOnError.IsPresent -and $sev -ge 4) {
        exit (if ($sev -ge 5) { 2 } else { 1 })
    }
}