Private/Invoke-InfraPulseEventLogCheck.ps1
|
function Invoke-InfraPulseEventLogCheck { [CmdletBinding()] param( [Parameter(Mandatory)] [psobject]$Context, [Parameter(Mandatory)] [System.Collections.IDictionary]$Settings ) $logs = @($Settings.Logs) if ($logs.Count -eq 0) { return New-InfraPulseResult -Status 'Skipped' -CheckName 'EventLog' -Category 'Reliability' -ComputerName $Context.ComputerName -Target 'Windows event logs' -Message 'No Windows event logs are configured.' -Recommendation 'Add log names under Checks.EventLog.Logs.' } $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $scriptBlock = { param($CheckSettings) if ($env:OS -ne 'Windows_NT') { throw 'The EventLog check requires a Windows target.' } $startTime = (Get-Date).AddHours(-[double]$CheckSettings.LookbackHours) foreach ($logName in @($CheckSettings.Logs)) { try { $null = Get-WinEvent -ListLog $logName -ErrorAction Stop } catch { [pscustomobject]@{ LogName = [string]$logName Exists = $false QuerySucceeded = $false Count = 0 RetrievedCount = 0 Truncated = $false TopProviders = @() Samples = @() Error = $_.Exception.Message } continue } $filter = @{ LogName = [string]$logName StartTime = $startTime } if (@($CheckSettings.Levels).Count -gt 0) { $filter.Level = @($CheckSettings.Levels) } try { # One record beyond the cap distinguishes an exactly-full result # set from a truncated one. $queriedEvents = @(Get-WinEvent -FilterHashtable $filter -MaxEvents ([int]$CheckSettings.MaxEvents + 1) -ErrorAction Stop) } catch { if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') { $queriedEvents = @() } else { [pscustomobject]@{ LogName = [string]$logName Exists = $true QuerySucceeded = $false Count = 0 RetrievedCount = 0 Truncated = $false TopProviders = @() Samples = @() Error = $_.Exception.Message } continue } } $truncated = $queriedEvents.Count -gt [int]$CheckSettings.MaxEvents if ($truncated) { $queriedEvents = @($queriedEvents | Select-Object -First ([int]$CheckSettings.MaxEvents)) } $retrievedCount = $queriedEvents.Count $events = @($queriedEvents) if (@($CheckSettings.ExcludeProviders).Count -gt 0) { $events = @($events | Where-Object { $_.ProviderName -notin @($CheckSettings.ExcludeProviders) }) } if (@($CheckSettings.ExcludeEventIds).Count -gt 0) { $events = @($events | Where-Object { $_.Id -notin @($CheckSettings.ExcludeEventIds) }) } $topProviders = @( $events | Group-Object -Property ProviderName | Sort-Object -Property Count -Descending | Select-Object -First 5 | ForEach-Object { [pscustomobject]@{ Provider = if ([string]::IsNullOrWhiteSpace([string]$_.Name)) { '<unknown>' } else { [string]$_.Name } Count = [int]$_.Count } } ) $samples = @() foreach ($eventRecord in @($events | Select-Object -First 5)) { $message = $null if ([bool]$CheckSettings.IncludeMessages) { try { $message = ([string]$eventRecord.Message -replace '[\r\n]+', ' ').Trim() if ($message.Length -gt 300) { $message = $message.Substring(0, 300) + '...' } } catch { $message = $null } } # LevelDisplayName is localized; the invariant name keeps the # machine-readable exports comparable across target locales. $levelName = switch ([int]$eventRecord.Level) { 1 { 'Critical' } 2 { 'Error' } 3 { 'Warning' } 4 { 'Information' } 5 { 'Verbose' } default { [string]$eventRecord.Level } } $samples += [pscustomobject]@{ TimeCreated = $eventRecord.TimeCreated Id = [int]$eventRecord.Id Level = $levelName ProviderName = [string]$eventRecord.ProviderName Message = $message } } [pscustomobject]@{ LogName = [string]$logName Exists = $true QuerySucceeded = $true Count = $events.Count RetrievedCount = $retrievedCount Truncated = $truncated TopProviders = $topProviders Samples = $samples Error = $null } } } $raw = @(Invoke-InfraPulseCommand -Context $Context -ScriptBlock $scriptBlock -ArgumentList @($Settings)) $stopwatch.Stop() $results = @() foreach ($log in $raw) { $querySucceeded = $true if ($null -ne $log.PSObject.Properties['QuerySucceeded']) { $querySucceeded = [bool]$log.QuerySucceeded } if (-not [bool]$log.Exists) { $status = 'Unknown' $message = "Windows event log '$($log.LogName)' is unavailable." $recommendation = 'Validate the configured log name and the account permission to read that log.' } elseif (-not $querySucceeded) { $status = 'Unknown' $message = "Windows event log '$($log.LogName)' could not be queried." $recommendation = 'Validate event-log read permissions and inspect the query error captured in the result evidence.' } elseif ([int]$log.Count -ge [int]$Settings.CriticalCount) { $status = 'Critical' # A truncated result set is a lower bound, not an exact count. $message = if ([bool]$log.Truncated) { "At least $($log.Count) critical/error event(s) in '$($log.LogName)' during the last $($Settings.LookbackHours) hour(s) (collection capped at $($Settings.MaxEvents))." } else { "$($log.Count) critical/error event(s) in '$($log.LogName)' during the last $($Settings.LookbackHours) hour(s)." } $recommendation = 'Triage the highest-volume providers and correlate events with service impact and recent changes.' } elseif ([int]$log.Count -ge [int]$Settings.WarningCount) { $status = 'Warning' $message = if ([bool]$log.Truncated) { "At least $($log.Count) critical/error event(s) in '$($log.LogName)' during the last $($Settings.LookbackHours) hour(s) (collection capped at $($Settings.MaxEvents))." } else { "$($log.Count) critical/error event(s) in '$($log.LogName)' during the last $($Settings.LookbackHours) hour(s)." } $recommendation = 'Review recurring providers and event IDs before the error rate becomes operationally significant.' } elseif ([bool]$log.Truncated) { $status = 'Unknown' $message = "The '$($log.LogName)' query reached the configured limit of $($Settings.MaxEvents) event(s); $($log.Count) event(s) matched after exclusions." $recommendation = 'Increase Checks.EventLog.MaxEvents or narrow the lookback window before treating the event volume as healthy.' } else { $status = 'Healthy' $message = "$($log.Count) critical/error event(s) in '$($log.LogName)' during the last $($Settings.LookbackHours) hour(s)." $recommendation = '' } $retrievedCount = [int]$log.Count if ($null -ne $log.PSObject.Properties['RetrievedCount']) { $retrievedCount = [int]$log.RetrievedCount } $observedValue = if ([bool]$log.Truncated -and $status -in @('Warning', 'Critical')) { "$([int]$log.Count)+" } else { [int]$log.Count } $evidence = [ordered]@{ LogName = [string]$log.LogName Count = [int]$log.Count RetrievedCount = $retrievedCount LookbackHours = [double]$Settings.LookbackHours Levels = @($Settings.Levels) Truncated = [bool]$log.Truncated TopProviders = @($log.TopProviders) Samples = @($log.Samples) QueryError = [string]$log.Error } $results += New-InfraPulseResult -Status $status -CheckName 'EventLog' -Category 'Reliability' -ComputerName $Context.ComputerName -Target ([string]$log.LogName) -Message $message -ObservedValue $observedValue -WarningThreshold (">= {0} events" -f $Settings.WarningCount) -CriticalThreshold (">= {0} events" -f $Settings.CriticalCount) -Recommendation $recommendation -Evidence $evidence -DurationMs ($stopwatch.Elapsed.TotalMilliseconds / [math]::Max($raw.Count, 1)) -ErrorMessage ([string]$log.Error) } return $results } |