Action.Logging.psm1

# ========================================
# Action.Logging Module
# ========================================
#
# Module Version : 2.0
# Date Created: 06-21-2025
# Last Revised: 06-22-2025
# Created by : Action.Logging Contributors
# Required files: None
# Required folders: None
#
# Licensed under the MIT License.
# See LICENSE file in the project root for full license information.
#
# ========================================

<#
.SYNOPSIS
    Action.Logging - Enhanced PowerShell Logging Module with async support.
 
.LINK
    For full documentation, see README.md
#>


# Enhanced logging structure with severity levels
enum LogLevel {
    ERROR = 1
    WARN = 2
    INFO = 3
    DEBUG = 4
    TRACE = 5
}

# Module-level variables for async logging
$script:LogQueue = [System.Collections.Concurrent.ConcurrentQueue[PSObject]]::new()
$script:LoggerRunspace = $null
$script:LoggerPowerShell = $null
$script:CurrentLogFile = $null
$script:AsyncLoggingEnabled = $false
$script:LoggingStopRequested = $false
$script:ConsoleOutputEnabled = $true

<#
.SYNOPSIS
    Writes enhanced log entries with severity levels and color coding.
 
.DESCRIPTION
    Creates timestamped log entries with severity levels, saves to log files,
    and displays color-coded output to the console. Automatically creates
    log directories if they don't exist.
 
.PARAMETER Message
    The message to log.
 
.PARAMETER Level
    The severity level of the log entry. Default is INFO.
 
.PARAMETER LogPath
    The path where log files will be stored. Default is based on script name.
 
.EXAMPLE
    Write-EnhancedLog -Message "Script started" -Level INFO
 
.EXAMPLE
    Write-EnhancedLog -Message "Critical error occurred" -Level ERROR
 
.EXAMPLE
    Write-EnhancedLog -Message "Debug information" -Level DEBUG -LogPath "C:\custom\logs"
#>

function Write-EnhancedLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,
        
        [Parameter(Mandatory = $false)]
        [LogLevel]$Level = [LogLevel]::INFO,
        
        [Parameter(Mandatory = $false)]
        [string]$LogPath
    )
    
    # If no LogPath specified, use calling script name
    if (-not $LogPath) {
        $callingScript = (Get-PSCallStack)[1].ScriptName
        if ($callingScript) {
            $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($callingScript)
            $LogPath = "C:\temp\automation\logs\$scriptName"
        } else {
            $LogPath = "C:\temp\automation\logs\PowerShell"
        }
    }
    
    try {
        # Ensure log directory exists
        if (-not (Test-Path $LogPath)) {
            New-Item -ItemType Directory -Path $LogPath -Force | Out-Null
        }
        
        # Create session-based log file if not exists
        if (-not $script:CurrentLogFile) {
            $script:CurrentLogFile = "$LogPath\$(Get-Date -Format 'MMddyyyy-HHmmss').log"
            # Clean up old logs when creating new session
            Clear-OldLog -LogPath $LogPath
        }
        
        $timestamp = Get-Date -Format 'MM-dd-yyyy HH:mm:ss'
        $logEntry = "$timestamp [$Level] $Message"
        
        # Write to log file
        Add-Content -Path $script:CurrentLogFile -Value $logEntry -Encoding UTF8 -ErrorAction SilentlyContinue
        
        # Output to console with color coding if enabled
        if ($script:ConsoleOutputEnabled) {
            switch ($Level) {
                ([LogLevel]::ERROR) { Write-Host $logEntry -ForegroundColor Red }
                ([LogLevel]::WARN) { Write-Host $logEntry -ForegroundColor Yellow }
                ([LogLevel]::INFO) { Write-Host $logEntry -ForegroundColor Green }
                ([LogLevel]::DEBUG) { Write-Host $logEntry -ForegroundColor Cyan }
                ([LogLevel]::TRACE) { Write-Host $logEntry -ForegroundColor Gray }
            }
        }
    }
    catch {
        if ($script:ConsoleOutputEnabled) {
            Write-Warning "Failed to write to log: $($_.Exception.Message)"
            # Fallback to console output only
            Write-Host "$timestamp [$Level] $Message" -ForegroundColor White
        }
    }
}

<#
.SYNOPSIS
    Cleans up old log files, keeping only the most recent ones.
 
