src/public/Logging/Get-AitherErrorLog.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
    Retrieve error logs for debugging and troubleshooting

.DESCRIPTION
    Retrieves error logs from the AitherZero logging system. Filters errors by various
    criteria and provides detailed error information including stack traces, parameters,
    and context for debugging.

    This cmdlet is essential for troubleshooting issues and understanding what went wrong
    during script execution or module operations.

.PARAMETER Since
    Only return errors that occurred after this date/time. Useful for filtering recent errors.

    Examples:
    - (Get-Date).AddHours(-1) - Last hour
    - (Get-Date).AddDays(-1) - Last 24 hours
    - "2025-01-15" - Since a specific date

.PARAMETER Until
    Only return errors that occurred before this date/time.

.PARAMETER Cmdlet
    Filter errors by cmdlet name. Returns only errors from the specified cmdlet(s).

    Examples:
    - "Invoke-AitherScript"
    - "Get-AitherConfigs", "Set-AitherConfig"

.PARAMETER ErrorId
    Filter by specific error ID. Use this to find a specific error that was reported.

.PARAMETER ComputerName
    Filter errors by computer name. Useful in multi-machine environments.

.PARAMETER Level
    Filter by error level. Default is 'Error', but you can also get 'Warning' or 'Critical' level entries.

.PARAMETER Count
    Maximum number of errors to return. Default is 100. Use -1 for all errors.

.PARAMETER Format
    Output format: Object (default), Table, List, or JSON.

.INPUTS
    System.String
    You can pipe error IDs or cmdlet names to Get-AitherErrorLog.

.OUTPUTS
    PSCustomObject
    Returns error log entries with properties:
    - ErrorId: Unique error identifier
    - Timestamp: When the error occurred
    - Cmdlet: Cmdlet that generated the error
    - Level: Error level (Error, Warning, Critical)
    - Message: Error message
    - Exception: Exception details
    - StackTrace: Stack trace for debugging
    - Parameters: Parameters that were passed
    - ComputerName: Computer where error occurred

.EXAMPLE
    Get-AitherErrorLog

    Gets the most recent 100 errors.

.EXAMPLE
    Get-AitherErrorLog -Since (Get-Date).AddHours(-1)

    Gets errors from the last hour.

.EXAMPLE
    Get-AitherErrorLog -Cmdlet "Invoke-AitherScript" -Count 50

    Gets the 50 most recent errors from Invoke-AitherScript.

.EXAMPLE
    Get-AitherErrorLog -ErrorId "12345678-1234-1234-1234-123456789abc"

    Gets a specific error by its ID.

.EXAMPLE
    Get-AitherErrorLog -Since (Get-Date).AddDays(-1) -Format JSON | Out-File errors.json

    Exports yesterday's errors to a JSON file.

.EXAMPLE
    "Invoke-AitherScript", "Get-AitherConfigs" | Get-AitherErrorLog

    Gets errors from multiple cmdlets by piping cmdlet names.

.NOTES
    Error logs are stored in:
    - Structured JSON: library/logs/structured/structured-YYYY-MM-DD.jsonl
    - Text logs: library/logs/aitherzero-YYYY-MM-DD.log

    This cmdlet searches both sources to provide comprehensive error information.

.LINK
    Export-AitherErrorReport
    Write-AitherLog
#>

