Introduce OAuth authentication for Logfire#6
Conversation
petyosi
left a comment
There was a problem hiding this comment.
SessionEnd hot-path regression risk
The recent commit 0f1b117 ("Close the session trace before /exit kills the SessionEnd hook") was specifically designed so the eager send_otlp for the root span (scripts/log-event.py:1167) runs before any locks or slow work — Claude Code aggressively kills hook subprocesses when /exit shuts the parent down, so the finalized root span has to ship first.
This PR moves OAuth resolution into main() (scripts/log-event.py:1269-1283), which runs before the dispatch into handle_session_end. When the access token is within 60s of expiry, that adds a synchronous refresh to the hot path:
- lock acquisition: up to 5s (
oauth_token.py:81,LOCK_MAX_WAIT_SECONDS) discover_metadataGET: 10s timeout (oauth_token.py:209)_http_post_formrefresh POST: 30s timeout (oauth_token.py:218)
Worst case ~45s before the eager root-span send even starts, against a 30s SessionEnd budget. In practice the refresh window is narrow (60s before expiry) so most sessions won't hit it — but when they do, /exit will kill the subprocess mid-refresh and the trace silently won't close, which is exactly the failure mode 0f1b117 was meant to prevent.
A few possible mitigations, any one would help:
- Plumb a short
timeoutthroughget_access_token→_refresh→_http_post_form/discover_metadata, and pass ~3s from the hook hot path (the CLI subcommands can keep the current 30s). - Cache
token_endpointin the stored bundle on login, so refresh skips the discovery roundtrip entirely. - Skip lazy refresh in
SessionEndspecifically: if the access token has any validity left, ship the trace first and let the next session'sSessionStartdo the refresh.
(3) is the smallest change and arguably the cleanest — SessionEnd is a one-shot finalization and doesn't benefit from a fresh token vs. a 30s-from-expiry one.
The eager root-span send added in 0f1b117 had to ship before any blocking work, but the OAuth resolution path in main() could synchronously refresh a near-expiry token (lock + discovery + token POST, up to ~45s) before the eager send ever ran — defeating the guard the SessionEnd hook needs against /exit killing the subprocess mid-finalisation. Two fixes: - Skip the lazy refresh on SessionEnd if the access token still has any validity left (`skip_refresh_if_valid` on `get_access_token`). The trace ships first; the next session's SessionStart handles the refresh. - Cache `token_endpoint` in the stored bundle at login (and on the first refresh of a legacy bundle), so the refresh path skips the RFC 8414 discovery GET. Login already has the metadata in hand. Also fix a stale docstring example: the RFC 9728 `resource` value is `https://logfire-us.pydantic.dev/v1`, not the `/v1/traces` endpoint URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
LOGFIRE_TOKEN, so users can authenticate the plugin against Logfire without manually managing a long-lived write token.scripts/auth.pyCLI (login/logout/status/refresh) wired up via/logfire-session-capture:login,/logfire-session-capture:logout,/logfire-session-capture:status,/logfire-session-capture:refreshslash commands.scripts/oauth_token.pyshared module: persists a single token bundle at~/.logfire/claude-code-logfire-plugin.json(recording its ownbase_url), auto-refreshes when within 60s of expiry, and serialises refresh-token rotation via anos.mkdirlock so concurrent sessions don't race.scripts/log-event.pynow resolves auth in this order:LOGFIRE_TOKENenv var (highest precedence, backwards-compatible) → stored OAuth bundle → skip OTLP export silently.Test plan
python3 scripts/auth.py logincompletes the device-code flow and writes~/.logfire/claude-code-logfire-plugin.jsonpython3 scripts/auth.py statusreports the token expiry / scopepython3 scripts/auth.py refreshrotates the access tokenpython3 scripts/auth.py logoutremoves the bundleLOGFIRE_TOKEN), running a full SessionStart → Stop → SessionEnd hook lifecycle exports spans to LogfireLOGFIRE_TOKENset, the env var still wins over the stored bundle