.DESCRIPTION
    Removes old log files from the specified path, keeping only the last
    10 log files to manage disk space. Files are sorted by creation time.
 
.PARAMETER LogPath
    The path containing log files to clean up.
 
.PARAMETER KeepCount
    Number of recent log files to keep. Default is 10.
 
.EXAMPLE
    Clear-OldLogs -LogPath "C:\temp\automation\logs\MyScript"
 
.EXAMPLE
    Clear-OldLogs -LogPath "C:\temp\automation\logs\MyScript" -KeepCount 5
#>

function Clear-OldLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$LogPath,
        
        [Parameter(Mandatory = $false)]
        [int]$KeepCount = 10
    )
    
    try {
        if (Test-Path $LogPath) {
            $logFiles = Get-ChildItem -Path $LogPath -Filter "*.log" | 
                        Sort-Object CreationTime -Descending
            
            if ($logFiles.Count -gt $KeepCount) {
                $filesToDelete = $logFiles[$KeepCount..($logFiles.Count - 1)]
                $filesToDelete | Remove-Item -Force -ErrorAction SilentlyContinue
                Write-Verbose "Cleaned up $($filesToDelete.Count) old log files from $LogPath"
            }
        }
    }
    catch {
        Write-Warning "Failed to clean up old logs in ${LogPath}: $($_.Exception.Message)"
    }
}

# Start async logging background worker
function Start-AsyncLogger {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $false)]
        [string]$LogPath
    )
    
    # Don't start if already running
    if ($script:AsyncLoggingEnabled) {
        Write-Warning "Async logging is already running"
        return
    }
    
    if ($PSCmdlet.ShouldProcess("Async Logger", "Start background logging")) {
    
    # Initialize log path
    if (-not $LogPath) {
        $callingScript = (Get-PSCallStack)[1].ScriptName
        if ($callingScript) {
            $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($callingScript)
            $LogPath = "C:\temp\automation\logs\$scriptName"
        } else {
            $LogPath = "C:\temp\automation\logs\PowerShell"
        }
    }
    
    # Ensure log directory exists
    if (-not (Test-Path $LogPath)) {
        New-Item -ItemType Directory -Path $LogPath -Force | Out-Null
    }
    
    # Create session log file
    $script:CurrentLogFile = "$LogPath\$(Get-Date -Format 'MMddyyyy-HHmmss').log"
    Clear-OldLog -LogPath $LogPath
    
    # Create runspace for background logging
    $script:LoggerRunspace = [RunspaceFactory]::CreateRunspace()
    $script:LoggerRunspace.ApartmentState = "STA"
    $script:LoggerRunspace.ThreadOptions = "ReuseThread"
    $script:LoggerRunspace.Open()
    
    # Create synchronized hashtable for shared state
    $syncHash = [hashtable]::Synchronized(@{
        LogQueue = $script:LogQueue
        CurrentLogFile = $script:CurrentLogFile
        StopRequested = $false
        ConsoleOutputEnabled = $script:ConsoleOutputEnabled
    })
    
    # Share synchronized hashtable with runspace
    $script:LoggerRunspace.SessionStateProxy.SetVariable("syncHash", $syncHash)
    
    # Create PowerShell instance for background work
    $script:LoggerPowerShell = [PowerShell]::Create()
    $script:LoggerPowerShell.Runspace = $script:LoggerRunspace
    
    # Define background worker script
    $workerScript = {
        $logBuffer = [System.Collections.Generic.List[string]]::new()
        $lastFlush = [DateTime]::Now
        $flushInterval = [TimeSpan]::FromMilliseconds(500)
        
        while (-not $syncHash.StopRequested) {
            $hasMessages = $false
            $message = $null
            
            # Collect messages from queue
            while ($syncHash.LogQueue.TryDequeue([ref]$message)) {
                $hasMessages = $true
                $logBuffer.Add($message.Entry)
                
                # Console output based on level if enabled
                if ($syncHash.ConsoleOutputEnabled) {
                    switch ($message.Level) {
                        'ERROR' { Write-Host $message.Entry -ForegroundColor Red }
                        'WARN' { Write-Host $message.Entry -ForegroundColor Yellow }
                        'INFO' { Write-Host $message.Entry -ForegroundColor Green }
                        'DEBUG' { Write-Host $message.Entry -ForegroundColor Cyan }
                        'TRACE' { Write-Host $message.Entry -ForegroundColor Gray }
                    }
                }
            }
            
            # Flush buffer to file periodically or when buffer is large
            $shouldFlush = $logBuffer.Count -gt 0 -and (
                $logBuffer.Count -ge 50 -or 
                ([DateTime]::Now - $lastFlush) -gt $flushInterval
            )
            
            if ($shouldFlush) {
                try {
                    Add-Content -Path $syncHash.CurrentLogFile -Value $logBuffer -Encoding UTF8
                    $logBuffer.Clear()
                    $lastFlush = [DateTime]::Now
                }
                catch {
                    Write-Warning "Async logger failed to write: $_"
                }
            }
            
            # Sleep briefly if no messages
            if (-not $hasMessages) {
                Start-Sleep -Milliseconds 50
            }
        }
        
        # Final flush on shutdown
        if ($logBuffer.Count -gt 0) {
            try {
                Add-Content -Path $syncHash.CurrentLogFile -Value $logBuffer -Encoding UTF8
            }
            catch {
                Write-Warning "Final flush failed: $_"
            }
        }
    }
    
    # Start background worker
    $null = $script:LoggerPowerShell.AddScript($workerScript)
    $null = $script:LoggerPowerShell.BeginInvoke()
    
    # Store sync hash reference
    $script:SyncHash = $syncHash
    
    $script:AsyncLoggingEnabled = $true
    Write-Host "Async logging started for $LogPath" -ForegroundColor Green
    }
}

