# The G-Log report and MCP tools G-Log has one report and several ways in: `glog --report` writes it as JSON or as a page, and `glog --mcp` answers questions from that same report. A number is therefore the same whichever way you ask for it. This document describes the JSON (schema version 1) and the MCP tools. The HTTP API is below; MCP over HTTP comes with the cluster in a later version. --- ## Conventions | Kind of field | Rule | Example | |---|---|---| | names ending in `Share` | a fraction from 0 to 1, never a percentage | `"errorShare5xx": 0.002911` | | names ending in `Ms` | milliseconds | `"afterP95Ms": 292` | | times | ISO 8601 in UTC, to the minute or second | `"from": "2026-09-12T00:00:00Z"` | | `change` | a fraction: `(after - before) / before`; `null` when there is no before | `"change": 0.4747` | | estimates | marked with `estimate` or `approximate`, or `Estimate` in the name | `"uniqueClientsEstimate": 57134` | | text | none: codes and values only; the words live in the page, in five languages | `"reason": "needsDays"` | Unique clients come from HyperLogLog (within about 1%); percentiles from histograms. **Masking.** By default every value is written as it is in the log. With `--mask`, or `MaskSensitive = 1` in `HKLM\Software\Garia.Net\G-Log` (set by the installers), the values of these fields are masked everywhere, advice included, and `options.masked` is `true`: | Field | Masked as | |---|---| | `client`, `topClient` | IPv4 without the last part (`10.20.1.x`), IPv6 after the first three groups (`2001:db8:646c::x`) | | `user` | a fixed label per name (`user-3fa2c1`) | | `secretValue` | `********` | | `cardNumber` | the first six and last four digits (`411111******1111`) | **Compatibility.** New analyses and fields are added without changing `schemaVersion`. When an existing field changes meaning or shape, `schemaVersion` goes up. Ignore fields you do not know. --- ## The report ```powershell glog --report C:\inetpub\logs\LogFiles\W3SVC1 --out report.json ``` | Field | Content | |---|---| | `schemaVersion` | `1` | | `tool` | `{"name":"G-Log","version":"0.3.0"}` | | `generatedAt` | when the report was made, UTC | | `options` | `{"top":25,"apdexThresholdMs":500,"endpointLimit":1000,"masked":false}`; with `--since`, `--from` or `--to` also `window` with `from` and `to` (UTC) | | `scan` | `{"seconds":0.03,"threads":5,"kernel":"avx512","bytesRead":56781840,"reruns":0}` | | `sources` | what was read: `path`, `name`, `bytes`, `format` (`w3c`, `common`, `combined`, `vhost_combined`, or `mixed` when a folder holds several), `folder`, `files` | | `summary` | `{"severity":"critical","somethingWrong":true,"requests":72482,"critical":1,"warning":3,"info":4}` | | `chapters` | eleven chapters, each `{"id", "number", "analyses": {...}}` | | `advice` | the findings, most severe first | | `skipped` | every analysis that could not be done, with its reason | | `linesOnAgent` | only when a G-Log summary was read: the analyses (`chapter.analysis`) whose log lines stayed on the machine that made it (see *Summaries*) | | `lineSchemas` | the column names of the log lines in the report, per schema id (see *Log lines*) | In `inventory.files.items`, a log inside a zip archive has the archive in its name (`logs-2024.zip\W3SVC1\u_ex240205.log`). `bytes` is the size on disk, packed for `.gz` and zip, and `unpackedBytes` the log itself. A file that could not be read has `status: "notRead"` and a `reason`: `cannotRead`, `notW3c`, `notHttpErr`, `lineTooLong`, `gzipTruncated`, `gzipInvalid`, `zipInvalid`, `zipEncrypted`, `zipMethod`, `zipTruncated`, `zipDamaged` or `outsideWindow`. `notW3c` keeps its name from before 0.6.0 and means: not a web server log G-Log reads (W3C, Apache or nginx). `outsideWindow`: the file was last written more than an hour before the start of the time window, so it was not read; the newest file is always read. ### Summaries `glog --summary` and `/api/summary` write an analysis as a file: every count, table and time series of the report, without the log lines. `--report`, `--compare`, `/api/report` and the MCP tools read such a file as if the logs it was made from had been read there, and the numbers come out the same. It is recognised by its first bytes (`GLOGSUM`), whatever it is called; in a folder, files ending in `.glogsum` are read as well. Several summaries, or logs and summaries together, add up into one report. `--summary` also reads summaries, so the days of several machines can be packed into one file. In `inventory.files.items` a summary has `kind: "summary"` and `machine`; its `pattern` and `rollover` are those of the log it was made from, and `unpackedBytes` is the size of those logs. A summary that cannot be used has `reason` `summaryInvalid` (not complete, or damaged) or `summaryVersion` (made by a G-Log with another summary format). Chapter 0 then has `inventory.summaries`: `count`, `machines`, and per summary `machine`, `source` (the path on that machine), `file`, `madeAt`, `logLines`, and `from` and `to` when it was made with a time window. The top-level `linesOnAgent` lists every analysis that would have shown log lines; the page puts a note under those. With a time window, only the lines inside it count everywhere in the report, the period is the part of the window that has lines, and `inventory.lines.outsideWindow` counts the lines that were left out. The HTTP.sys error log is read in full. `--compare` reads the earlier period in full as well. ### CSV and text `glog --report ... --format csv` writes one table: the advice by default (with `title`, `body` and `do` in English), `--table skipped`, or `--table chapter.analysis` for the rows of an analysis (`errors.statusCodes`, `performance.slowestPages`), `--table chapter.analysis.list` for another list in it (`errors.perUrl.serverErrors`), and one row for an analysis without a list (`overview.keyNumbers`). `--tables` prints every name with its number of rows. Columns are the field names as in the JSON, nested fields with a dot (`responseMs.p95`); lists of log lines are left out. A text value starting with `=`, `+`, `-` or `@` gets an apostrophe in front; a file gets a UTF-8 byte order mark. The separator is a comma; `--csv-sep ;` or `--csv-sep tab` writes what Excel expects in a language that uses the comma as the decimal mark (the same option works for `glog --search --format csv`). The CSV button in the page follows the language of the page: a comma for English, a semicolon for the other four. `--format text` is a summary in plain English: source, period, window, requests, error shares, response time and each finding with what to do. `--with-text` keeps the JSON and adds `title`, `body` and `do` to every advice. ### Chapters and analyses | # | `id` | Analyses | |---|---|---| | 0 | `inventory` | files, summaries (only when a summary was read), period, lines, format, fields, missingFields, otherColumns, counts, sources, cannotTell | | 1 | `overview` | keyNumbers, fiveQuestions, availability, apdex, errorBudget, previousPeriod, serverEvents | | 2 | `traffic` | overTime, weekPattern, peak, concurrency, perNode, methods, ports, hosts, protocolVersion, growthForecast | | 3 | `performance` | overTime, histogram, slowestPages, timeHogs, slowByClient, perServer, throughput, regressions, underLoad | | 4 | `errors` | overTime, statusCodes, win32, perUrl, newErrors, brokenLinks, concentration, httpSysRejections, droppedConnections | | 5 | `clients` | uniqueOverTime, topClients, sessions, browsers, bots, network, users, realClients, countries | | 6 | `applications` | sites, applications, endpoints, newEndpoints, unusedEndpoints, queryParameters, fileTypes, topDocuments, cacheEffect, largest | | 7 | `origin` | referrers, searchEngines, landingPages, navigation, campaigns, brokenInboundLinks | | 8 | `bandwidth` | overTime, consumers, compression | | 9 | `security` | scanners, loginAttacks, attackPatterns, sensitiveFiles, suspiciousAgents, unusualMethods, highRate, cardNumbers, secrets, adminPaths | | 10 | `logging` | periodAndGaps, clock, fields, schemaChanges, badLines, volume, timezoneRollover | Read an analysis as `chapters[id].analyses[key]`, for example the key numbers: ```json {"status":{"1xx":0,"2xx":62615,"3xx":7512,"4xx":2144,"5xx":211},"requests":72482, "errorShare4xx":0.02958,"errorShare5xx":0.002911,"responseMs":{"p50":28,"p95":292,"p99":808,"avg":84}, "uniqueClientsEstimate":57134,"sessionsEstimate":71988,"bytesOut":1895674224,"bytesIn":56857380} ``` Per-minute series hold 1,440 values per day under `days`. ### Percentiles that hit the last bucket Percentiles per URL, per server and per hour come from coarse buckets that run to just over 65 seconds. A percentile that lands in the last bucket is a lower bound, not a measurement, and the report says so with a boolean next to it: `"p95Ms": 122880, "p95MsCapped": true` (and `worstP95MsCapped` in `overview.availability.worstStretches`). The page writes such a value as `> 2 min`. The `maxMs` of a row is exact, so use that for the slowest request. ### Log lines Rows that are about requests carry the lines from the log itself: the first three and the last three, in file order. They appear in `overview.availability.worstStretches`, `performance.slowestPages` and `timeHogs`, `errors.statusCodes` (400 and up), `errors.perUrl`, `errors.concentration.clients`, `clients.topClients` (their error lines), `clients.users`, `security.scanners`, `loginAttacks`, `attackPatterns.urls`, `sensitiveFiles`, `cardNumbers` and `secrets`. ```json {"url":"/product/{id}","requests":277755,"errors5xx":1108, "linesSeen":1108, "lines":[{"file":"W3SVC1\\u_ex260910.log","line":1930,"schema":"412a91ed51e3f7af", "text":"2026-09-10 00:16:41 10.20.1.12 GET /product/1321 - 443 - 244.27.95.227 Mozilla/5.0+... - 500 0 0 31518 578 1257"}]} ``` | Field | Content | |---|---| | `linesSeen` | how many lines there were behind this row | | `lines[].file` | the file name; with more than one file, with its folder | | `lines[].line` | the line number in that file, counting every line from 1 (in a `.gz` or zip: in the unpacked log) | | `lines[].schema` | the key into `lineSchemas`: the column names, in order. A W3C line splits on spaces. An Apache or nginx line has names such as `remote_addr`, `time_local`, `request` and `http_user_agent`: there a field between quotes (with `\"` or `\x22` inside) or between brackets is one field | | `lines[].text` | the line as it is in the log; with masking on, with the sensitive fields masked | ### Whether an analysis could be done Every analysis has a `status`: | `status` | Meaning | Extra fields | |---|---|---| | `ok` | done in full | | | `partial` | done, with limits | `notes`: codes such as `durationAndDepthNotTracked` | | `skipped` | not possible with this input | `reason`, and where it fits `field`, `minDays`, `haveDays` | ```json {"status":"skipped","reason":"missingField","field":"X-Forwarded-For"} {"status":"skipped","reason":"needsDays","minDays":7,"haveDays":1} {"status":"partial","estimate":71988,"method":"clientAgent30min","approximate":true,"notes":["durationAndDepthNotTracked"]} ``` | `reason` | Meaning | |---|---| | `missingField` | the log does not contain `field`; chapter 0 says how to turn it on | | `fieldEmpty` | `field` is logged but has no values | | `needsTwoPeriods` | needs `--compare` | | `needsDays` | needs `minDays` days of logs; the report covers `haveDays` | | `needsHttperr` | needs the HTTP.sys error log | | `needsGeoIp` | needs a GeoIP database | | `needsFolder` | needs a folder of logs rather than one file | | `needsMultipleSources` | needs logs from more than one server | | `singleNode` | the log comes from a single server | | `noValidLines` | the log has no valid lines | | `baselineWithoutTimeTaken` | the comparison period has no `time-taken` | ### Advice ```json {"code":"pci.cardNumbers","severity":"critical","chapter":"security","analysis":"cardNumbers", "values":{"lines":3,"urls":1},"pci":{"v4":"3.5.1","v321":"3.4"}} ``` `severity` is `critical`, `warning` or `info`. `chapter` and `analysis` point at the evidence. `values` holds the numbers the text is built from. `pci` appears only on findings that touch PCI DSS. Some findings take their severity from the values, so read it from each item. | Code | English title in the page | |---|---| | `overview.badMinutes` | {badMinutes} bad minutes, the longest stretch {longestMinutes} minutes | | `errors.serverErrorRate` | {share} of requests got a server error | | `errors.brokenLinks` | {links} links lead to a 404 | | `errors.concentratedClient` | {client} causes {share} of all errors | | `errors.failingUrl` | {url} gives a server error on {share} of requests | | `errors.gatewayTimeout` | {requests} requests ended in 504 Gateway Timeout | | `errors.httpSysServiceUnavailable` | HTTP.sys refused {requests} requests with a 503 before IIS saw them | | `errors.appPoolAbandoned` | A worker process disappeared with requests still open: {requests} times | | `performance.p95High` | 95 out of 100 requests take up to {p95Ms} | | `performance.urlDominatesTime` | {url} takes {share} of all server time | | `performance.slowByClientNetwork` | {share} of slow requests are slow because of the visitor | | `performance.longRequests` | {urls} URLs hold requests open for minutes, the longest {longestMs} | | `performance.slowerOverTime` | The server got slower during the day while traffic stayed the same | | `clients.gaveUp` | {requests} visitors gave up after {avgMs} on average | | `errors.oneAgent` | {share} of the server errors come from one caller | | `performance.serverSlower` | {server} is slower than the rest: p95 {p95Ms} against {medianP95Ms} | | `traffic.unbalancedNodes` | {server} handles {share} of the traffic across {nodes} servers | | `traffic.plainHttp` | {requests} requests still arrive over plain HTTP | | `applications.lowCacheRevalidation` | Static files are hardly ever answered with 304 | | `bandwidth.compressionCandidates` | Large text files that may be sent uncompressed: {urls} | | `clients.loadBalancerWithoutXff` | {client} sends {share} of all requests | | `security.attackPatterns` | {lines} requests contain attack patterns | | `security.sensitiveServed` | Sensitive files were served: {requests} times | | `security.sensitiveProbes` | {requests} requests for sensitive files, none served | | `security.scanners` | Scanning for weak spots, most from {topClient} | | `security.loginAttack` | Login attack, most failed sign-ins from {topClient} | | `security.secretsInQuery` | Passwords or tokens in the query string: {lines} requests | | `security.highRate` | Addresses sending more than 600 requests a minute: {clients} | | `security.scannerAgents` | Known attack tools in the user agent: {requests} requests | | `security.unusualMethods` | {requests} requests with an unusual method | | `security.httpSysParseRejections` | HTTP.sys rejected {requests} malformed requests | | `pci.cardNumbers` | Card numbers in URLs: {lines} requests | | `logging.missingTimeTaken` | The log has no time-taken field | | `logging.missingResponseTime` | The log has no response time (Apache and nginx; `values.format` names the format) | | `logging.missingFields` | Useful fields are not logged: {fields} | | `logging.badLines` | {lines} lines could not be read | | `logging.outOfOrder` | {lines} lines are more than a minute out of order | | `logging.gaps` | {gaps} gaps without any traffic, the longest {longestMinutes} minutes | | `change.serverErrorsUp` | Server errors went from {beforeShare} to {afterShare} | | `change.slower` | Responses got slower: p95 from {beforeP95Ms} to {afterP95Ms} | | `change.traffic` | Traffic per minute changed by {trafficShareChange} | | `change.regressedUrl` | {url} got slower: average from {beforeAvgMs} to {afterAvgMs} | | `change.newServerErrors` | {requests} server errors with a status that did not occur before | ### Two periods With `--compare`, the analyses that were skipped with `needsTwoPeriods` are filled in. Periods are seldom the same length, so they are compared per active minute and as shares. `overview.previousPeriod`: ```json {"status":"ok", "current":{"path":"...\\u_ex260912_slow_cart.log","from":"2026-09-12T00:00:00Z","to":"2026-09-12T23:59:59Z","requests":72482,"activeMinutes":1440}, "baseline":{"path":"...\\u_ex260912.log","from":"2026-09-12T00:00:00Z","to":"2026-09-12T23:59:59Z","requests":72482,"activeMinutes":1440}, "metrics":[{"metric":"requestsPerMinute","before":50.33,"after":50.33,"change":0}, {"metric":"p95Ms","before":198,"after":292,"change":0.4747}]} ``` `performance.regressions` compares URLs with at least `minRequests` requests in *both* periods. A URL counts as slower when its p95 is 1.5 times as high and at least 100 ms more, or its average 1.5 times as high and at least 50 ms more. The list is sorted by extra server time (`extraMs`: extra average time × requests). ```json {"status":"ok","minRequests":100,"compared":23,"regressed":1, "rule":{"minRatio":1.5,"minExtraP95Ms":100,"minExtraAvgMs":50}, "items":[{"url":"/api/cart","beforeRequests":6674,"afterRequests":6674,"beforeAvgMs":62,"afterAvgMs":311, "beforeP95Ms":176,"afterP95Ms":832,"extraMs":1661826}]} ``` | Analysis | Content | |---|---| | `errors.newErrors` | `statusCodes`: status, substatus and win32 codes of 400 and up that did not occur before (at least 5 times); `urls`: URLs with server errors now and none before | | `applications.newEndpoints` | endpoints with at least 10 requests that did not occur before, with `firstSeen` | | `applications.unusedEndpoints` | endpoints with at least 10 requests before and none now, with `lastSeen` | | `overview.fiveQuestions.whatChanged` | error share and p95 before and after, the change in traffic, the slowest-grown URL (`slowerUrl`), the URL with the most extra server errors (`url`), the number of new endpoints | | `clients.newVsReturning` | new and returning clients, estimated from the two HyperLogLogs | --- ## HTTP API ``` glog --server [--port N] [--threads N] [--mask] glog --show-key ``` `--server` listens on **7788** on every address, or the port in `HKLM\Software\Garia.Net\G-Log\ServerPort`. The port never moves: a firewall rule points at it, so a port already in use is an error on standard error, not a quiet move to another number. The installers can register this as the Windows service **GLogServer**, which starts `glog.exe --service`. **TLS.** A server or an agent that listens outside its own machine speaks **https**, with Schannel of Windows and a self-signed certificate of that machine (see [Agents and the server](#agents-and-the-server)). The page on the loopback (`glog --open`) stays plain HTTP. `--no-tls` runs without, for a trial on one machine. **The key.** Every `/api/` request, and `/metrics`, carries the key of that machine, in the header `X-GLog-Token` or as `?token=`. `glog --show-key` prints it; it lives in `%ProgramData%\GariaNet\G-Log\keys.json`, a file only SYSTEM and Administrators can read, and is created on first use. `/api/hello` answers without a key, so a script can check which port it reached, and names the fingerprint of the certificate. **The page needs the key as well** on a server: `/?site=...&token=` once, after which a cookie (`glog-token`, HttpOnly, SameSite=Strict) keeps the browsing working. Without it the page answers 401 `badToken`. | Route | Gives back | |---|---| | `GET /` | the report page; with `?path=` the report of that source is in the page. On a server: `?token=` once (see above), and the overview refreshes every minute | | `GET /api/hello` | `{"tool":"G-Log","version":"0.17.0","auth":true,"role":"server","tls":true,"thumbprint":"83E5..."}` - no key needed | | `GET /api/report` | the whole report, the same JSON as `glog --report --format json` | | `GET /api/advice` | the findings, with the sources and the summary | | `GET /api/overview` | chapter 1, with the five questions | | `GET /api/inventory` | chapter 0: what is in the log, and what it therefore cannot tell | | `GET /api/search` | the log lines that match `query`, as `glog --search --format json` | | `GET /api/explain` | what `code` means and what to check, as `glog --explain --format json`; `kind` optional (see [`glog_explain`](#glog_explain)) | | `GET /api/summary` | the analysis as a summary file (`application/octet-stream`), as `glog --summary`; the headers `X-GLog-Summary-Lines` and `X-GLog-Summary-Machine` say how many lines and from which machine. Read it back with `glog --report` | | `GET /metrics` | Prometheus text: `glog_requests_total`, `glog_findings_critical`, `glog_findings_warning`, `glog_findings_info`, `glog_something_wrong` | **Parameters.** `path` is one file or folder; `paths` is several, separated by `;`, read as one source (a site's log folder plus the HTTPERR folder). `compare` and `comparePaths` are the earlier period. `since` (`15m`, `2h`, `7d`), or `from` and `to` (UTC), limit the lines that count. `/api/search` takes `query`, `limit` (1-500, default 50) and `skip`. `/api/explain` takes `code` and optionally `kind`; a wrong `kind` is `badCode`. **Errors** are JSON with a code, so a caller can act on it: `pathRequired`, `notFound`, `noLogsInFolder`, `notALog`, `cannotRead`, `badQuery`, `badCode`, `badWindow`, `badLimit`, `badToken`, `noSuchRoute`, `internalError`. A code prefixed with `compare.` is about the earlier period. HTTP status is 400 for a wrong request, 401 without a valid key, 404 for a path or route that is not there, 500 for a fault in G-Log itself. The API and the MCP server share one report cache: the last four sources stay in memory, and a source is read again as soon as its number of files, total size or latest change differs. An assistant and a dashboard asking the same question therefore read the log once. ## Agents and the server ``` glog --agent [--port N] [--push https://server:7788 --push-key KEY] [--agent-path ...] [--no-iis] [--interval 1m] [--keep-days 7] [--cpu 10] [--store ] [--machine NAME] [--once] [--no-tls] glog --server [--port N] [--pull web01:7788=KEY[#FINGERPRINT] ...] [--interval 1m] [--keep-days 7] [--store ] [--no-tls] ``` An **agent** runs on a web server, as the service **GLogAgent** (`glog.exe --service --agent`). Every round it reads the sites from `applicationHost.config`, the HTTP.sys error folder and any `--agent-path` folders, and for each log file of the last `--keep-days` days that changed since its summary was made, it makes that summary again: a finished day is read once. Summaries go to `%ProgramData%\GariaNet\G-Log\store\\.glogsum` and older ones are removed. It reads with one core at below-normal priority, and its process is held to `--cpu` percent of all processors (a job object; default 10, `0` for no limit). The **server** collects those summaries in `%ProgramData%\GariaNet\G-Log\cluster\\\` in one of two ways, per agent: | | How | On the web server | |---|---|---| | **fetch** | the server has the agent in `--pull` (or `Agents` in the registry) and asks every interval what is new | 7788 inbound, for the server only | | **send** | the agent has `--push` and `--push-key` (or `AgentServerUrl` and `AgentServerKey`) and posts new summaries to `/api/ingest` | nothing inbound | Without options both read their settings from `HKLM\Software\Garia.Net\G-Log`: `AgentServerUrl`, `AgentServerKey`, `AgentInterval`, `AgentKeepDays`, `AgentCpuPercent`, `AgentPaths` (multi-string) for the agent; `Agents` (multi-string, one `web01:7788 KEY` per line), `PullInterval` and `ClusterKeepDays` for the server. `Tls` (0) turns the encryption off, as `--no-tls` does. Keys given in the registry move to `keys.json` at the first start. `--once` runs one agent round, prints the status and stops. ### Pairing: an agent without a key typed over ```cmd glog --pair-key [--note "web03"] on the server: a key of 20 characters, one hour, one use glog --agent --push https://server:7788 --pair ``` The agent posts `{machine, thumbprint}` to `POST /api/pair` with the pairing key in the header `X-GLog-Pair`. That route is the only one that does not need the key of the server: the pairing key *is* the key, once. The answer is `{key, machine, thumbprint}` - a key of its own for that agent, and the fingerprint of the server so the agent can pin it. Both sides write it in their `keys.json`, and the pairing key is removed. A key of an agent is accepted only on `/api/ingest`, `/api/agent/heartbeat` and `/api/agent/context`; everything else needs the key of the server (`--show-key`). So one web server never knows the keys of the others, and a stolen agent key cannot read the cluster. A wrong or expired pairing key gives 401 `badPairingKey` and a line on the server's standard error. ### Encrypted, and who is who Agent and server speak TLS (Schannel; no OpenSSL, no extra DLL). At its first start each of them makes a **self-signed certificate** for itself - RSA 2048, five years, `CN=`, friendly name *G-Log* - in the certificate store of the machine (`LocalMachine\MY`), or of the user when there are no administrator rights. A management network has no certificate authority that signs for a web server, so a chain says nothing here. The **fingerprint** does: - the server learns the fingerprint of an agent at its first contact and writes it in `keys.json`; from then on another certificate means the round fails with `thumbprintMismatch` and nothing is read; - an agent that sends does the same with the certificate of its server; - you can give it beforehand: `--pull web01:7788=KEY#83E5AC98...` (a colon or space in the fingerprint is fine); - `/api/hello` names it, and so does the line G-Log prints at its first contact, so you can compare it with what the other machine reports. Before a key or a summary goes over the line, the fingerprint is checked on a request that carries nothing (`/api/hello`); only then does the real request follow, over the same connection. **Keys are not in the registry.** `%ProgramData%\GariaNet\G-Log\keys.json` holds the key of this machine, the key of the server for an agent that sends, and per agent its address, key and fingerprint. The file has an access list of SYSTEM and Administrators only, so a local user of a web server cannot read the keys of the cluster. What was in `HKLM\Software\Garia.Net\G-Log` (`Token`, `AgentServerKey`, `Agents`) moves there at the first start and is deleted from the registry; if that fails for lack of rights, G-Log says so and keeps using the registry. For tests, `GLOG_KEYS` names another file. A site is joined across machines **by its name in IIS**, not by its `W3SVC` number, which can differ per machine. **Routes on an agent** (with the agent's own key): | Route | Gives back | |---|---| | `GET /api/agent/status` | `role`, `version`, `machine`, `time` and `timeUnix` (for the clock difference), `mode` (`pull` or `push`), the last round (`built`, `upToDate`, `errors`, `seconds`), every site with its folder and `files`, and for sending `push` with `lastOk`, `pending` and `error` | | `GET /api/agent/summaries` | every kept summary: `site`, `file`, `bytes`, `modified` (read up to, UTC seconds), `madeAt`, `lines` | | `GET /api/agent/summary?site=&file=` | the bytes of one of those; nothing outside that list | | `GET /api/report?site=` and the other report routes, `/api/search?site=` | as on any G-Log, with `site` naming a site the agent knows | **Routes on a server** (with the server's key): | Route | Gives back | |---|---| | `POST /api/ingest?modified=` | stores a summary sent by an agent; the body may be up to 1 GB, but only with the right key (without it the limit stays 4 MB and the answer is `413 bodyTooLarge`). `GET` answers `405`, which an agent uses to check its key first | | `GET /api/agents` | per agent `address`, `machine`, `mode`, `version`, `lastContact`, `silent` (nothing for three rounds), `clockSkewSeconds`, `summariesReceived`, `error` (`badToken`, `cannotConnect`, `http404`, ...) | | `GET /api/cluster/sites` | per site `name`, `machines`, `summaries`, `bytes`, `lines`, `readUpTo` | | `GET /api/report?site=Shop[&machine=WEB01]` | the report of that site over all machines, or one; also `advice`, `overview`, `inventory`, `summary`, `metrics` | | `GET /api/search?site=Shop&query=...` | the question is passed to every agent the server fetches from that has the site; `machines` says per machine how many lines matched or why it could not answer (`agentNotReachable` for an agent that only sends), and every line carries its `machine` | A site the server has no summaries of is `404 noSuchSite`. **A site over several machines.** When the summaries come from more than one machine and the log has no `s-computername`, each machine becomes one server in the report, named after the machine: `traffic.perNode` then has `serverName: "machine"` and the note `serverIsMachine`. The server passes the clock difference it measured for each machine to the report: `logging.clock.skewBetweenServers` lists `machine` and `skewSeconds` (positive: that clock is ahead), and a difference of 60 seconds or more is the finding `logging.clockSkew` (warning, PCI DSS 10.6.1 / 3.2.1 10.4). An agent that sends reports its clock with a heartbeat (`POST /api/agent/heartbeat`) every round. ## Alerts The server evaluates its alert rules after every round (`--interval`, default a minute) over the summaries of the last two days. A rule fires only after it has held for its minimum number of rounds, and is resolved only after it has been false for its recovery margin, so nothing flickers. | Rule | Fires when | Min. / recovery (rounds) | Event ID | |---|---|---|---| | `node.errorRate` | one machine has at least 5% server errors in the last 10 minutes and at least 3 times (+2 points) the share of the other machines in the site; both with 50 requests or more | 2 / 5 | 1001 | | `logging.stopped` | a machine has written no request for 15 minutes or more, while the hour before averaged 2 per minute or more, and its agent does answer | 1 / 2 | 1002 | | `agent.silent` | the server has not heard an agent for three of its rounds (at least 15 s): fetching failed, or no heartbeat | 1 / 1 | 1003 | | `httperr.queue` | HTTP.sys refused 10 or more requests in the last 10 minutes: 503 from the queue (`QueueFull`, `AppPoolTimer`, ...) or abandoned by the app pool | 1 / 10 | 1004 | | `security.attackWave` | 100 or more requests with attack patterns since the previous round | 1 / 30 | 1005 | | `pci.cardNumber` | a new request with a card number in the URL (critical) | 1 / 60 | 1006 | | `apppool.failure` | a worker process of an app pool crashed, ended unexpectedly or stopped responding in the last 15 minutes; critical when rapid-fail protection stopped the app pool | 1 / 2 | 1007 | | `certificate.expiring` | the certificate of an HTTPS binding expires within 30 days (critical within 7, or expired), or is not in the certificate store | 1 / 1 | 1008 | | `disk.logVolume` | a log volume has less than 10% or 5 GB free (critical under 3%) | 1 / 2 | 1009 | | `performance.p95Regression` | the p95 of a site in the last hour with traffic is at least 2 times and 500 ms above the median of the same clock hour on the previous days (critical: 4 times and 2 s); needs at least 5 earlier days with 100 timed requests in that hour | 3 / 5 | 1010 | A resolved alert has event ID + 100 (1101-1110). An alert that gets worse while it is firing (a certificate going from warning to critical) is announced again. `GET /api/alerts` gives `firing` and every alert that is firing or was resolved in the last 24 hours: `id` (`rule|site|subject`), `rule`, `severity`, `state` (`firing`, `resolved`), `site`, `subject` (the machine, or the agent's address for `agent.silent`), `text` (why, with the numbers), `link` (the page for that site, at that analysis), `eventId`, `since`, `resolvedAt`, and `values` with the numbers themselves. The state is kept in `cluster\alerts.json`, so a restart does not fire the same alert again. `learning` lists the sites whose p95 baseline has fewer than 5 days (`daysWithData`, `daysNeeded`). The alerts read the summaries of the last 8 days. Where alerts go, besides `/api/alerts`: | Destination | Setting | What arrives | |---|---|---| | Windows event log, *Application*, source **G-Log** | on by default; `AlertEventLog` = 0 or `--no-eventlog` turns it off | warning or error when firing, information when resolved, with the event IDs above; the installers register the source when the server is chosen | | webhook | `AlertWebhook` or `--webhook https://...` | `POST` JSON: `{"tool":"G-Log","event":"firing" or "resolved","text":"Warning: ...","alert":{...}}`; the `text` field is what Teams and Slack show | | syslog | `AlertSyslog` or `--syslog host:514` | UDP, RFC 5424 with a CEF message: `CEF:0\|Garia.Net\|G-Log\|\|\|\|\|cs1Label=site cs1= dvchost= msg=` | | Prometheus | `GET /metrics` on the server without a path | `glog_alerts_firing{severity="critical"}` and `{severity="warning"}` | **In the page.** On the server, `/` is an overview: the alerts (firing, and resolved in the last day), the sites with a link to their report, and the agents with their last contact and clock difference; it refreshes every minute. The report of a site (`/?site=`) shows the alerts of that site above the summary. The page carries a second data block, `glog-server`, with `alerts`, `sites`, `agents` and `site`, and writes each alert in the language of the page from its rule and values. ## Server context With an agent, G-Log also knows what happened on the web server itself (PLAN §10). Every round the agent collects: | What | From | |---|---| | worker process events: `rapidFail` (5002), `workerTerminated` (5009), `workerNotResponding` (5010, 5011, 5013), `recycle` (5074, 5076-5080, 5117) | the System log, source WAS, with the app pool | | crashes of `w3wp`: `appError` (Application Error 1000, with the faulting module and exception code), `dotnetUnhandled` (.NET Runtime 1026, with the exception) | the Application log | | deploys: a changed `web.config`, `appsettings*.json` or `bin\*.dll` in the folder of a site, with the files | the physical paths in `applicationHost.config` | | certificates: every SSL binding of HTTP.sys (IP and SNI) with the subject and expiry of its certificate, and the sites that use it | HTTP.sys and the LocalMachine certificate store | | per minute: processor (`cpuPermille`), HTTP.sys queue length (`queue`) and rejected requests (`rejected`); free space of the log volumes | performance counters (by their English names, so a Dutch or German Windows works too) | | per minute per app pool (`pools`): processor (`cpuPermille`) and private memory (`memoryMb`) of its worker processes | the `w3wp.exe` processes, with the app pool from `-ap` in their command line | One JSON file per day (UTC), `store\_context\context-YYYYMMDD.json` on the agent and `cluster\\_context\` on the server, kept 14 days. `GET /api/agent/context` lists the days with their `modified`; `?day=YYYY-MM-DD` gives one. The server fetches changed days, and an agent that sends posts them to `POST /api/agent/context`. `--no-context` turns the collection off. In the report of a site on the server, `overview.serverEvents` lists the `events` of the app pools of that site and crashes without an app pool, the `deploys` of that site, and the `certificates` of its bindings (`expires`, `daysLeft`, `error: notInStore`), with `failures`, `rapidFail` and `deployCount`. Events and deploys are those of the report's period, with an hour on either side. Without an agent the analysis is skipped with `reason: needsAgent`. Findings: `server.appPoolFailures` (warning, critical with rapid-fail), `server.certificateExpiring` (30 days or less; critical within 7), `server.certificateExpired` and `server.certificateMissing` (critical). `workers` gives per app pool of the site `avgCpuShare`, `peakCpuShare` with `peakAt`, and `peakMemoryMb` in the period. The page marks events and deploys on the time lines of traffic, response time and errors per server (a dashed line with a triangle, listed in the tooltip), and without a second period *What changed?* names the last deploy or failure up to shortly after *Since when?*. For tests without IIS: `GLOG_TEST_EVENTS` names a file with one event XML per line instead of the event log, and `GLOG_TEST_CERT_BINDINGS` gives bindings as `host:port=thumbprint@CurrentUser;...` instead of HTTP.sys, and `GLOG_TEST_WORKER_PROCESS` names another program that counts as a worker process when its command line has `-ap`. ## Live following An agent reads a plain log file up to its last complete line and remembers where it stopped (`.tail`, with a fingerprint of the first 4 KB). The next round reads only what was added and adds it to the summary; a line IIS is still writing waits for the next round. A file that became shorter or starts differently (rotated, replaced) is read again from the start; `.gz`, zip and HTTP.sys logs are always read whole. Measured on a 1 GB log: 1.41 s for the first round, 0.11 s for a round with 10,000 new lines. With `--interval 15s` the agent is about as current as IIS writes its buffer (up to a minute). ## Logs of a Linux server, over SSH A source may be `ssh://user@host[:port]/path` - for `--report`, `--compare`, `--search`, `--summary` and `--agent-path`. G-Log runs the `ssh.exe` from `System32\OpenSSH` (never one from the PATH), lists the logs with `find`, and fetches them into `%LOCALAPPDATA%\G-Log\ssh\_`, or into `\_ssh` for an agent. After that they are read as a local folder. | | | |---|---| | before a file is fetched | of a file G-Log does not have yet, the first 4 KB come over first; only if those begin like a web server log does the rest follow, so a folder of application or system logs costs kilobytes instead of megabytes. `.gz` and `.zip` cannot be judged that way and are always fetched | | what goes over the line | only what was added: a grown log is fetched from where the last round stopped (`tail -c +N`); a shorter or replaced log (logrotate) is fetched whole again | | signing in | with a key (`--ssh-key `), and then `BatchMode` so a service can never wait on a question. Does that server take passwords only? `--ssh-password` asks for one, `--ssh-save-password ` keeps it in `keys.json` for a service, `--ssh-passwords` lists which targets have one. `--ssh-known-hosts ` keeps the host keys somewhere else than in the profile | | the host key | `StrictHostKeyChecking=accept-new`: a server that was not known yet is remembered, a changed key is refused - the same rule as the fingerprint between agent and server | | rights on that server | read access to the log files; on Debian and Ubuntu the group `adm` | | what it costs there | one `find` and one `tail` or `cat` per changed file, per round | | for tests | `GLOG_SSH_CACHE` names another folder for the fetched files | Everything after that is the same as for a local folder: the same report, the same search, and the same summaries if an agent reads it, so a Linux web server takes part in a site across machines. ## Trying it out `api/G-Log.postman_collection.json` holds an example of every call below, grouped the way you would use them, and is installed next to this file under `api`. Import it in Postman, set `baseUrl` and `key`, and start at **hello**. A server on the network uses HTTPS with its own certificate, which Postman will not trust: turn off *SSL certificate verification*, or pin the fingerprint that `/api/hello` gives you. ## What a source says about itself `sources[0]` holds `path`, `name`, `bytes` (on disk, so compressed for `.gz` and zip), `unpackedBytes` when that differs, `format` and how many files were read. `bytes` counts everything that was read, including HTTP.sys error logs beside a site's own logs. ## Log formats | | | |---|---| | IIS W3C, Kestrel W3C | the `#Fields` line, also when it changes halfway | | HTTP.sys error logs | `httperr*.log` | | Apache, nginx | common, combined, vhost_combined, with a response time behind it | | nginx in JSON | recognised by itself; fields by their nginx name, in any order | | your own format | `--log-format ''`, or the path of the configuration plus `--log-format-name` | `--log-format` sets the format for everything that run reads, and works with `--report`, `--search`, `--agent`, `--server` and `--mcp`. nginx `$variables` and Apache `%directives` are both understood; a variable G-Log does not know is still read and kept as a column, so it stays visible in the line. ## PCI DSS: the daily review | Route / command | | |---|---| | `glog --review [--day ] [--site ]` | the review of a day as JSON; makes it if it is not there yet | | `glog --reviews` | the last thirty days with the retention figures | | `glog --sign-off "" [--day] [--note-text]` | sign a day off | | `glog --finding --status [--sign-off] [--note-text]` | open, investigated, accepted or resolved | | `GET /api/reviews?days=N` | the same list, with `retention` | | `GET /api/review?day=&site=` | one day: findings, status, history, sign-offs | | `POST /api/review?day=&site=` | `{"who":"...","note":"..."}` signs the day off; with `{"finding":"","status":"..."}` it moves one finding | Reviews are one JSON file per day in `%ProgramData%\GariaNet\G-Log\reviews\` (`GLOG_REVIEWS` names another folder, for tests). The server writes the review of yesterday by itself, once that day has passed. `POST /api/review` is the only POST the page's `glog-token` cookie may do; everything else that sends data needs the key in `X-GLog-Token`. ## PCI DSS: integrity and the audit trail | Route / command | | |---|---| | `glog --integrity [--store ]` | the register of sealed log files as JSON; exit code 1 when something changed or went missing | | `glog --audit [N]` | the last N lines of the audit trail (200 by default) | | `GET /api/audit?limit=N` | the same lines over the API, newest first | | `GET /api/integrity` | the same register over the API: per file its path, site, size, SHA-256, when it was sealed and last checked, and its state | The register lives next to the summaries (`\integrity.json`, or `GLOG_INTEGRITY`) and holds `path`, `site`, `bytes`, `sha256`, `sealedAt`, `checkedAt` and `state` (`ok`, `changed` or `missing`) per file. A file is sealed once it has been quiet for two hours; the file of today keeps growing and is left alone. The audit trail is JSON per line in `%ProgramData%\GariaNet\G-Log\audit.log` (or `GLOG_AUDIT`), appended only: `at`, `peer`, `who`, `what` (page, report, search, export, reportFile, settings, audit, mcp), `target`, `detail` and `status`. `who` is the key that was used - `server key`, `page`, `agent `, `unknown key` - never the key itself and never a person. `/api/settings` says whether the trail is on and where it is. ## NinjaOne over the API A server with a NinjaOne connection writes the numbers of every machine it collects to the custom fields of the matching device, every `intervalMinutes` (60 by default). The connection is set with `glog --ninja-setup` and lives in `keys.json`; the client secret is never shown again, not on the page and not in `/api/settings`, which only says which NinjaOne address is used. | What G-Log calls | | |---|---| | `POST /ws/oauth/token` | client credentials, scope `monitoring management`; the token is kept until it expires | | `GET /v2/devices` | to match a machine name to a device (systemName, dnsName, hostname or displayName, without the domain) | | `PATCH /v2/device//custom-fields` | the fields `glogSummary`, `glogRequests`, `glogServerErrorPercent`, `glogP95Ms`, `glogFindings`, `glogSites` and `glogReport` | Nothing else is called. Alerts and tickets stay on the NinjaOne side, as a condition on a field or on the Windows event G-Log writes: the API can neither raise an alert nor open a ticket without a user context. ## The page of the server | Route | | |---|---| | `GET /api/reports` | the schedules with `lastRun` and `nextRun`, and per schedule `files` (name, bytes, time), newest first | | `GET /api/reports/file?name=&file=` | one made report, as the file it is (html, json, csv, text). Only a file of that schedule; `..` or a path separator is refused | | `GET /api/settings` | version, role, machine, port, TLS and its thumbprint, masking, where the key file and the data folder are, the round, how long summaries are kept, and where the schedules stand. No keys | These are the three the page uses for its views Search, Reports and Settings. A **GET** on `/api/` also accepts the `glog-token` cookie that the page received when it was opened with `?token=`, so the page can ask its own server without putting the key in every address. The cookie is `HttpOnly` and `SameSite=Strict`; sending something in (POST) still needs `X-GLog-Token`. ## Reports on a schedule The service makes reports by itself, from `%ProgramData%\GariaNet\G-Log\reports.json` (`GLOG_REPORTS` names another file, for tests). It is read every round, so a change takes effect within a minute; a file that cannot be read is reported once on standard error and stops nothing else. | Field | | |---|---| | `name` | required; also the file name, `-20260918.html` | | `site` or `path` / `paths` | the source: a site of this cluster over all its machines, or a folder, file or `ssh://` address | | `every` | `day`, `week` (with `on`: monday…sunday), `hour`, or a length of time such as `15m` | | `at` | for `day` and `week`: the time in UTC, such as `06:00` | | `period` | what is in the report: `yesterday` (default), `today`, `lastWeek`, `24h`, `7d`, `30d` | | `format` | `html` (default), `json`, `csv` (with `table`), `text` | | `out` | folder or share; default `%ProgramData%\GariaNet\G-Log\reports` | | `keep` | how many files of that name stay, 30 by default | `glog --reports` prints the schedules as JSON with `lastRun` and `nextRun`; `glog --report-now ` makes one now and prints the path. The last run of each schedule is kept in `reports-state.json`, so a restart neither skips nor repeats one, and a schedule that was just added waits for its first real moment. ## MCP ```powershell glog --mcp [--allow-write] [--threads N] on this machine, over standard input and output POST /mcp?token= on a server, over HTTP (since 0.20.0) ``` **Over HTTP.** A server (`glog --server`) offers the same tools at `POST /mcp`: one JSON-RPC message in the body, one answer back, with the key of that machine in `X-GLog-Token` or `?token=`. A notification (a message without `id`) answers 202 with an empty body. There is no event stream and no session: every tool of G-Log is a question and an answer. A request that carries an `Origin` header from another site is refused with 403 `badOrigin`, so a page in a browser cannot use it on someone's behalf. On a server every tool also takes **`site`** instead of `path`: the name of a site in IIS, over all the machines that report it, read from the summaries the agents sent. `/api/cluster/sites` lists them. A site the server does not collect comes back as a tool error that says so. Writing is off over HTTP, so `glog_report` with `format: "html"` is not available there. Newline-delimited JSON-RPC over standard input and output; messages to standard error. Both protocol generations work: **2026-07-28** (no handshake, `_meta` with protocol version and client capabilities on each request, `server/discover`) and the handshake with `initialize` for **2025-11-25**, **2025-06-18**, **2025-03-26** and **2024-11-05**. An unsupported version gets error `-32022` with the supported list. Every tool returns `structuredContent` and the same JSON as text. A problem with the input (a path that does not exist, a folder without logs, a missing argument) is a tool result with `isError: true` and a sentence the assistant can act on, not a protocol error. `glog_explain` needs no path. Every tool is read-only except `glog_report` with `format: "html"`. **Paths.** Each tool takes `path` (a file or folder) and optionally `paths` (more of them, read together as one source). Reports are kept in memory for the last four sources; a source is read again when its number of files, total size or latest change time differs. **Source.** Every result starts with `source`: `path`, `bytes`, `from`, `to` and `"timezone":"UTC"`. ### `glog_overview` Is something wrong, where, since when, and who notices. `since` is the stretch of bad minutes that is still running at the end of the log; when nothing is running any more, it is the longest stretch - the same one `overview.availability.worstStretches` puts first, with `durationMinutes` and `ongoing` next to it. It is not the most recent bad minute. Input: `path`, `paths`. Output: `summary`, `overview` (the analyses of chapter 1) and `urgentAdvice` (everything except `info`, with text). ### `glog_advice` Input: `path`, `paths`, `minSeverity` (`info`, `warning`, `critical`; default `info`). Output: `summary` and `advice`. Each item is the report's advice item plus `title`, `body` and `do` in English, with the values filled in. ### `glog_top` Input: `path`, `paths`, `list`, `limit` (1-100, default 10). | `list` | From | |---|---| | `slowestPages`, `timeHogs` | `performance.slowestPages`, `performance.timeHogs` | | `serverErrorUrls`, `clientErrorUrls`, `statusCodes`, `brokenLinks` | chapter `errors` | | `clients`, `subnets`, `bots` | chapter `clients` | | `referrers`, `landingPages` | chapter `origin` | | `endpoints`, `fileTypes` | chapter `applications` | | `attackUrls`, `attackClients`, `scanners`, `secrets`, `cardNumberUrls` | chapter `security` | Output: `list`, `analysis` (where the rows come from), `status`, `items` and `notes`. When the analysis was skipped: `status: "skipped"` with `reason`. ### `glog_timeseries` Input: `path`, `paths`, `metric`, `bucketMinutes` (1-1440). | `metric` | `unit` | `aggregation` | |---|---|---| | `requests`, `errors4xx`, `errors5xx` | requests | `sumPerBucket` | | `bytesOut`, `bytesIn` | bytes | `sumPerBucket` | | `p50Ms`, `p95Ms`, `p99Ms` | ms | `worstMinuteInBucket` | Without `bucketMinutes` the bucket is chosen from 1, 5, 10, 15, 30, 60 up to 1440 minutes so there are at most 96 points, from the first to the last minute with traffic. Output: `points` as `[bucket start UTC, value]`. ### `glog_inventory` Input: `path`, `paths`. Output: `inventory` (chapter 0: files, period, lines, fields, what is missing and what that rules out) and `skippedAnalyses`. ### `glog_what_changed` A period against an earlier one. Input: `path`, `paths` (the period to judge), `compare`, `comparePaths` (the earlier period). `path` and `compare` are required. Output: `previousPeriod`, `whatChanged`, `regressions`, `newErrors`, `newEndpoints`, `unusedEndpoints` and `changeAdvice` (the `change.*` findings with text). An error about the earlier period starts with `compare: `. ### `glog_search` The log lines themselves that match a query. Not from the report cache: a search reads only what it needs. Input: `path`, `paths`, `query` (required), `limit` (1-500, default 50), `skip` (default 0). Output: `query`, `total` (all matching lines), `skipped`, `returned`, `nextSkip` (when there are more), `timezone` and `lines`, each with `file`, `line`, `fields` (every column by name) and `text`. The query language is the one of `glog --search`: `status:5xx`, `status>=500`, `status<400`, `url:/api/*`, `client:10.1.2.3`, `ip:10.1.0.0/16`, `user:`, `method:`, `host:`, `ua:`, `ref:`, `query:`, `sub:`, `win32:`, `time>2s`, `time<100ms`, `from:07:00`, `to:07:20` (or with a date), free text, `url:"/with space"` for a value with spaces, and `-` to exclude. `ua:`, `ref:` and `query:` match part of the value, or the whole value when the term holds a `*` (`ua:*python-requests*`). A query that cannot be read is a tool error that says why. ### `glog_explain` What a code from a web server log means and what to check. Needs no log file. Input: `code` (required) and `kind` (`status`, `win32` or `httperr`, when a number could be both). The code is read as it stands in a log: | Code | Read as | |---|---| | `404`, `404.13`, `HTTP 503.0`, `status:500.19` | an HTTP status, with the IIS substatus when there is one: the substatus and the status each get a match | | `64`, `10054`, `win32:500` | a Win32 code from `sc-win32-status`. A three-digit number is a status unless only a Win32 explanation exists (121, 123) | | `0x80070005`, `2147942405`, `-2147024891` | an HRESULT; one of Win32 (`0x8007xxxx`) is explained as its Win32 code, with `fromHresult` | | `Timer_MinBytesPerSecond`, `httperr:QueueFull` | a reason from an HTTP.sys error log, without regard to case | Output: `query` and `matches`, each with `kind` (`substatus`, `status`, `statusClass`, `win32` or `httperr`), `code`, `meaning` and, where there is something to do, `do`. A status gives `status` and `subStatus`; a Win32 code `win32`, `hex`, `windowsMessage` (the text of Windows itself) and `source` (`glog`, or `windows` when only Windows knows the code); an HTTP.sys reason `category` and `categoryText`. A status without its own text (`418`) is explained by its class. A substatus G-Log does not know gives `unknownSubStatus` next to the status. No match at all: `matches` is empty and `hint` says what can be explained. G-Log explains 45 status codes, 103 IIS substatus codes (including the ASP.NET Core Module codes 500.30-500.38 and 502.5, URL Rewrite 500.50-500.53, request filtering 404.5-404.20 and dynamic IP restrictions 403.501-403.504), 26 Win32 codes and security HRESULTs, and the reasons of HTTP.sys. The texts are in `ui/i18n.js` (`x.status.*`, `x.sub.*`, `x.win32.*`, `httperr.*`) in the five languages of the page, which shows them as a tooltip on the codes in its tables; the API, the command line and MCP use English. ```powershell glog --explain 500.19 # text for a terminal; exit code 1 when there is no explanation glog --explain 64 --format json # the same JSON as glog_explain ``` ### `glog_report` Input: `path`, `paths`, `chapter`, `includeSeries`, `format`, `table`, `out`, `since`, `from`, `to`. - `chapter`: `contents` (default: summary and the chapters with their analyses), one chapter id, `advice`, or `all`. - Series of more than 48 values are replaced by `"omitted: 1440 values, use glog_timeseries or includeSeries"`, unless `includeSeries` is `true`. - `format: "html"` with `out`: writes the full page to `out`. Only when the server was started with `--allow-write`, only to a path ending in `.html` or `.htm`, and never over an existing file. - `format: "csv"` with `table` (`advice` by default, `skipped`, or `chapter.analysis`): `csv`, the table exactly as `glog --format csv` writes it to standard output. `format: "text"`: `text`, the plain-English summary. **A time window on every report tool.** `glog_overview`, `glog_advice`, `glog_top`, `glog_timeseries`, `glog_inventory` and `glog_report` take `since` (`15m`, `2h`, `7d`), or `from` and `to` (UTC, such as `2026-09-15T07:00`, a date, or `07:00` for today): only the lines in that window count, as with `glog --since`. The cache keeps a report per window.