function Get-AitherErrorLog {
[OutputType([PSCustomObject])]
[CmdletBinding()]
param(
    [Parameter()]
    [DateTime]$Since,

    [Parameter()]
    [DateTime]$Until,

    [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [string[]]$Cmdlet,

    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]$ErrorId,

    [Parameter()]
    [string]$ComputerName,

    [Parameter()]
    [ValidateSet('Error', 'Warning', 'Critical')]
    [string]$Level = 'Error',

    [Parameter()]
    [int]$Count = 100,

    [Parameter()]
    [ValidateSet('Object', 'Table', 'List', 'JSON')]
    [string]$Format = 'Object',

    [switch]$ShowOutput
)

begin {
    # Save original log targets
    $originalLogTargets = $script:AitherLogTargets

    # Set log targets based on ShowOutput parameter
    if ($ShowOutput) {
        # Ensure Console is in the log targets
        if ($script:AitherLogTargets -notcontains 'Console') {
            $script:AitherLogTargets += 'Console'
        }
    }
    else {
        # Remove Console from log targets if present (default behavior)
        if ($script:AitherLogTargets -contains 'Console') {
            $script:AitherLogTargets = $script:AitherLogTargets | Where-Object { $_ -ne 'Console' }
        }
    }

    $moduleRoot = Get-AitherModuleRoot
    $logsPath = Join-Path $moduleRoot 'AitherZero/library/logs'
    $structuredPath = Join-Path $logsPath 'structured'
    $allErrors = @()
}

process { try {
        # Search structured JSON logs
        if (Test-Path $structuredPath) {
            $jsonlFiles = Get-ChildItem -Path $structuredPath -Filter "structured-*.jsonl" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending

            foreach ($file in $jsonlFiles) {
                # Check date range if specified
                if ($Since -and $file.LastWriteTime -lt $Since) {
                    continue
                }
                if ($Until -and $file.CreationTime -gt $Until) {
                    continue
                }

                $lines = Get-Content -Path $file.FullName -ErrorAction SilentlyContinue
                foreach ($line in $lines) {
                    try {
                        $entry = $line | ConvertFrom-Json -AsHashtable -ErrorAction SilentlyContinue

                        if ($entry -and $entry.Level -eq $Level) {
                            # Apply filters
                            if ($Cmdlet -and $entry.Source -notin $Cmdlet) {
                                continue
                            }
                            if ($ErrorId -and $entry.ErrorId -notin $ErrorId) {
                                continue
                            }
                            if ($ComputerName -and $entry.Computer -ne $ComputerName) {
                                continue
                            }
                            if ($Since -and [DateTime]$entry.Timestamp -lt $Since) {
                                continue
                            }
                            if ($Until -and [DateTime]$entry.Timestamp -gt $Until) {
                                continue
                            }

                            $allErrors += $entry
                        }
                    }
                    catch {
                        # Skip invalid JSON lines
                        continue
                    }
                }
            }
        }

        # Also search text logs if structured logs don't have enough
        if ($allErrors.Count -lt $Count -or $Count -eq -1) {
            $textLogFiles = Get-ChildItem -Path $logsPath -Filter "aitherzero-*.log" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending

            foreach ($file in $textLogFiles) {
                if ($Since -and $file.LastWriteTime -lt $Since) {
                    continue
                }
                if ($Until -and $file.CreationTime -gt $Until) {
                    continue
                }

                $lines = Get-Content -Path $file.FullName -ErrorAction SilentlyContinue
                foreach ($line in $lines) {
                    if ($line -match '\[ERROR\]|\[CRITICAL\]') {
                        # Parse log line format: [timestamp] [LEVEL] [Source] Message
                        if ($line -match '\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\]\s+\[(\w+)\]\s+\[([^\]]+)\]\s+(.+)') {
                            $logTimestamp = [DateTime]::Parse($matches[1])
                            $logLevel = $matches[2]
                            $logSource = $matches[3]
                            $logMessage = $matches[4]

                            # Apply filters
                            if ($Level -ne $logLevel -and $logLevel -ne 'CRITICAL') {
                                continue
                            }
        if ($Cmdlet -and $logSource -notin $Cmdlet) {
                                continue
                            }
        if ($Since -and $logTimestamp -lt $Since) {
                                continue
                            }
        if ($Until -and $logTimestamp -gt $Until) {
                                continue
                            }

                            $allErrors += [PSCustomObject]@{
                                Timestamp = $logTimestamp
                                Level = $logLevel
                                Source = $logSource
                                Message = $logMessage
                                ErrorId = [System.Guid]::NewGuid().ToString()
                            }
                        }
                    }
                }
            }
        }
    }
    catch {
        Write-AitherLog -Level Warning -Message "Error reading error logs: $($_.Exception.Message)" -Source $PSCmdlet.MyInvocation.MyCommand.Name
    }
}

end {
    try {
        try {
        # Sort by timestamp (newest first) and limit count
        $sortedErrors = $allErrors | Sort-Object Timestamp -Descending

        if ($Count -gt 0) {
            $sortedErrors = $sortedErrors | Select-Object -First $Count
        }

        # Format output
        switch ($Format) {
            'Table' {
                return $sortedErrors | Format-Table -AutoSize
            }
            'List' {
                return $sortedErrors | Format-List
            }
            'JSON' {
                return $sortedErrors | ConvertTo-Json -Depth 10
            }
            default {
                return $sortedErrors
            }
        }
    }
    catch {
        Invoke-AitherErrorHandler -ErrorRecord $_ -CmdletName $PSCmdlet.MyInvocation.MyCommand.Name -Operation "Retrieving error logs" -Parameters $PSBoundParameters -ThrowOnError
    }
    }
    finally {
        # Restore original log targets
        $script:AitherLogTargets = $originalLogTargets
    }
}


}