# Stop async logging gracefully
function Stop-AsyncLogger {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    
    if (-not $script:AsyncLoggingEnabled) {
        return
    }
    
    if ($PSCmdlet.ShouldProcess("Async Logger", "Stop background logging")) {
    
    Write-Host "Stopping async logger..." -ForegroundColor Yellow
    
    # Signal stop
    $script:SyncHash.StopRequested = $true
    
    # Wait for queue to empty (max 5 seconds)
    $timeout = [DateTime]::Now.AddSeconds(5)
    while ($script:LogQueue.Count -gt 0 -and [DateTime]::Now -lt $timeout) {
        Start-Sleep -Milliseconds 100
    }
    
    # Clean up resources
    if ($script:LoggerPowerShell) {
        $script:LoggerPowerShell.Stop()
        $script:LoggerPowerShell.Dispose()
        $script:LoggerPowerShell = $null
    }
    
    if ($script:LoggerRunspace) {
        $script:LoggerRunspace.Close()
        $script:LoggerRunspace.Dispose()
        $script:LoggerRunspace = $null
    }
    
    $script:AsyncLoggingEnabled = $false
    $script:LoggingStopRequested = $false
    Write-Host "Async logger stopped" -ForegroundColor Green
    }
}

# Write log entry asynchronously
function Write-EnhancedLogAsync {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,
        
        [Parameter(Mandatory = $false)]
        [LogLevel]$Level = [LogLevel]::INFO
    )
    
    # Fall back to sync logging if async not enabled
    if (-not $script:AsyncLoggingEnabled) {
        Write-EnhancedLog -Message $Message -Level $Level
        return
    }
    
    $timestamp = Get-Date -Format 'MM-dd-yyyy HH:mm:ss'
    $logEntry = "$timestamp [$Level] $Message"
    
    # Queue the message
    $logMessage = [PSCustomObject]@{
        Entry = $logEntry
        Level = $Level.ToString()
    }
    
    $script:LogQueue.Enqueue($logMessage)
}

# Control console output for logging
function Set-LoggingConsoleOutput {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [bool]$Enabled
    )
    
    if ($PSCmdlet.ShouldProcess("Console Output", "Set logging console output to $Enabled")) {
        $script:ConsoleOutputEnabled = $Enabled
    
    # Update async logger if running
    if ($script:AsyncLoggingEnabled -and $script:SyncHash) {
        $script:SyncHash.ConsoleOutputEnabled = $Enabled
    }
    
        $status = if ($Enabled) { "enabled" } else { "disabled" }
        Write-Verbose "Console output $status for logging module"
    }
}

# Export module functions and enum
Export-ModuleMember -Function Write-EnhancedLog, Clear-OldLog, Start-AsyncLogger, Stop-AsyncLogger, Write-EnhancedLogAsync, Set-LoggingConsoleOutput