Public/Get-SnowSyslog.ps1
|
function Get-SnowSyslogNewRow { # Returns only rows not yet seen, given the newest timestamp and the sys_ids already emitted at that timestamp. param( [AllowEmptyCollection()][object[]]$Row = @(), [string]$LastCreated, [System.Collections.Generic.HashSet[string]]$SeenIds ) foreach ($item in $Row) { $created = [string]$item.sys_created_on if ($LastCreated -and $created -lt $LastCreated) { continue } if ($LastCreated -and $created -eq $LastCreated -and $SeenIds.Contains([string]$item.sys_id)) { continue } $item } } function Write-SnowSyslogLine { param([Parameter(Mandatory)][object]$Row) $level = if ($Row.PSObject.Properties['level']) { [string]$Row.level } else { '' } $name = switch ($level) { { $_ -in '2', 'error' } { 'error' } { $_ -in '1', 'warning' } { 'warning' } { $_ -in '3', 'debug' } { 'debug' } default { 'info' } } $color = switch ($name) { 'error' { 'Red' } 'warning' { 'Yellow' } 'debug' { 'DarkGray' } default { 'Gray' } } $source = if ($Row.PSObject.Properties['source']) { [string]$Row.source } else { '' } $message = if ($Row.PSObject.Properties['message']) { [string]$Row.message } elseif ($Row.PSObject.Properties['url']) { [string]$Row.url } else { '' } Write-Host ('{0} {1,-7} {2} {3}' -f $Row.sys_created_on, $name.ToUpperInvariant(), $source, $message) -ForegroundColor $color } function Get-SnowSyslog { <# .SYNOPSIS Queries the ServiceNow system log, optionally following new entries like tail -f. .DESCRIPTION Rows are returned oldest first. A time or row limit is always applied: by default the last 15 minutes, capped at 100 rows. With -Wait the log is polled every -PollSeconds (minimum 2) and only new rows are emitted; the last seen sys_created_on plus the sys_ids seen at that timestamp prevent duplicates and gaps. Failed polls back off and retry. Press Ctrl+C to stop. Use -Pretty for coloured console output instead of objects. .EXAMPLE Get-SnowSyslog -Level error -Last 50 .EXAMPLE Get-SnowSyslog -Wait -Source 'Flow Engine' -Message timeout -PollSeconds 10 #> [CmdletBinding()] [OutputType([pscustomobject])] param( [ValidateSet('syslog', 'syslog_transaction', 'syslog_app_scope')][string]$Table = 'syslog', [ValidateSet('error', 'warning', 'info', 'debug')][string]$Level, [string]$Source, [string]$Message, [string]$User, [datetime]$Since, [ValidateRange(1, 10000)][int]$Last = 100, [switch]$Wait, [ValidateRange(2, 3600)][int]$PollSeconds = 5, [ValidateRange(0, 1000000)][int]$MaxPolls = 0, [switch]$Pretty, [string[]]$Fields, [ValidateSet('Rest', 'Sdk', 'Auto')][string]$Backend ) if ($Table -eq 'syslog_transaction' -and ($Level -or $Source)) { throw [System.ArgumentException]::new('-Level and -Source are not supported for syslog_transaction.') } $levelValues = @{ info = '0'; warning = '1'; error = '2'; debug = '3' } $terms = [System.Collections.Generic.List[string]]::new() if ($Level) { $terms.Add("level=$($levelValues[$Level])") } if ($Source) { $terms.Add("source=$(ConvertTo-SnowQueryValue $Source)") } if ($Message) { $messageField = if ($Table -eq 'syslog_transaction') { 'url' } else { 'message' } $terms.Add("${messageField}LIKE$(ConvertTo-SnowQueryValue $Message)") } if ($User) { $terms.Add("sys_created_by=$(ConvertTo-SnowQueryValue $User)") } $sinceTime = if ($PSBoundParameters.ContainsKey('Since')) { $Since.ToUniversalTime() } else { [datetime]::UtcNow.AddMinutes(-15) } $format = 'yyyy-MM-dd HH:mm:ss' $sinceText = $sinceTime.ToString($format, [Globalization.CultureInfo]::InvariantCulture) $terms.Add("sys_created_on>=$sinceText") $filter = $terms -join '^' $request = @{ Table = $Table; Fields = $Fields; Backend = $Backend } if ($Fields) { $requiredFields = @('sys_id', 'sys_created_on') if ($Pretty) { if ($Table -eq 'syslog_transaction') { $requiredFields += 'url' } else { $requiredFields += @('level', 'source', 'message') } } $request.Fields = @($Fields + $requiredFields | Select-Object -Unique) } $emit = { param($row) if ($Pretty) { Write-SnowSyslogLine -Row $row } else { if ($row -is [System.Collections.IDictionary]) { $row = [pscustomobject]$row } if ($row -is [pscustomobject]) { $row.PSTypeNames.Insert(0, "Snowplow.Record.$Table") } $row } } # Initial query: newest -Last rows within the window, then oldest first. $initial = @(Invoke-SnowBackendRequest @request -Query "$filter^ORDERBYDESCsys_created_on^ORDERBYDESCsys_id" -PageSize ([Math]::Min($Last, 1000)) -First $Last) [array]::Reverse($initial) $lastCreated = $sinceText $seen = [System.Collections.Generic.HashSet[string]]::new() foreach ($row in $initial) { $created = [string]$row.sys_created_on if ($created -gt $lastCreated) { $lastCreated = $created; $seen.Clear() } if ($created -eq $lastCreated) { [void]$seen.Add([string]$row.sys_id) } & $emit $row } if (-not $Wait) { return } Write-Warning "Following $Table every $PollSeconds second(s). Each poll is a request against the instance; press Ctrl+C to stop." $failures = 0 $polls = 0 try { while ($true) { $delay = if ($failures) { [Math]::Min($PollSeconds * [Math]::Pow(2, $failures), 300) } else { $PollSeconds } Start-SnowSleep -Seconds $delay $polls++ $pollFilter = (@($terms | Where-Object { $_ -notlike 'sys_created_on>=*' }) + "sys_created_on>=$lastCreated") -join '^' try { $rows = @(Invoke-SnowBackendRequest @request -Query "$pollFilter^ORDERBYsys_created_on^ORDERBYsys_id" -PageSize 1000) $failures = 0 } catch { $failures++ if ($failures -ge 5) { throw } Write-Warning "Syslog poll failed ($($_.Exception.Message)); backing off." $rows = @() } foreach ($row in @(Get-SnowSyslogNewRow -Row $rows -LastCreated $lastCreated -SeenIds $seen)) { $created = [string]$row.sys_created_on if ($created -gt $lastCreated) { $lastCreated = $created; $seen.Clear() } [void]$seen.Add([string]$row.sys_id) & $emit $row } if ($MaxPolls -and $polls -ge $MaxPolls) { break } } } finally { Write-Verbose 'Syslog follow stopped.' } } |