LibreDevOpsHelpers.Logger/LibreDevOpsHelpers.Logger.psm1

Set-StrictMode -Version Latest

# Module-scoped minimum level. Messages below this are suppressed. DEBUG also
# respects $DebugPreference, so it stays hidden unless the caller opts in.
$script:LdoLogLevels = @{ DEBUG = 0; INFO = 1; SUCCESS = 1; WARN = 2; ERROR = 3 }

# Minimum level and output format. Both can be seeded from the environment so that
# operators can control logging in CI/CD without touching code, and both fall back to
# sensible defaults (show everything; structured JSON) when unset or invalid.
$script:LdoMinLogLevel = if ($env:LDO_LOG_LEVEL -and $script:LdoLogLevels.ContainsKey($env:LDO_LOG_LEVEL.ToUpperInvariant())) {
    $env:LDO_LOG_LEVEL.ToUpperInvariant()
}
else {
    'DEBUG'
}

$script:LdoLogFormat = switch -Regex ($env:LDO_LOG_FORMAT) {
    '^(?i)jsonindented$' { 'JsonIndented'; break }
    '^(?i)text$' { 'Text'; break }
    default { 'Json' }  # covers 'json', unset, and any unrecognised value
}

function Set-LdoLogLevel {
    <#
    .SYNOPSIS
        Sets the minimum level that Write-LdoLog will emit.

    .DESCRIPTION
        Messages below the configured level are dropped. The default is DEBUG, which
        shows everything (DEBUG still also requires $DebugPreference to be set, as it
        is routed through Write-Debug). The initial value can also be supplied via the
        LDO_LOG_LEVEL environment variable.

    .PARAMETER Level
        One of DEBUG, INFO, WARN, ERROR. SUCCESS is treated at the same threshold as
        INFO.

    .EXAMPLE
        Set-LdoLogLevel -Level WARN

        Suppresses DEBUG, INFO and SUCCESS messages, leaving only WARN and ERROR.
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('DEBUG', 'INFO', 'WARN', 'ERROR')]
        [string]$Level
    )

    $script:LdoMinLogLevel = $Level
}

function Get-LdoLogLevel {
    <#
    .SYNOPSIS
        Returns the current minimum level that Write-LdoLog will emit.

    .DESCRIPTION
        Returns the threshold set by Set-LdoLogLevel (or seeded from the LDO_LOG_LEVEL
        environment variable). Messages below this level are suppressed.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param()

    $script:LdoMinLogLevel
}

function Set-LdoLogFormat {
    <#
    .SYNOPSIS
        Sets the default output format that Write-LdoLog will emit.

    .DESCRIPTION
        Controls how every log message is rendered unless a call overrides it with its
        own -Format. The default is Json, which emits one compact JSON object per line
        (newline-delimited JSON) suitable for ingestion by log aggregators such as
        Splunk, Elasticsearch or Azure Monitor. Text emits a human-readable, coloured
        line for interactive CLI use. The initial value can also be supplied via the
        LDO_LOG_FORMAT environment variable.

    .PARAMETER Format
        Json (compact, one object per line), JsonIndented (pretty-printed, for local
        debugging only - not newline-delimited), or Text.

    .EXAMPLE
        Set-LdoLogFormat -Format Text

        Switches subsequent log output to the human-readable coloured format.
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Json', 'JsonIndented', 'Text')]
        [string]$Format
    )

    $script:LdoLogFormat = $Format
}

function Get-LdoLogFormat {
    <#
    .SYNOPSIS
        Returns the current default output format (Json or Text).
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param()

    $script:LdoLogFormat
}

