Repository navigation
Access log and JSON logging mode for Kibana - #31
Conversation
x2s3 had no access log of its own, so request timings existed only in uvicorn's text output, where they could not be aggregated. Add a pure ASGI AccessLogMiddleware and an optional JSON output format so latency can be charted in Kibana. Setting log_format: json (or X2S3_LOG_FORMAT=json) writes one Elastic Common Schema object per line to stdout, which a shipper can parse straight off the container's logs. Text remains the default and stays human-readable. The middleware is pure ASGI rather than BaseHTTPMiddleware because x2s3 mostly streams large objects: the app coroutine only returns once the last body chunk is sent, so event.duration is real transfer time rather than time-to-first-byte, and the response bytes can be counted on the way past. Uvicorn's own access log is disabled wherever the middleware is installed, so requests are logged once, now with the duration and the target that served them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
html.escape() only accepts strings, so any None reaching an error template raised AttributeError and turned a handled upstream error into an unhandled 500 with a traceback. Seen in production listing a bucket whose backend answered with an empty <Resource/>, which botocore parses to None. Guard it once in the templates, where every caller routes through: the same crash was reachable from list_objects_v2 forwarding its optional prefix as the key, and from HTTPException.detail, which is not required to be a string. Also restore the Resource fallback in handle_s3_exception. dict.get's default is skipped when the key is present but null, which is exactly the case here, so the error named no resource at all instead of the key it failed on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@claude Run a security review on this PR |
Code reviewTwo issues. Checked for bugs and CLAUDE.md compliance (no CLAUDE.md files in this repo). 1. 2. Merging this PR deletes The diff removes the 22-line It looks inadvertent rather than intended. The branch history shows the file was created and then removed here while experimenting with review automation:
Meanwhile the equivalent If keeping it was the intent, restoring the file on this branch before merge is enough. |
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
x2s3 had no access log of its own, so request timings only ever existed in uvicorn's text output where they could not be aggregated. This adds an access log and an optional JSON output format, so latency can be charted in Kibana.
Setting
log_format: json(orX2S3_LOG_FORMAT=json) writes one Elastic Common Schema object per line to stdout, which a shipper parses straight off the container's logs. Text remains the default and stays human-readable; nothing changes unless you turn this on.What's in it
AccessLogMiddlewareis pure ASGI rather thanBaseHTTPMiddleware, because x2s3 mostly streams large objects: the app coroutine only returns once the last body chunk is sent, soevent.durationis real transfer time rather than time-to-first-byte, and the response bytes can be counted on the way past.Each access line carries
event.duration(nanoseconds, the ECS unit),http.request.method,http.response.status_code,http.response.body.bytes,url.path/url.query/url.domain,client.ip,user_agent.original,trace.id, andlabels.target— the dimension worth slicing latency by.trace.idis the same value already returned in thex-amz-request-idheader, and it is bound to every line logged while serving the request, so a client can quote an id and you get the whole request.ECS fields are written as dotted keys rather than nested dicts; Elasticsearch expands them on ingest, so the indexed document is the same one without any dict-building in the code.
Uvicorn's own access log is disabled wherever the middleware is installed, so requests are logged once. That means text mode changes slightly: the same one-line-per-request format, now with the duration, the byte count and the target. Doing it in code rather than with
--no-access-logcovers the pinned command line baked into the Docker image.Also here
A pre-existing crash, found while reading the new JSON logs in production:
html.escape()only accepts strings, so aNonereaching an error template raisedAttributeErrorand turned a handled upstream error into an unhandled 500 with a traceback. The trigger was a backend answering with an empty<Resource/>, which botocore parses toNone. Guarded once in the templates where every caller routes through — the same crash was also reachable fromlist_objects_v2forwarding its optional prefix as the key, and fromHTTPException.detail, which is not required to be a string. TheResourcefallback inhandle_s3_exceptionis restored too:dict.get's default is skipped when the key is present but null, which is exactly this case.Testing
tests/test_log.pycovers both formats: JSON mode emits one parseable ECS object per request with a numeric duration, error statuses log at warning, and text mode stays human-readable.tests/test_xss_prevention.pygains six tests for the null-value crash, including the reported traceback end to end throughhandle_s3_exception. 91 pass.Also verified under real uvicorn in both modes, confirming the access log is not duplicated and that uvicorn's own lines are routed through the JSON sink.
Docs
docs/StructuredLogging.mdcovers the field list, a Filebeat input snippet, and the Kibana side (setevent.duration's field format to Duration/nanoseconds and a percentile agg reads as p95/p99 directly).Claude Code Reviews
This PR also deletes the failed security review CI that was mistakenly merged onto main. We are experimenting with Claude Code review on this PR.
@StephanPreibisch @neomorphic