FastNativeLogger.psm1

<#
.SYNOPSIS
    Initializes a high-performance, buffered .NET StreamWriter for script-wide logging.
 
.DESCRIPTION
    Sets up a persistent file handle using [System.IO.File]::Open with FileShare.Read permissions.
    This allows other processes (like Tail-File or Notepad) to read the log while the script is writing.
    It utilizes a 4KB buffer and disables AutoFlush to minimize I/O overhead.
 
.PARAMETER LogDirectory
    The directory where the log file will be created. Defaults to $PSScriptRoot or the current location.
 
.PARAMETER LogFile
    The name or full path of the log file. If only a name is provided, it combines with LogDirectory.
 
.EXAMPLE
    Initialize-Logger -LogDirectory "C:\Logs" -LogFile "BackupSync.log"
#>

function Initialize-Logger {
    [CmdletBinding()]
    param(
        [string]$LogDirectory,
        [string]$LogFile
    )
    $finalDir   = $null
    $finalFile  = $null
    $base       = if (-not ([string]::IsNullOrWhiteSpace($PSScriptRoot))) {
        $PSScriptRoot
    }
    else {
        (Get-Location).Path
    }

    if ($LogFile) {
        $candidate      = $LogFile
        $dirFromFile    = Split-Path -Path $candidate
        if ([string]::IsNullOrWhiteSpace($dirFromFile)) {
            if ($LogDirectory) {
                $finalDir   = $LogDirectory
                $finalFile  = Join-Path $finalDir $candidate
            }
            else {
                $finalDir   = $base
                $finalFile  = Join-Path $base $candidate
            }
        }
        else {
            $finalDir   = $dirFromFile
            $finalFile  = $candidate
        }
        if (-not (Test-Path -LiteralPath $finalFile -PathType Leaf)) {
            try {
                $null = New-Item -ItemType File -Path $finalFile -Force
            }
            catch {
                throw "LogFile '$finalFile' is invalid or cannot be created. $_"
            }
        }
    }
    elseif ($LogDirectory) {
        $finalDir   = $LogDirectory
        $finalFile  = Join-Path $finalDir 'log.txt'
    }
    else {
        $finalDir   = $base
        $finalFile  = Join-Path $base 'log.txt'
    }

    try {
        if (Test-Path -LiteralPath $finalDir) {
            if (-not (Test-Path -LiteralPath $finalDir -PathType Container)) {
                throw "LogDirectory path exists but is not a directory: '$finalDir'"
            }
        }
        else {
            $null = New-Item -ItemType Directory -Path $finalDir -Force
        }
    }
    catch {
        throw "LogDirectory '$finalDir' is invalid or cannot be created. $_"
    }

    try {
        if (Test-Path -LiteralPath $finalFile) {
            if (-not (Test-Path -LiteralPath $finalFile -PathType Leaf)) {
                throw "LogFile path exists but is not a file: '$finalFile'"
            }
        }
        else {
            $null = New-Item -ItemType File -Path $finalFile -Force
        }
    }
    catch {
        throw "LogFile '$finalFile' is invalid or cannot be created. $_"
    }

    $fs = [System.IO.File]::Open(
        $finalFile,
        [System.IO.FileMode]::Append,
        [System.IO.FileAccess]::Write,
        [System.IO.FileShare]::Read
    )
    $enc                = [System.Text.UTF8Encoding]::new($false)
    $writer             = [System.IO.StreamWriter]::new($fs, $enc, 4096)
    $writer.AutoFlush   = $false
    $Script:Logger      = [pscustomobject]@{
        Path   = $finalFile
        Writer = $writer
    }
}

<#
.SYNOPSIS
    Writes a timestamped entry to the log buffer and optionally to the console.
 
.DESCRIPTION
    Captures a UTC ISO 8601 timestamp and writes the message to the internal .NET StreamWriter.
    If the $Silent variable is not set to $true, it also renders the output to the console
    with ANSI-mapped foreground colors based on severity.
 
.PARAMETER Severity
    The severity of the message. Validated against a set including Emergency, Alert, Critical, Error, Warning, Notice, Info and Debug.
    Follow syslog rfc. https://www.rfc-editor.org/rfc/rfc5424
 
.PARAMETER Message
    The string content to be logged.
 
.EXAMPLE
    Write-Log -Severity Error -Message "SERVER connection failed: Timeout"
 
.NOTES
    Console output is synchronous; for maximum throughput in loops, suppress console output using $Silent = $true.
    TODO: Implement more format specifier for flexible timestamp formatting in the future.
    (https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings#table-of-format-specifiers)
#>

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [ValidateSet('Emergency', 'Alert', 'Critical', 'Error', 'Warning', 'Notice', 'Info', 'Debug', IgnoreCase = $false)]
        [string]$Severity = "Info",
        [Parameter(Position = 1, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Message
    )
    if ((-not ($Script:Logger)) -or (-not ($Script:Logger.Writer))) {
        throw "Logger not initialized. Call Initialize-Logger first."
    }

    $ts = [DateTime]::UtcNow.ToString("o")
    $Script:Logger.Writer.WriteLine("$ts [$Severity] $Message")

    # Console output unless suppressed
    if (-not ($Silent)) {
        switch ($Severity) {
            'Emergency' { $color = 'Red'        }
            'Alert'     { $color = 'DarkRed'    }
            'Critical'  { $color = 'DarkRed'    }
            'Error'     { $color = 'Magenta'    }
            'Warning'   { $color = 'Yellow'     }
            'Notice'    { $color = 'Cyan'       }
            'Info'      { $color = 'Green'      }
            'Debug'     { $color = 'Gray'       }
            default     { $color = 'White'      }
        }
        Write-Host "$ts " -NoNewline
        Write-Host "$Severity " -NoNewline -ForegroundColor $color
        Write-Host "$Message "
    }
}

<#
.SYNOPSIS
    Manually flushes the StreamWriter buffer to the physical disk.
 
.DESCRIPTION
    Because AutoFlush is disabled for performance, log entries reside in a 4KB RAM buffer.
    Call this function to ensure all pending entries are written to disk without closing the file handle.
 
.EXAMPLE
    Clear-Logger
 
.NOTES
    Use this after critical operations or before a long-running external process to ensure
    the log file is up to date.
#>

function Clear-Logger {
    [CmdletBinding()]
    param()
    if ($Script:Logger -and $Script:Logger.Writer) {
        $Script:Logger.Writer.Flush()
    }
}

<#
.SYNOPSIS
    Safely closes the log file handle and disposes of the StreamWriter.
 
.DESCRIPTION
    Flushes any remaining buffered data, closes the underlying FileStream, and clears
    the $Script:Logger object from memory.
 
.EXAMPLE
    Close-Logger
 
.NOTES
    Always include this in a 'finally' block or at the end of your script to prevent
    file-lock issues and memory leaks.
#>

function Close-Logger {
    [CmdletBinding()]
    param()
    if ($Script:Logger -and $Script:Logger.Writer) {
        try {
            $Script:Logger.Writer.Close() 
        }
        finally {
            $Script:Logger = $null 
        }
    }
}

Export-ModuleMember -Function Initialize-Logger, Write-Log, Clear-Logger, Close-Logger