<# .SYNOPSIS G-Log search for NinjaOne: the web server log lines of the last minutes that match a query, in the script output. With -AlertOnMatch, exit code 1 when there are any. .DESCRIPTION Runs glog.com --search on a log file or folder and prints the matching lines with their file and line number, such as every server error on the payment pages in the last hour. Files last written before that period are skipped. Query terms, all of which have to match: status:5xx or status:500,503; url:/api/* (a prefix, or * as wildcard); url:"/my files/*" for a value with spaces; client:10.1.2.3 or ip:10.1.0.0/16; user:; method:POST; host:; ua:curl; ref:; query:; sub:; win32:; time>2s; time<100ms; any other word or quoted text anywhere in the line; a - in front excludes a term. .PARAMETER Query What to look for, such as "status:5xx url:/pay/*". Required. .PARAMETER LogPath A log file or folder. Default: the IIS log folder of this machine, all sites. .PARAMETER SinceMinutes How many minutes back to read. Default 60. .PARAMETER Limit At most this many lines in the output. Default 50. .PARAMETER AlertOnMatch End with exit code 1 when any line matches, so NinjaOne can raise an alert on it. .PARAMETER GLog Full path to glog.com. Default: the standard installation folder. .PARAMETER TimeoutSeconds Give up after this long. Default 600. .NOTES G-Log by Garia.Net - https://garia.net/ Run as System, so every log folder can be read. Windows PowerShell 5.1 or later. Exit codes: 0 done (with -AlertOnMatch: nothing matched), 1 lines matched with -AlertOnMatch, or the logs could not be read, 2 glog.com not found or wrong input. #> [CmdletBinding()] param( [string]$Query = '', [string]$LogPath = "$env:SystemDrive\inetpub\logs\LogFiles", [int]$SinceMinutes = 60, [int]$Limit = 50, [switch]$AlertOnMatch, [string]$GLog = '', [int]$TimeoutSeconds = 600 ) $ErrorActionPreference = 'Stop' # NinjaOne script variables arrive as environment variables with the same name. if ($env:query) { $Query = $env:query } if ($env:logPath) { $LogPath = $env:logPath } if ($env:sinceMinutes) { $SinceMinutes = [int]$env:sinceMinutes } if ($env:limit) { $Limit = [int]$env:limit } if ($env:alertOnMatch) { $AlertOnMatch = $env:alertOnMatch -match '^(1|true|yes)$' } if ($env:glog) { $GLog = $env:glog } if ($env:timeoutSeconds) { $TimeoutSeconds = [int]$env:timeoutSeconds } function Find-GLog([string]$Given) { if ($Given) { return $Given } $pf = $env:ProgramW6432 if (-not $pf) { $pf = $env:ProgramFiles } Join-Path $pf 'GariaNetTools\G-Log\glog.com' } # Windows command-line quoting: a quote gets a backslash, and backslashes right before a quote # (or before the closing quote) are doubled, or "C:\" would swallow the quote after it. function Format-Argument([string]$Value) { $escaped = [regex]::Replace($Value, '(\\*)"', { param($m) ($m.Groups[1].Value * 2) + '\"' }) '"' + ($escaped -replace '(\\+)$', '$1$1') + '"' } # glog.com with both output streams read as UTF-8, whatever the console code page is. function Invoke-GLog([string]$Exe, [string[]]$Arguments, [int]$Timeout) { $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Exe $psi.Arguments = ($Arguments | ForEach-Object { Format-Argument $_ }) -join ' ' $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 $p = [System.Diagnostics.Process]::Start($psi) $out = $p.StandardOutput.ReadToEndAsync() $err = $p.StandardError.ReadToEndAsync() if (-not $p.WaitForExit($Timeout * 1000)) { try { $p.Kill() } catch { } return [pscustomobject]@{ ExitCode = -1; Output = ''; Error = "no result within $Timeout seconds" } } $p.WaitForExit() [pscustomobject]@{ ExitCode = $p.ExitCode; Output = $out.Result; Error = $err.Result.Trim() } } # ------------------------------------------------------------------------------ input --- if (-not $Query) { Write-Output 'Query is required, for example: -Query "status:5xx url:/api/*"' exit 2 } if ($SinceMinutes -lt 1 -or $Limit -lt 1) { Write-Output 'SinceMinutes and Limit must be 1 or more.' exit 2 } $exe = Find-GLog $GLog if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { Write-Output "glog.com not found at $exe. Install G-Log 0.7.0 or later, or pass -GLog." exit 2 } # ----------------------------------------------------------------------------- search --- $run = Invoke-GLog $exe @('--search', $LogPath, $Query, '--since', "$($SinceMinutes)m", '--limit', "$Limit", '--out', '-') $TimeoutSeconds if ($run.ExitCode -eq 2) { Write-Output "G-Log did not accept the query: $($run.Error)" exit 2 } if ($run.ExitCode -ne 0) { Write-Output "G-Log could not read $LogPath (exit code $($run.ExitCode)): $($run.Error)" exit 1 } # Standard error ends with "G-Log: 50 of 1108 matching lines, in 0.79 s". $total = 0 if ($run.Error -match 'of (\d+) matching lines') { $total = [int64]$Matches[1] } $lines = @($run.Output -split "`r?`n" | Where-Object { $_ }) if ($total -eq 0) { Write-Output "No lines match '$Query' in the last $SinceMinutes minutes on $env:COMPUTERNAME." exit 0 } $shown = '' if ($total -gt $lines.Count) { $shown = " (the first $($lines.Count) below)" } Write-Output "$total lines match '$Query' in the last $SinceMinutes minutes on $env:COMPUTERNAME$shown." Write-Output '' foreach ($l in $lines) { Write-Output $l } if ($AlertOnMatch) { exit 1 } exit 0