RunLog.psm1

# RunLog.psm1 - Simplified PowerShell Logging Module

# Enum for log levels
enum LogLevel {
    Debug = 0
    Information = 1
    Warning = 2
    Error = 3
    Critical = 4
}

# Main Logger Class
class RunLogger {
    [string]$LogFilePath
    [LogLevel]$MinimumLogLevel
    
    # Constructor
    RunLogger([string]$logFilePath)
    {
        $this.LogFilePath = $logFilePath
        $this.MinimumLogLevel = [LogLevel]::Information
        $this.Initialize()
    }
    
    RunLogger([string]$logFilePath, [LogLevel]$minimumLogLevel)
    {
        $this.LogFilePath = $logFilePath
        $this.MinimumLogLevel = $minimumLogLevel
        $this.Initialize()
    }
    
    # Initialize method to ensure log directory exists
    hidden [void]Initialize()
    {
        $logDirectory = Split-Path -Path $this.LogFilePath -Parent
        if (-not (Test-Path -Path $logDirectory))
        {
            New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null
        }
    }
    
    # Private method to check if log level should be written
    hidden [bool]ShouldLog([LogLevel]$level)
    {
        return $level -ge $this.MinimumLogLevel
    }
    
    # Core logging method - handles all the heavy lifting
    hidden [void]WriteLogEntry([LogLevel]$level, [string]$message, [System.Exception]$exception = $null)
    {
        if (-not $this.ShouldLog($level)) { return }
        
        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
        $processId = [System.Diagnostics.Process]::GetCurrentProcess().Id
        $threadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId
        
        # Build the log entry
        $logEntry = "[$timestamp] [$processId`:$threadId] [$level] $message"
        
        # Add exception details if provided
        if ($exception)
        {
            $logEntry += "`n Exception: $($exception.GetType().Name): $($exception.Message)"
            if ($exception.StackTrace)
            {
                $logEntry += "`n StackTrace: $($exception.StackTrace)"
            }
        }
        
        # Simple retry with exponential backoff
        $maxRetries = 5
        $retryDelay = 50 # milliseconds
        $mutex = $null
        
        for ($i = 0; $i -lt $maxRetries; $i++)
        {
            try
            {
                # Use mutex for thread safety across processes
                $mutexName = "RunLogger_" + ($this.LogFilePath -replace '[\\/:*?"<>|]', '_')
                $mutex = [System.Threading.Mutex]::new($false, $mutexName)
                
                if ($mutex.WaitOne(1000))
                {
                    # 1 second timeout
                    try
                    {
                        # Atomic write operation
                        [System.IO.File]::AppendAllText($this.LogFilePath, $logEntry + [Environment]::NewLine, [System.Text.Encoding]::UTF8)
                        return # Success
                    }
                    finally
                    {
                        $mutex.ReleaseMutex()
                    }
                }
                else
                {
                    throw "Mutex timeout - file may be locked"
                }
            }
            catch
            {
                if ($i -eq ($maxRetries - 1))
                {
                    # Final fallback - write to console
                    Write-Host "[$timestamp] [CONSOLE-FALLBACK] [$level] $message" -ForegroundColor Yellow
                    if ($exception)
                    {
                        Write-Host " Exception: $($exception.GetType().Name): $($exception.Message)" -ForegroundColor Red
                    }
                    return
                }
                else
                {
                    Start-Sleep -Milliseconds $retryDelay
                    $retryDelay *= 2 # Exponential backoff
                }
            }
            finally
            {
                if ($mutex)
                {
                    $mutex.Dispose()
                    $mutex = $null
                }
            }
        }
    }
    
    # Public logging methods
    [void]Debug([string]$message) { $this.WriteLogEntry([LogLevel]::Debug, $message, $null) }
    [void]Debug([string]$message, [System.Exception]$exception) { $this.WriteLogEntry([LogLevel]::Debug, $message, $exception) }
    
    [void]Information([string]$message) { $this.WriteLogEntry([LogLevel]::Information, $message, $null) }
    [void]Information([string]$message, [System.Exception]$exception) { $this.WriteLogEntry([LogLevel]::Information, $message, $exception) }
    
    [void]Warning([string]$message) { $this.WriteLogEntry([LogLevel]::Warning, $message, $null) }
    [void]Warning([string]$message, [System.Exception]$exception) { $this.WriteLogEntry([LogLevel]::Warning, $message, $exception) }
    
    [void]Error([string]$message) { $this.WriteLogEntry([LogLevel]::Error, $message, $null) }
    [void]Error([string]$message, [System.Exception]$exception) { $this.WriteLogEntry([LogLevel]::Error, $message, $exception) }
    
    [void]Critical([string]$message) { $this.WriteLogEntry([LogLevel]::Critical, $message, $null) }
    [void]Critical([string]$message, [System.Exception]$exception) { $this.WriteLogEntry([LogLevel]::Critical, $message, $exception) }
}

# Helper functions
function New-RunLogger
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [string]$LogFilePath,
        [Parameter(Mandatory = $false)]
        [LogLevel]$MinimumLogLevel = [LogLevel]::Information
    )
    
    if ($PSCmdlet.ShouldProcess("Creating logger with file path '$LogFilePath'"))
    {
        return [RunLogger]::new($LogFilePath, $MinimumLogLevel)
    }
}

function Get-LogLevel
{
    [CmdletBinding()]
    param ()
    
    return [LogLevel].GetEnumNames()
}

# Export functions
Export-ModuleMember -Function @('New-RunLogger', 'Get-LogLevel')