Skip to content

Add automatic Logfire export for captured sessions - #28

Merged
duncankmckinnon merged 4 commits into
mainfrom
logfire-export
Aug 20, 2026
Merged

Add automatic Logfire export for captured sessions#28
duncankmckinnon merged 4 commits into
mainfrom
logfire-export

Conversation

@duncankmckinnon

Copy link
Copy Markdown
Owner

Summary

  • Mirrors every thirdeye session into Pydantic Logfire live, as OTel traces — no separate export step. tool_call/tool_result pairs (matched on Claude's tool_use_id / Codex's call_id) merge into one span with a real duration; everything else becomes a timeline marker, all under one trace per session.
  • Export is dispatched from Store.append_event — the single call site every Claude Code / Codex hook already goes through — but the actual Logfire call (configure + a network flush) runs in a detached, unwaited-for background process (thirdeye.otel_worker), so enabling this adds no latency to tool calls.
  • New thirdeye logfire enable/disable/status CLI commands and a settings page + Enable/Disable button in the web UI, both persisting the gateway key and project to ~/.thirdeye/config.yaml indefinitely.
  • Homebrew formula now bundles both the ui and logfire extras' resources, so brew install gets everything without a separate pip install.

Notable implementation details

  • Each session becomes one Logfire trace: the first event exported becomes the trace's root span, and its real trace_id/span_id are persisted to a small otel.json sidecar so later hook subprocesses (which can't share in-memory state) parent correctly under it.
  • Hardened against two issues found by hand while testing against the real logfire SDK: Logfire's default scrubber redacts any attribute whose key matches /session/, which was blanking the session identifier (fixed by using the OTel GenAI gen_ai.conversation.id key instead); and Logfire's background token-check thread writes warnings straight to stderr on a bad/unreachable token even after the call that triggered it returns, which needed a permanent process-global suppression rather than a scoped one (this runs inside hook subprocesses whose stdout/stderr Claude Code may read as part of a hook decision, so it must stay silent).
  • The Homebrew formula's new resources were validated by actually building the full 35-package dependency closure from sdist into a throwaway Python 3.12 venv and running thirdeye logfire status / thirdeye --help against it, not just transcribed from a lockfile.

Test plan

  • pytest — 1668 passed
  • ruff check / ruff format --check clean
  • Manual smoke test of real thirdeye-claude-* hook binaries with Logfire enabled (bad token): stdout/stderr silent, hook returns in ~100ms, otel.json and job-file cleanup both confirmed async
  • Web settings page exercised end-to-end via Starlette TestClient and a live app instance (enable persists + masks token, disable keeps token, badge updates)
  • Homebrew formula's new dependency closure built from sdist and imported successfully in a fresh venv
  • Real brew install --build-from-source pass (not runnable in this environment) — worth doing before merge, along with the still-placeholder url/sha256 on the top-level thrdi source tarball pending an actual PyPI release

🤖 Generated with Claude Code

duncankmckinnon and others added 4 commits August 18, 2026 17:49
Mirrors every thirdeye session into Pydantic Logfire live, as OTel traces,
with no separate sync step. Export is dispatched from the same Claude Code /
Codex hooks that already capture events, but the actual Logfire call runs in
a detached background process (thirdeye.otel_worker) so a slow or unreachable
endpoint adds no latency to the tool call that triggered it.

- thirdeye logfire enable/disable/status (CLI) and a settings page + button
  in the web UI, both persisting the gateway key/project to config.yaml
- tool_call/tool_result pairs merge into one span with a real duration;
  everything else becomes a timeline marker under one trace per session
- Homebrew formula now bundles the ui and logfire extras' resources

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each individual model call within a turn now gets its own "chat" span
nested under that turn's assistant_message, carrying real per-call
gen_ai.usage.* counts plus gen_ai.input.messages / gen_ai.output.messages
with the actual conversation content (text, tool calls/results, and
reasoning/thinking blocks) — matching the format Logfire's own GenAI
instrumentations use, so traces render with a proper chat view instead of
just token totals. Codex keeps local usage tracking only for now: its
rollout reports far more usage entries than matching content frames, so
there's no clean per-call content to attach yet.

Also fixes Logfire's default scrubber redacting anything containing the
word "session" (very common in a coding agent's own captured content) via
a scrubbing callback that exempts only that pattern, leaving real secret
patterns (password, api_key, etc.) redacted as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@duncankmckinnon
duncankmckinnon merged commit c647a87 into main Aug 20, 2026
5 checks passed
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.

1 participant