<# .SYNOPSIS G-Log web server report for NinjaOne: read the logs of the last hours and fill custom fields with the key numbers, the findings and an HTML report. .DESCRIPTION Runs glog.com without a browser and reads the JSON it writes to standard output. Fills up to six NinjaOne custom fields: glogSummary Text one line: requests, server errors, p95, findings glogRequests Integer requests in the period glogServerErrorPercent Decimal share of requests with a server error (5xx), in percent glogP95Ms Integer 95 out of 100 requests take up to this many ms glogFindings Text the findings, most severe first glogReport WYSIWYG key numbers, findings with what to do, slowest pages and URLs with server errors, as tables Outside NinjaOne the same values are printed instead, so the script can be tried in an ordinary PowerShell window first. Nothing is written to disk. .PARAMETER LogPath A log file or folder. Default: the IIS log folder of this machine, all sites. .PARAMETER SinceHours How many hours back to read. Default 24. .PARAMETER GLog Full path to glog.com. Default: the standard installation folder. .PARAMETER SummaryField Name of the text field for the summary. Empty skips it. The same goes for RequestsField, ServerErrorField, P95Field, FindingsField and ReportField. .PARAMETER TopRows How many rows the tables in the report have. Default 10. .PARAMETER TimeoutSeconds Give up after this long. Default 1800. .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 fields filled, 1 the logs could not be read, 2 glog.com not found or wrong input. #> [CmdletBinding()] param( [string]$LogPath = "$env:SystemDrive\inetpub\logs\LogFiles", [int]$SinceHours = 24, [string]$GLog = '', [string]$SummaryField = 'glogSummary', [string]$RequestsField = 'glogRequests', [string]$ServerErrorField = 'glogServerErrorPercent', [string]$P95Field = 'glogP95Ms', [string]$FindingsField = 'glogFindings', [string]$ReportField = 'glogReport', [int]$TopRows = 10, [int]$TimeoutSeconds = 1800 ) $ErrorActionPreference = 'Stop' # NinjaOne script variables arrive as environment variables with the same name. if ($env:logPath) { $LogPath = $env:logPath } if ($env:sinceHours) { $SinceHours = [int]$env:sinceHours } if ($env:glog) { $GLog = $env:glog } if ($env:topRows) { $TopRows = [int]$env:topRows } if ($env:timeoutSeconds) { $TimeoutSeconds = [int]$env:timeoutSeconds } $inv = [System.Globalization.CultureInfo]::InvariantCulture 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() } } function ConvertTo-Html([string]$Text) { [System.Net.WebUtility]::HtmlEncode($Text) } function Set-NinjaField([string]$Name, [string]$Value) { if (-not $Name) { return } try { if (Get-Command 'Ninja-Property-Set-Piped' -ErrorAction SilentlyContinue) { $Value | Ninja-Property-Set-Piped $Name } elseif (Get-Command 'Ninja-Property-Set' -ErrorAction SilentlyContinue) { Ninja-Property-Set $Name $Value } elseif ($Value.Length -gt 300) { Write-Output ('[{0}] ({1} characters)' -f $Name, $Value.Length) } else { Write-Output ('[{0}] {1}' -f $Name, $Value) } } catch { Write-Warning ('Could not fill custom field {0}: {1}' -f $Name, $_.Exception.Message) } } # An analysis of the report by chapter and name, or $null when it was skipped. function Get-Analysis($Report, [string]$Chapter, [string]$Name) { $c = @($Report.chapters | Where-Object { $_.id -eq $Chapter }) | Select-Object -First 1 if (-not $c) { return $null } $a = $c.analyses.$Name if (-not $a -or $a.status -eq 'skipped') { return $null } $a } function Add-Table($Sb, [string]$Title, [string[]]$Headers, $Rows) { if (@($Rows).Count -eq 0) { return } [void]$Sb.Append('

' + (ConvertTo-Html $Title) + '

