#region Globals if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { $Global:LogCache = [System.Collections.ArrayList]::new() } if (-not $Global:EventSinkCache -or -not ($Global:EventSinkCache -is [hashtable])) { $Global:EventSinkCache = @{} } #endregion Globals #region Helpers: Formatting + File function Get-LogColor { param( [ValidateSet("Info", "Warning", "Error", "Success", "General")] [string]$Level ) switch ($Level) { "Info" { "Cyan" } "Warning" { "Yellow" } "Error" { "Red" } "Success" { "Green" } default { "White" } } } function Get-EventIdForLevel { param( [ValidateSet("Info", "Warning", "Error", "Success", "General")] [string]$Level, [int]$CustomEventID ) if ($CustomEventID) { return $CustomEventID } switch ($Level) { "Info" { 1000 } "Warning" { 2000 } "Error" { 3000 } "Success" { 4000 } default { 1000 } } } function Get-EventEntryTypeForLevel { param( [ValidateSet("Info", "Warning", "Error", "Success", "General")] [string]$Level ) switch ($Level) { "Info" { "Information" } "Warning" { "Warning" } "Error" { "Error" } "Success" { "Information" } default { "Information" } } } function Append-Utf8NoBomLine { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [string]$Line ) $utf8NoBom = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::AppendAllText($Path, $Line + [Environment]::NewLine, $utf8NoBom) } #endregion Helpers: Formatting + File #region Helpers: Event Log Binding function Test-IsAdmin { try { $current = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($current) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { return $false } } function Initialize-EventLogBinding { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$DesiredLog, [Parameter(Mandatory)] [string]$DesiredSource, [ValidateSet('Repair', 'Unique', 'Follow')] [string]$ConflictPolicy = 'Repair' ) $isAdmin = Test-IsAdmin $effectiveLog = $DesiredLog $effectiveSource = $DesiredSource function Ensure-LogAndSource { param([string]$LogName, [string]$SourceName) if (-not $isAdmin) { return $false } if (-not [System.Diagnostics.EventLog]::SourceExists($SourceName)) { New-EventLog -LogName $LogName -Source $SourceName -ErrorAction Stop } elseif ([System.Diagnostics.EventLog]::LogNameFromSourceName($SourceName, '.') -ne $LogName) { return $false } return $true } if ([System.Diagnostics.EventLog]::SourceExists($DesiredSource)) { $boundLog = [System.Diagnostics.EventLog]::LogNameFromSourceName($DesiredSource, '.') if ($boundLog -ne $DesiredLog) { switch ($ConflictPolicy) { 'Follow' { $effectiveLog = $boundLog $effectiveSource = $DesiredSource } 'Unique' { $candidate = "$DesiredSource.SAMY" $i = 0 while ([System.Diagnostics.EventLog]::SourceExists($candidate)) { $i++ $candidate = "$DesiredSource.SAMY$i" } $effectiveLog = $DesiredLog $effectiveSource = $candidate $ok = Ensure-LogAndSource -LogName $effectiveLog -SourceName $effectiveSource if (-not $ok) { throw "Unable to create unique Event Log source '$effectiveSource' under '$effectiveLog'." } } 'Repair' { if (-not $isAdmin) { throw "Event source '$DesiredSource' is bound to '$boundLog' but repair requires elevation." } if (Get-Command Remove-EventLog -ErrorAction SilentlyContinue) { Remove-EventLog -Source $DesiredSource -ErrorAction Stop } else { $regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\$boundLog\$DesiredSource" if (Test-Path $regPath) { Remove-Item -Path $regPath -Recurse -Force -ErrorAction Stop } } $effectiveLog = $DesiredLog $effectiveSource = $DesiredSource Ensure-LogAndSource -LogName $effectiveLog -SourceName $effectiveSource | Out-Null $verify = [System.Diagnostics.EventLog]::LogNameFromSourceName($effectiveSource, '.') if ($verify -ne $effectiveLog) { throw "Repair failed: '$effectiveSource' is still bound to '$verify' (wanted '$effectiveLog')." } } } } else { # Bound correctly. Nothing else required. $effectiveLog = $DesiredLog $effectiveSource = $DesiredSource } } else { if ($isAdmin) { $effectiveLog = $DesiredLog $effectiveSource = $DesiredSource Ensure-LogAndSource -LogName $effectiveLog -SourceName $effectiveSource | Out-Null } else { $effectiveLog = 'Application' $effectiveSource = 'Windows PowerShell' } } [pscustomobject]@{ LogName = $effectiveLog; Source = $effectiveSource; IsAdmin = $isAdmin } } #endregion Helpers: Event Log Binding #region Public: Write-LogHelper function global:Write-LogHelper { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Message, [ValidateSet("Info", "Warning", "Error", "Success", "General")] [string]$Level = "Info", [string]$TaskCategory = "GeneralTask", [switch]$LogToEvent = $false, [string]$EventSource = "SAMY", [string]$EventLog = "SVSMSP Events", [ValidateSet('Repair', 'Unique', 'Follow')] [string]$EventLogConflictPolicy = 'Repair', [int]$CustomEventID, [string]$LogFile, [switch]$PassThru ) $EventID = Get-EventIdForLevel -Level $Level -CustomEventID $CustomEventID $Color = Get-LogColor -Level $Level $EntryType = Get-EventEntryTypeForLevel -Level $Level $FormattedMessage = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)" Write-Host $FormattedMessage -ForegroundColor $Color $logEntry = [PSCustomObject]@{ Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") Level = $Level Message = $FormattedMessage } [void]$Global:LogCache.Add($logEntry) if ($LogFile) { try { Append-Utf8NoBomLine -Path $LogFile -Line "$($logEntry.Timestamp) $FormattedMessage" } catch { Write-Host "[Warning] Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Yellow } } if ($LogToEvent) { $desiredKey = "$EventLog|$EventSource|$EventLogConflictPolicy" if (-not $Global:EventSinkCache.ContainsKey($desiredKey)) { try { $ev = Initialize-EventLogBinding -DesiredLog $EventLog -DesiredSource $EventSource -ConflictPolicy $EventLogConflictPolicy $Global:EventSinkCache[$desiredKey] = [pscustomobject]@{ Ready = $true LogName = $ev.LogName Source = $ev.Source } } catch { $Global:EventSinkCache[$desiredKey] = [pscustomobject]@{ Ready = $false LogName = $EventLog Source = $EventSource Error = $_.Exception.Message } Write-Host "[Warning] Failed to initialize Event Log '$EventLog' / source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow } } $sink = $Global:EventSinkCache[$desiredKey] if ($sink.Ready) { try { $EventMessage = "TaskCategory: $TaskCategory | Message: $Message" Write-EventLog -LogName $sink.LogName -Source $sink.Source -EntryType $EntryType -EventId $EventID -Message $EventMessage } catch { Write-Host "[Warning] Failed to write to Event Log '$($sink.LogName)' / source '$($sink.Source)': $($_.Exception.Message)" -ForegroundColor Yellow } } else { Write-Host "[Warning] Event Log not initialized for '$EventLog' / '$EventSource'. Skipping Event Log write." -ForegroundColor Yellow } } if ($PassThru) { return $logEntry } } #endregion Public: Write-LogHelper #region Public: Write-LogHybrid function global:Write-LogHybrid { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Message, [ValidateSet("Info", "Warning", "Error", "Success", "General")] [string]$Level = "Info", [string]$TaskCategory = "GeneralTask", [switch]$LogToEvent, [string]$EventSource = "SVSMSP_Module", [string]$EventLog = "SVSMSP Events", [ValidateSet('Repair', 'Unique', 'Follow')] [string]$EventLogConflictPolicy = 'Repair', [int]$CustomEventID, [string]$LogFile, [switch]$PassThru, [ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")] [string]$ForegroundColorOverride ) $formatted = "[$Level] [$TaskCategory] $Message" $invokeParams = @{ Message = $Message Level = $Level TaskCategory = $TaskCategory LogToEvent = $LogToEvent EventSource = $EventSource EventLog = $EventLog EventLogConflictPolicy = $EventLogConflictPolicy } if ($PSBoundParameters.ContainsKey('CustomEventID')) { $invokeParams.CustomEventID = $CustomEventID } if ($PSBoundParameters.ContainsKey('LogFile')) { $invokeParams.LogFile = $LogFile } if ($PassThru) { $invokeParams.PassThru = $true } if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) { Write-Host $formatted -ForegroundColor $ForegroundColorOverride if (Get-Command Write-Log -ErrorAction SilentlyContinue) { Write-Log @invokeParams } else { Write-LogHelper @invokeParams } } else { if (Get-Command Write-Log -ErrorAction SilentlyContinue) { Write-Log @invokeParams } else { Write-LogHelper @invokeParams } } } #endregion Public: Write-LogHybrid