Utilities/Logging.ps1
# ================================ # Module: GinShell.Logging # ================================ # Global defaults (can be overridden from calling script/profile) $global:GsDefaultLogLevel = 'verbose' $global:GsEnableFileLogging = $true $global:GsEnableLokiLogging = $false $global:GsLogDirectory = "$env:ProgramData\Ginesys\CloudAdmin\GinShell\" $global:GsLokiUri = "http://monitoring.ginesys.cloud:3100" $global:GsLokiLabels = $null # Default to null, will be set later # ---------------------------------------- # Helper: Convert log type to numeric level # ---------------------------------------- function script:Get-LogLevelValue { param ([string]$Level) switch ($Level.ToLowerInvariant()) { 'verbose' { return 0 } 'debug' { return 1 } 'action' { return 2 } 'success' { return 3 } 'info' { return 4 } 'warning' { return 5 } 'error' { return 6 } 'critical' { return 7 } default { return 0 } } } # ---------------------------------------- # Helper: Color for console log type # ---------------------------------------- function script:Get-LogColor { param ([string]$Type) switch ($Type.ToLowerInvariant()) { 'verbose' { return 'DarkGray' } 'debug' { return 'Gray' } 'action' { return 'Cyan' } 'success' { return 'Green' } 'info' { return 'White' } 'warning' { return 'Yellow' } 'error' { return 'Red' } 'critical' { return 'Magenta' } default { return 'White' } } } # ---------------------------------------- # Core: Write-Log # ---------------------------------------- function Write-Log { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Message, [Parameter()] [ValidateSet('verbose', 'debug', 'action', 'success', 'info', 'warning', 'error', 'critical')] [string]$Type = 'Info', [Parameter()] [ValidateSet('verbose', 'debug', 'action', 'success', 'info', 'warning', 'error', 'critical')] [string]$LogLevel = $global:GsDefaultLogLevel, [Parameter()] [string]$LogFile, [Parameter()] [bool]$EnableFileLogging = $global:GsEnableFileLogging, [Parameter()] [bool]$EnableLokiLogging = $global:GsEnableLokiLogging ) $threshold = Get-LogLevelValue -Level $LogLevel $current = Get-LogLevelValue -Level $Type if ($current -lt $threshold) { return } $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $scriptName = $MyInvocation.PSCommandPath if (-not $scriptName) { $scriptName = "<interactive>" } $logMessage = "$timestamp [$Type] - $Message - [$scriptName]" $color = Get-LogColor -Type $Type Write-Host $logMessage -ForegroundColor $color # --- File Logging --- if ($EnableFileLogging) { if (-not $LogFile) { $baseName = if ($MyInvocation.PSCommandPath) { [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.PSCommandPath) } else { "interactive" } $LogFile = Join-Path -Path $global:GsLogDirectory -ChildPath "$(Get-Date -Format 'yyyy_MM_dd')__$baseName.log" } else { $baseName = [System.IO.Path]::GetFileNameWithoutExtension($LogFile) } $logDir = Split-Path -Path $LogFile -Parent if (-not (Test-Path -Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } Add-Content -Path $LogFile -Value $logMessage } # --- Loki Logging --- if ($EnableLokiLogging) { if ($global:GsLokiLabels -is [hashtable]) { Write-LokiLog -Message $Message -Type $Type } else { # If no global labels are set, use default labels Write-LokiLog -Message $Message -Type $Type -Labels @{ app = if ($MyInvocation.ScriptName) { [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.ScriptName) } else { 'Interactive_PowerShell' } hostname = $env:COMPUTERNAME script_path = if ($MyInvocation.ScriptName) { $MyInvocation.ScriptName } else { 'Interactive_PowerShell' } run_as = $env:USERNAME powershell_version = $PSVersionTable.PSVersion.ToString() } } } } # ---------------------------------------- # Core: Write-GsLokiLog # ---------------------------------------- function Write-LokiLog { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Message, [Parameter()] [ValidateSet('verbose', 'debug', 'action', 'success', 'info', 'warning', 'error', 'critical')] [string]$Type = 'info', [Parameter()] [hashtable]$Labels, [Parameter()] [string]$LokiUri ) $resolvedUri = $LokiUri if ([string]::IsNullOrWhiteSpace($resolvedUri)) { $resolvedUri = $global:GsLokiUri } if ([string]::IsNullOrWhiteSpace($resolvedUri)) { $resolvedUri = $env:GS_LOKI_URI } if ([string]::IsNullOrWhiteSpace($resolvedUri)) { Write-Error "The loki endpoints is not set yet, Hence unable to send log to loki endpoint" ; return } $finalLabels = @{} if ($global:GsLokiLabels -is [hashtable]) { $global:GsLokiLabels.GetEnumerator() | ForEach-Object { $finalLabels[$_.Key] = $_.Value } } if (-not [string]::IsNullOrWhiteSpace($env:GS_LOKI_LABELS)) { try { $envLabels = $env:GS_LOKI_LABELS | ConvertFrom-Json -AsHashtable $envLabels.GetEnumerator() | ForEach-Object { $finalLabels[$_.Key] = $_.Value } } catch { Write-Warning "Invalid JSON in GS_LOKI_LABELS environment variable: $($env:GS_LOKI_LABELS)" } } if ($Labels -is [hashtable] ) { $Labels.GetEnumerator() | ForEach-Object { $finalLabels[$_.Key] = $_.Value } } if (-not $finalLabels.ContainsKey('type')) { $finalLabels['type'] = $Type } if (-not $finalLabels.ContainsKey('hostname')) { $finalLabels['hostname'] = $env:COMPUTERNAME } if (-not $finalLabels.ContainsKey('script_path')) { $finalLabels['script'] = if ($MyInvocation.ScriptName) { $MyInvocation.ScriptName }else { 'Interactive_powershell' } } if (-not $finalLabels.ContainsKey('run_as')) { $finalLabels['run_as'] = $env:USERNAME } if ( -not $finalLabels.ContainsKey('powershell_version')) { $finalLabels['powershell_version'] = $PSVersionTable.PSVersion.ToString() } # If 'app' is not already set, derive from script file name or fallback if (-not $finalLabels.ContainsKey('app')) { if ($MyInvocation.ScriptName) { # Extract only the file name from the full path $finalLabels['app'] = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.ScriptName) } else { $finalLabels['app'] = 'Interactive_PowerShell' } } # Getting Time Stamp in Nanoseconds # This is compatible with both PowerShell 6+ and Windows PowerShell 5. $timestampNs = if ($PSVersionTable.PSVersion.Major -ge 6) { # PowerShell 6+ method: More modern and concise. # It uses [datetimeoffset]::UnixEpoch which is guaranteed to be available. [bigint](([datetimeoffset]::UtcNow) - ([datetimeoffset]::UnixEpoch)).Ticks * 100 } else { # Windows PowerShell 5.1 and older method: Maximizes compatibility. # Manually define the epoch because [datetimeoffset]::UnixEpoch may be missing. $unixEpoch = New-Object DateTime(1970, 1, 1, 0, 0, 0, [System.DateTimeKind]::Utc) # Get current UTC time and calculate the TimeSpan since epoch. $timeSpan = (Get-Date).ToUniversalTime() - $unixEpoch # Calculate nanoseconds and format as a string to prevent scientific notation. ($timeSpan.Ticks * 100).ToString('F0') } # Ensure 'values' is an array of arrays $payload = @{ streams = @( @{ stream = $finalLabels values = @( , @([string]$timestampNs, $Message) # ← Note the comma to force nested array ) } ) } $jsonPayload = $payload | ConvertTo-Json -Depth 5 -Compress $pushUrl = "$($resolvedUri.TrimEnd('/'))/loki/api/v1/push" try { $output = Invoke-RestMethod -Uri $pushUrl -Method Post -Body $jsonPayload -ContentType 'application/json' -ErrorAction Stop Write-Verbose "Loki log sent to $pushUrl and received response: $($output | ConvertTo-Json -Depth 5)" } catch { Write-Error "Loki log failed: $($_.Exception.Message)" Write-Debug "Payload: $jsonPayload" } } # ---------------------------------------- # Export Module Members # ---------------------------------------- Export-ModuleMember -Function Write-Log, Write-LokiLog |