This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 19:05:55 +02:00
1 parent ddc5c6593f
commit b9d137eee1
63 files changed
+1232 -445

No files matched your search

+45 -19
View File
@@ -1,44 +1,72 @@
# Stored log explorer
RustProxy 0.26.0 adds an authenticated, read-only **Logs** workspace at `/logs`. It is a local file-backed explorer for RustProxy logging outputs and is intentionally separate from **Live Logs**.
RustProxy 0.26.1 provides an authenticated, read-only **Logs** workspace at `/logs`. It is a local file-backed explorer for RustProxy logging outputs and is intentionally separate from **Live Logs**.
## Separation from Live Logs
Live Logs uses the bounded in-memory request history and Tokio broadcast channel. It remains available even when file logging is disabled and is optimized for current request activity.
Live Logs uses the bounded in-memory request history and Tokio broadcast channel. It remains available even when file logging is disabled and is optimized for current routed request activity.
The stored-log explorer does not read that memory ring. It reads only files derived from the active RustProxy logging configuration:
- the combined structured access-log path, or JSONL files directly inside the configured per-vhost directory;
- the configured optional error-log file.
Existing files remain browsable after their writer is disabled. Disabling file logging therefore stops new persistent access/protocol records but does not remove old files.
Existing files remain browsable after their writer is disabled. Low-level `protocol_error` records are sent only through configured persistent structured-log sinks and are deliberately not inserted into Live Logs.
## Protocol and malformed-traffic records
## Vhost-aware protocol records
Ordinary completed requests use `event: "request"`. RustProxy can also persist low-level records that fail before a normal routed HTTP exchange exists. Those records use:
Ordinary completed requests use `event: "request"`. Low-level failures use `event: "protocol_error"`. When RustProxy can safely identify a configured vhost, the record is persisted through that vhost's existing sink. In per-vhost file mode an unresolved record uses `_unmatched.jsonl`.
Attribution priority is:
1. vhost already selected by normal routing;
2. HTTP `:authority`;
3. HTTP `Host`;
4. TLS/QUIC SNI;
5. no safe match -> `_unmatched`.
All hostname matching reuses the normal exact/wildcard frontend resolver and listener binding rules. A listener or local port by itself is not sufficient to select a vhost.
A representative record is:
```json
{
"event": "protocol_error",
"reason": "tls_handshake_error",
"protocol_error": "tls_handshake_error",
"error": "...",
"error_detail": "...",
"client_ip": "192.0.2.10",
"local_addr": "192.0.2.20:443",
"tls_sni": "app.example",
"transport": "tls",
"http_version": "",
"status": 0,
"outcome": "protocol_error"
"vhost": "app",
"sni": "app.example",
"tls_sni": "app.example",
"authority": null,
"matched_by": "sni",
"duration_ms": 0
}
```
Covered classes include connection-limit rejection, malformed/incomplete HTTP/1 connections, invalid/disabled h2c upgrades, HTTP/2 protocol termination, TLS ClientHello and handshake errors/timeouts, and HTTP/3/QUIC handshake, header, protocol and stream-limit failures.
`matched_by` is `router`, `authority`, `host`, `sni` or `none`. Compatibility fields from 0.26.0 remain present so existing consumers do not need an immediate migration.
These low-level records are sent only through the configured persistent structured-log sinks (file, Logstash and/or Loki). They are deliberately **not** inserted into Live Logs, so scanner noise cannot displace the ordinary bounded live request history. For `protocol_error` events, the diagnostic envelope fields shown above are always emitted even when a custom JSON field mapping is active; custom/static fields are retained, but they cannot hide or overwrite the required protocol-diagnostic fields.
SNI, authority and Host are lookup inputs only. RustProxy never derives a filename directly from client-provided identity text. Only the configured internal vhost identifier returned by the shared resolver can select a vhost sink. Invalid internal IDs fall back to `_unmatched` rather than being sanitized into a new path.
Dependency error text is secret-redacted and capped before it enters the persistent record. Raw packets, request bodies and raw malformed request bytes are not captured.
Diagnostic identity values are control-character stripped and bounded before persistence. Existing error-detail redaction and limits remain in force. Raw packets, request bodies and raw malformed request bytes are not captured.
## Browser security model
## Logs Explorer UI
The workspace supports source, case-insensitive text, event class, HTTP status class, client IP and vhost filters. Text search scans stored values, including protocol diagnostics such as `reason`, `error`, `sni` and `authority`, and refreshes with a short debounce while typing.
The table provides account-scoped selectable columns. The default view keeps the original concise columns while optional protocol columns expose transport, match source, SNI and authority. At least one column must remain visible.
The table header stays visible while the result list scrolls. Selecting a row highlights it and expands the complete JSON record inline immediately below that row; selecting it again collapses the details.
The configured-file inventory is collapsed by default so installations with many per-vhost files do not lose most of the screen to source paths. The source selector still contains the configured sources, including `_unmatched.jsonl` when present.
For structured access logs, the explorer reconstructs canonical fields from the active JSON field mapping. In particular, `client_ip` remains available to the table and filter even when the output JSON key was renamed by a custom mapping.
## Browser security and bounds
The browser never supplies a filesystem path. `/api/file-logs` returns opaque source IDs derived from the current configuration, and `/api/file-logs/query` accepts only those IDs or `all`.
@@ -46,16 +74,14 @@ The reader is bounded:
- up to 2 MiB scanned from the tail of one source;
- up to 8 MiB across one query;
- up to 20,000 lines across the complete query, shared fairly across selected sources;
- up to 20,000 lines across the complete query, shared across selected sources;
- at most 32 sources in an `all` query;
- page size at most 500 and offset at most 10,000.
If more than 32 configured files are eligible for an `all` query, the newest 32 are scanned and the response is marked truncated. A specific source can always be selected directly.
File reads run in a blocking worker rather than the async proxy runtime. Only regular files are accepted. On Unix the reader opens with `O_NOFOLLOW`; a symlink is not followed. Per-vhost enumeration also rejects a configured directory that is itself a symlink and only lists direct regular `.jsonl` children.
The endpoints are private administration routes and inherit the existing panel session/Host/Origin access controls. There is no public-proxy route to the file reader.
## Filters
The workspace supports source, case-insensitive text, event class, HTTP status class, client IP and vhost filters. Results are paged from the bounded scanned tail. Selecting a row shows the complete parsed JSON object. RAW error-log lines are wrapped as a record with `event: "error"` and a `raw` field.
The explorer is diagnostic rather than a retention system. RustProxy does not add rollover, indexing or compression; use the existing operating-system log rotation policy for retention.
The explorer is diagnostic rather than a retention system. RustProxy does not add rollover, indexing or compression; use the operating-system log rotation policy for retention.