Skip to content

feat: structured JSON logs with request ids; readiness that checks dependencies - #81

Merged
jbouder merged 2 commits into
mainfrom
feat/logging-readiness
Oct 19, 2026
Merged

jbouder merged 2 commits into
mainfrom
feat/logging-readiness

Conversation

@jbouder

@jbouder jbouder commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Closes #46

What changes

  • JSON logs (api, mcp). logs.py sets up structlog, with stdlib integration so uvicorn's own lines are JSON too: one object per line with timestamp, level, logger, event and fields. The level comes from LOG_LEVEL.
  • Request ids. RequestIdMiddleware (pure ASGI):
    • It keeps an incoming X-Request-ID if it is well formed (≤128 chars of A-Za-z0-9._:-), otherwise it makes one.
    • It binds the id to the log context, writes it back into the request headers and echoes it on the response.
    • It writes one access line per request: method, path, status, duration_ms, and on the API the authenticated user. Probe paths log at DEBUG.
  • Propagation.
    • The UI's nginx forwards the incoming id on /api and /mcp, or generates one when none arrives.
    • MCP tools run in the session task, not the request's. So ToolLogMiddleware re-binds the id from the HTTP request, logs tool call with tool, outcome and duration, and ApiClient sends the id to apps-api.
  • GET /api/v1/readyz:
    • It checks that the Kubernetes API answers list namespaces (limit=1, 3s timeout, already in the API's cluster role) and, with auth on, that the realm JWKS loads (3s; a success is reused for 60s so probes don't hammer Keycloak).
    • It returns 503 when a check fails. The body names the failing check but not the reason, since the endpoint is public; the reason is logged.
    • The chart's API readiness probe uses it (timeoutSeconds: 8). Liveness stays on the static /healthz, so an outage takes pods out of rotation without restarting them.
  • MCP GET /healthz replaces the tcpSocket probes.
  • Docs: request ids and /readyz in the API reference, plus a "Logs and probes" section and LOG_LEVEL in configuration.

Not done here: the operator's readyz is still healthz.Ping. The issue mentions it, but its acceptance criteria don't require it.

Upgrade notes

  • Log output changes from uvicorn's text format to JSON, so update any log parsing.
  • The API pod now goes unready while Keycloak's JWKS or the Kubernetes API is unreachable.

Testing

  • Locally:
    • 11 new API tests: id generated, kept, or replaced when malformed; the access line carries the id; readyz ok, failing store → 503 without leaking the reason, JWKS down → 503 then up → 200; liveness unaffected.
    • 3 new MCP tests: /healthz over HTTP with the id echoed; ApiClient forwards the id or sends none.
    • Totals: 126 API tests and 25 MCP tests pass, plus ruff and mypy strict. nginx -t passes on the rendered config.
  • nebari-local, with the api, mcp and ui images built from this branch:
    • Every API and MCP line is JSON, including uvicorn's startup lines.
    • Request ids, browser to API: Envoy assigns the id at the gateway. The id on the response matched the API's access line, which also named the user (authz-alice).
    • Request ids, MCP: a tool call logged tool call with the same id as its /mcp access line. ApiClient run inside the MCP pod sent exec-trace-1, and the API logged it.
    • /api/v1/readyz returned 200 with kubernetes and jwks ok. With OIDC_JWKS_URL pointed at an unresolvable host it returned 503 {"jwks":"failed"} and logged the DNS error. After the override was removed it went back to 200.
    • The probe definitions were checked in helm template. The cluster runs its vendored chart, so the new probes weren't exercised by kubelet.

🤖 Generated with Claude Code

https://claude.ai/code/session_016pvMUQJoLkBDk6B94TTm45

…pendencies

- api, mcp: structlog JSON on stdout (uvicorn's lines included). A
  RequestIdMiddleware keeps a well-formed incoming X-Request-ID or makes
  one, binds it to every log line, echoes it on the response and writes
  one access line (method, path, status, duration, user on the API).
- mcp forwards the id to apps-api and logs each tool call with it; the
  UI's nginx forwards it on /api and /mcp, generating one if absent.
- api: /api/v1/readyz checks the Kubernetes API (namespace list) and,
  with auth on, the realm JWKS, 3s each; the chart's readiness probe
  uses it. Failing checks are named, reasons only logged.
- mcp: HTTP /healthz, used by both probes instead of tcpSocket.

Closes #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pvMUQJoLkBDk6B94TTm45
@github-actions

Copy link
Copy Markdown

📄 Docs preview for feat/logging-readiness:
https://feat-logging-readiness.apps-pack.pages.dev

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pvMUQJoLkBDk6B94TTm45
@jbouder
jbouder merged commit 8efa555 into main Oct 19, 2026
13 checks passed
@jbouder
jbouder deleted the feat/logging-readiness branch October 19, 2026 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Structured JSON logging with request ids, and readiness probes that check dependencies

1 participant