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 |