') foreach ($h in $Headers) { [void]$Sb.Append('') } [void]$Sb.Append('') foreach ($r in $Rows) { [void]$Sb.Append('') foreach ($cell in $r) { [void]$Sb.Append('') } [void]$Sb.Append('') } [void]$Sb.Append('
' + (ConvertTo-Html $h) + '
' + (ConvertTo-Html ([string]$cell)) + '
') } # ---------------------------------------------------------------------------- analyse --- if ($SinceHours -lt 1) { Write-Output "SinceHours must be 1 or more, not $SinceHours." 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 } $run = Invoke-GLog $exe @('--report', $LogPath, '--since', "$($SinceHours)h", '--format', 'json', '--with-text', '--out', '-') $TimeoutSeconds if ($run.ExitCode -ne 0) { Write-Output "G-Log could not read $LogPath (exit code $($run.ExitCode)): $($run.Error)" if ($run.ExitCode -eq 2) { exit 2 } exit 1 } try { $r = $run.Output | ConvertFrom-Json } catch { Write-Output "The G-Log result could not be read: $($_.Exception.Message)" exit 1 } # --------------------------------------------------------------------------- figures --- $requests = [int64]$r.summary.requests $kn = Get-Analysis $r 'overview' 'keyNumbers' $share5xx = 0.0 $p95 = $null if ($kn) { $share5xx = [double]$kn.errorShare5xx * 100 if ($kn.responseMs -and $null -ne $kn.responseMs.p95) { $p95 = [int64]$kn.responseMs.p95 } } $advice = @($r.advice) $counts = @() foreach ($sev in 'critical', 'warning', 'info') { $n = @($advice | Where-Object { $_.severity -eq $sev }).Count if ($n -gt 0) { $counts += "$n $sev" } } $findingText = 'no findings' if ($counts.Count -gt 0) { $findingText = $counts -join ', ' } $parts = @('{0} requests in the last {1} h' -f $requests, $SinceHours) $parts += [string]::Format($inv, '{0:0.00}% server errors', $share5xx) if ($null -ne $p95) { $parts += 'p95 {0} ms' -f $p95 } $parts += $findingText $summary = '{0}: {1}. Read {2}.' -f $env:COMPUTERNAME, ($parts -join ', '), (Get-Date -Format 'yyyy-MM-dd HH:mm') $findings = @($advice | ForEach-Object { $title = [string]$_.title if (-not $title) { $title = [string]$_.code } '{0}: {1}' -f $_.severity.ToUpperInvariant(), $title }) # ---------------------------------------------------------------------------- report --- $sb = New-Object System.Text.StringBuilder [void]$sb.Append('

' + (ConvertTo-Html $summary) + '

') $period = Get-Analysis $r 'inventory' 'period' $numbers = @(, @('Requests', $requests)) $numbers += , @('Server errors', [string]::Format($inv, '{0:0.00}%', $share5xx)) if ($kn) { $numbers += , @('Client errors', [string]::Format($inv, '{0:0.00}%', [double]$kn.errorShare4xx * 100)) } if ($null -ne $p95) { $numbers += , @('Response time p50 / p95', ('{0} ms / {1} ms' -f $kn.responseMs.p50, $p95)) } if ($period) { $numbers += , @('Period (UTC)', ('{0} - {1}' -f $period.from, $period.to)) } Add-Table $sb 'Key numbers' @('', '') $numbers Add-Table $sb 'Findings' @('Severity', 'Finding', 'What to do') @($advice | ForEach-Object { , @($_.severity, [string]$_.title, [string]$_.do) }) $slow = Get-Analysis $r 'performance' 'slowestPages' if ($slow) { Add-Table $sb 'Slowest pages' @('URL', 'Requests', 'p95 ms', 'Server errors') @($slow.items | Select-Object -First $TopRows | ForEach-Object { , @($_.url, $_.requests, $_.p95Ms, $_.errors5xx) }) } $perUrl = Get-Analysis $r 'errors' 'perUrl' if ($perUrl -and $perUrl.serverErrors) { Add-Table $sb 'URLs with server errors' @('URL', 'Server errors', 'Requests') @($perUrl.serverErrors | Select-Object -First $TopRows | ForEach-Object { , @($_.url, $_.errors5xx, $_.requests) }) } $html = $sb.ToString() if ($html.Length -gt 200000) { $html = '

' + (ConvertTo-Html $summary) + '

The full report is too large for this field.

' } # ---------------------------------------------------------------------------- fields --- Set-NinjaField $SummaryField $summary Set-NinjaField $RequestsField ([string]$requests) Set-NinjaField $ServerErrorField ([string]::Format($inv, '{0:0.00}', $share5xx)) if ($null -ne $p95) { Set-NinjaField $P95Field ([string]$p95) } Set-NinjaField $FindingsField ($findings -join '; ') Set-NinjaField $ReportField $html Write-Output $summary foreach ($f in $findings) { Write-Output " $f" } exit 0