function Write-LdoLog {
    <#
    .SYNOPSIS
        Writes a levelled, timestamped log message to the correct PowerShell stream.

    .DESCRIPTION
        The single logging entry point for all LibreDevOpsHelpers modules. By default
        each message is rendered as one compact JSON object (newline-delimited JSON)
        carrying a UTC ISO-8601 timestamp, level, invocation and message, plus any extra
        properties supplied via -Data. Pass -Format Text (or call Set-LdoLogFormat) for
        a human-readable coloured line instead.

        Each level is routed to a stream that never touches the success (output)
        pipeline, so the function is safe to call from inside other functions without
        corrupting their return values:

            DEBUG -> Write-Debug (shown when $DebugPreference is Continue)
            INFO -> Write-Information (information stream; coloured Write-Host in Text mode)
            SUCCESS -> Write-Information (information stream; coloured Write-Host in Text mode)
            WARN -> Write-Warning
            ERROR -> Write-Error (non-terminating; the caller decides whether to throw)

        Messages below the level set by Set-LdoLogLevel are suppressed.

    .PARAMETER Level
        Severity of the message. One of DEBUG, INFO, SUCCESS, WARN, ERROR.

    .PARAMETER Message
        The text to log.

    .PARAMETER InvocationName
        Name of the calling command, used as the JSON "invocation" field and the text
        prefix. Defaults to the immediate caller's command name when not supplied.

    .PARAMETER Data
        Optional hashtable of additional structured properties merged into the JSON
        record (for example correlation IDs or resource names). Ignored in Text mode.

    .PARAMETER Format
        Overrides the module default output format for this call only. Json (compact),
        JsonIndented (pretty-printed, for local debugging), or Text.

    .EXAMPLE
        Write-LdoLog -Level INFO -Message 'Starting deployment'

    .EXAMPLE
        Write-LdoLog -Level ERROR -Message "Failed: $($_.Exception.Message)"

    .EXAMPLE
        Write-LdoLog -Level INFO -Message 'Created resource group' -Data @{ resourceGroup = 'rg-prod'; correlationId = $cid }
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('DEBUG', 'INFO', 'SUCCESS', 'WARN', 'ERROR')]
        [string]$Level,

        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Message,

        [string]$InvocationName,

        [hashtable]$Data,

        [ValidateSet('Json', 'JsonIndented', 'Text')]
        [string]$Format
    )

    if (-not $InvocationName) {
        $caller = (Get-PSCallStack)[1]
        $InvocationName = if ($caller -and $caller.Command) { $caller.Command } else { '<script>' }
    }

    if ($script:LdoLogLevels[$Level] -lt $script:LdoLogLevels[$script:LdoMinLogLevel]) {
        return
    }

    if (-not $Format) {
        $Format = $script:LdoLogFormat
    }

    $now = Get-Date

    if ($Format -eq 'Text') {
        $timestamp = $now.ToString('yyyy-MM-dd HH:mm:ss')
        $line = '{0} [{1}] [{2}] {3}' -f $timestamp, $Level, $InvocationName, $Message
    }
    else {
        # ISO-8601 in UTC ("o" round-trip format) so downstream log systems can parse
        # an unambiguous, timezone-correct timestamp.
        $record = [ordered]@{
            timestamp = $now.ToUniversalTime().ToString('o')
            level = $Level
            invocation = $InvocationName
            message = $Message
        }
        if ($Data) {
            foreach ($key in $Data.Keys) {
                $record[[string]$key] = $Data[$key]
            }
        }
        # Compact (one object per line) is the default for log ingestion. JsonIndented is an
        # opt-in for local debugging and is not newline-delimited.
        if ($Format -eq 'JsonIndented') {
            $line = $record | ConvertTo-Json -Depth 10
        }
        else {
            $line = $record | ConvertTo-Json -Depth 10 -Compress
        }
    }

    switch ($Level) {
        'DEBUG' { Write-Debug $line }
        'INFO' { Write-LdoInfoLine -Line $line -Level $Level -Format $Format -Color Cyan }
        'SUCCESS' { Write-LdoInfoLine -Line $line -Level $Level -Format $Format -Color Green }
        'WARN' { Write-Warning $line }
        # Explicitly non-terminating: logging an error must never throw on its own,
        # even when the caller has $ErrorActionPreference = 'Stop'. The caller decides
        # whether to throw.
        'ERROR' { Write-Error $line -ErrorAction Continue }
    }
}

function Write-LdoInfoLine {
    # Emits INFO/SUCCESS lines without ever touching the success (output) stream.
    # JSON goes through Write-Information so it lands on the information stream as a
    # tagged, capturable InformationRecord with no ANSI colour to corrupt parsing.
    # Text uses coloured Write-Host for readable interactive CLI output.
    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)][AllowEmptyString()][string]$Line,
        [Parameter(Mandatory)][string]$Level,
        [Parameter(Mandatory)][string]$Format,
        [Parameter(Mandatory)][System.ConsoleColor]$Color
    )

    if ($Format -eq 'Text') {
        Write-Host $Line -ForegroundColor $Color
    }
    else {
        Write-Information -MessageData $Line -Tags $Level -InformationAction Continue
    }
}

Export-ModuleMember -Function Write-LdoLog, Set-LdoLogLevel, Get-LdoLogLevel, Set-LdoLogFormat, Get-LdoLogFormat