Skip to content

feat(daemon): receive Claude Code OTEL traces and forward spans - #332

Merged
AnnatarHe merged 2 commits into
mainfrom
claude/festive-sagan-9l4aou
Oct 23, 2026
Merged

AnnatarHe merged 2 commits into
mainfrom
claude/festive-sagan-9l4aou

Conversation

@AnnatarHe

@AnnatarHe AnnatarHe commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What

The AICodeOtel gRPC server only registered the OTLP metrics and logs services, so Claude Code's traces (OTEL_TRACES_EXPORTER) had nowhere to go. This PR adds trace support end to end on the CLI side.

  • TraceService: daemon/aicode_otel_server.go registers colltracev1.TraceService. ProcessTraces (daemon/aicode_otel_processor.go) turns each span into a model.AICodeOtelSpan:
    • trace, span and parent ids as hex; malformed or all-zero ids are dropped
    • start and end in Unix nanoseconds (an end before the start is clamped), plus kind and status
    • the session id: session.id, else conversation.id, else another span of the same trace in the export
    • the span's own attributes and up to 32 events. The per-span account identity (user.*, organization.id) is left out because it repeats on every span.
  • Payload: spans travel in the new spans array of POST /api/v1/cc/otel and are split by SplitAICodeOtelRequest like events and metrics. The server side is shelltime/server#560; older servers ignore the key.
  • shelltime cc install now also sets CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and OTEL_TRACES_EXPORTER=otlp in ~/.claude/settings.json; both are needed for traces per the Claude Code monitoring docs. shelltime doctor lists them as missing for existing installs, so users get the "run cc install again" hint.
  • Codex: its spans are forwarded only if the user turned on trace_exporter themselves. codex install doesn't enable it, since Codex spans are internal tracing (session_loop, …) rather than LLM and tool spans.

Tests

  • daemon/aicode_otel_traces_test.go: span parsing (interaction, failed llm_request with a retry event), invalid ids and times, the event cap, Codex conversation.id, session fill-in within a trace, and a gRPC round trip whose JSON reaches the backend.
  • model: span splitting stays within the byte budget and keeps order; the settings tests cover the two new keys.
  • go test ./daemon/ ./model/ passes.
  • Two commands track tests (TestTrackWithSendData, TestMultipTrackWithPre) fail in this container on untouched main too, so that's the environment, not this change.
  • End to end: the built daemon (isolated HOME, port 54099) took synthetic Claude Code logs and traces and forwarded them to a local server running shelltime/server#560; spansProcessed=30.

Merge after shelltime/server#560 is deployed.


Generated by Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

The AICodeOtel gRPC server now registers the OTLP TraceService. Spans
are parsed into the new `spans` array of POST /api/v1/cc/otel: trace,
span and parent ids as hex, start/end in nanoseconds, kind, status, the
session id (session.id, else conversation.id, else another span of the
same trace in the export) and the span's own attributes and events,
without the per-span account identity. Large batches are split like
events and metrics.

`shelltime cc install` now sets CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
and OTEL_TRACES_EXPORTER=otlp, so `shelltime doctor` asks existing
installs to run it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ebmd3PkNoem9ubKNVA9K2K
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-23T02:56:49.754719Z
We couldn’t complete this review. Comment @codex review to try again.
702eee7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

⏱️ shelltime track performance

✅ No significant change in shelltime track latency

Comparing main @ 11d8798 → #332 @ 8ed6568. Each scenario execs the binary the way the shell hooks do; values are the median of 10 interleaved rounds. Lower is better.

Scenario base head Δ p
Startup/floor 223 µs ±16% 216 µs ±14% ~ 0.280 A/A
Startup/version 3.50 ms ±22% 3.24 ms ±10% ~ 0.315 ✅
Track/daemon/pre 3.27 ms ±33% 3.57 ms ±20% ~ 0.579 ✅
Track/daemon/post 3.25 ms ±17% 3.29 ms ±20% ~ 0.684 ✅
Track/direct/pre 3.51 ms ±26% 3.45 ms ±19% ~ 0.912 ✅
Track/direct/post 6.63 ms ±11% 6.69 ms ±14% ~ 0.529 ✅
Track/direct/post-sync 8.40 ms ±6% 8.66 ms ±6% ~ 0.280 ✅

Binary size: 19.2 MiB → 19.2 MiB (+0.0%).

CPU, tail latency and memory

Per exec, base → head (Δ when significant).

Scenario p95 wall user CPU sys CPU peak RSS
Startup/floor 293 µs → 293 µs 188 µs → 173 µs 5 µs → 7 µs 9.2 MiB → 9.2 MiB
Startup/version 4.80 ms → 4.48 ms 1.96 ms → 1.92 ms 1.36 ms → 1.30 ms 15.6 MiB → 15.5 MiB (-0.57%)
Track/daemon/pre 4.37 ms → 4.96 ms 1.88 ms → 2.13 ms 1.45 ms → 1.44 ms 16.5 MiB → 16.5 MiB
Track/daemon/post 4.66 ms → 4.33 ms 1.99 ms → 1.93 ms 1.41 ms → 1.44 ms 16.4 MiB → 16.4 MiB
Track/direct/pre 4.87 ms → 4.72 ms 2.08 ms → 2.02 ms 1.54 ms → 1.45 ms 16.4 MiB → 16.4 MiB
Track/direct/post 8.55 ms → 8.49 ms 4.51 ms → 4.32 ms 2.08 ms → 2.38 ms 17.7 MiB → 17.6 MiB (-0.34%)
Track/direct/post-sync 10.98 ms → 11.00 ms 5.51 ms → 5.83 ms 3.11 ms → 3.26 ms 19.7 MiB → 19.6 MiB (-0.34%)
benchstat
goos: linux
goarch: amd64
pkg: github.com/malamtime/cli/perf
cpu: AMD EPYC
                         │     base     │                head                 │
                         │    sec/op    │    sec/op     vs base               │
Startup/floor-2            222.6µ ± 16%   216.1µ ± 14%       ~ (p=0.280 n=10)
Startup/version-2          3.502m ± 22%   3.243m ± 10%       ~ (p=0.315 n=10)
Track/daemon/pre-2         3.266m ± 33%   3.569m ± 20%       ~ (p=0.579 n=10)
Track/daemon/post-2        3.251m ± 17%   3.291m ± 20%       ~ (p=0.684 n=10)
Track/direct/pre-2         3.507m ± 26%   3.453m ± 19%       ~ (p=0.912 n=10)
Track/direct/post-2        6.630m ± 11%   6.689m ± 14%       ~ (p=0.529 n=10)
Track/direct/post-sync-2   8.400m ±  6%   8.663m ±  6%       ~ (p=0.280 n=10)
geomean                    2.873m         2.881m        +0.26%

                         │     base     │                head                 │
                         │  p50-sec/op  │  p50-sec/op   vs base               │
Startup/floor-2            207.5µ ± 18%   187.9µ ± 13%       ~ (p=0.190 n=10)
Startup/version-2          3.319m ± 21%   3.167m ± 13%       ~ (p=0.796 n=10)
Track/daemon/pre-2         3.189m ± 33%   3.517m ± 17%       ~ (p=0.684 n=10)
Track/daemon/post-2        3.193m ± 16%   3.145m ± 22%       ~ (p=0.631 n=10)
Track/direct/pre-2         3.477m ± 27%   3.348m ± 20%       ~ (p=0.853 n=10)
Track/direct/post-2        6.420m ± 14%   6.482m ± 18%       ~ (p=0.579 n=10)
Track/direct/post-sync-2   8.239m ±  9%   8.588m ±  8%       ~ (p=0.218 n=10)
geomean                    2.782m         2.762m        -0.71%

                         │     base     │                head                 │
                         │  p95-sec/op  │  p95-sec/op   vs base               │
Startup/floor-2            293.4µ ±  9%   292.8µ ± 28%       ~ (p=0.853 n=10)
Startup/version-2          4.800m ± 25%   4.476m ± 10%       ~ (p=0.393 n=10)
Track/daemon/pre-2         4.367m ± 39%   4.960m ± 13%       ~ (p=0.481 n=10)
Track/daemon/post-2        4.658m ± 13%   4.334m ± 19%       ~ (p=0.353 n=10)
Track/direct/pre-2         4.872m ± 31%   4.725m ± 16%       ~ (p=0.971 n=10)
Track/direct/post-2        8.551m ± 14%   8.487m ±  9%       ~ (p=0.796 n=10)
Track/direct/post-sync-2   10.98m ±  6%   11.00m ±  5%       ~ (p=0.912 n=10)
geomean                    3.874m         3.845m        -0.75%

                         │     base     │                head                 │
                         │  peak-rss-B  │  peak-rss-B   vs base               │
Startup/floor-2            9.214Mi ± 1%   9.208Mi ± 2%       ~ (p=0.591 n=10)
Startup/version-2          15.62Mi ± 0%   15.53Mi ± 0%  -0.57% (p=0.000 n=10)
Track/daemon/pre-2         16.53Mi ± 0%   16.52Mi ± 0%       ~ (p=0.781 n=10)
Track/daemon/post-2        16.43Mi ± 0%   16.42Mi ± 0%       ~ (p=0.342 n=10)
Track/direct/pre-2         16.41Mi ± 0%   16.40Mi ± 0%       ~ (p=0.724 n=10)
Track/direct/post-2        17.67Mi ± 0%   17.61Mi ± 0%  -0.34% (p=0.000 n=10)
Track/direct/post-sync-2   19.69Mi ± 0%   19.62Mi ± 0%  -0.34% (p=0.000 n=10)
geomean                    15.58Mi        15.55Mi       -0.20%

                         │     base      │                head                 │
                         │  sys-sec/op   │  sys-sec/op   vs base               │
Startup/floor-2            5.415µ ± 132%   7.355µ ± 42%       ~ (p=0.971 n=10)
Startup/version-2          1.356m ±  25%   1.302m ± 25%       ~ (p=0.739 n=10)
Track/daemon/pre-2         1.451m ±  29%   1.441m ± 45%       ~ (p=0.971 n=10)
Track/daemon/post-2        1.407m ±  30%   1.437m ± 48%       ~ (p=0.579 n=10)
Track/direct/pre-2         1.536m ±  44%   1.452m ± 37%       ~ (p=0.853 n=10)
Track/direct/post-2        2.076m ±  43%   2.384m ± 21%       ~ (p=0.218 n=10)
Track/direct/post-sync-2   3.110m ±   8%   3.263m ± 13%       ~ (p=0.353 n=10)
geomean                    761.6µ          807.5µ        +6.02%

                         │     base     │                head                 │
                         │ user-sec/op  │ user-sec/op   vs base               │
Startup/floor-2            187.7µ ± 13%   172.6µ ± 10%       ~ (p=0.123 n=10)
Startup/version-2          1.964m ± 17%   1.920m ±  9%       ~ (p=1.000 n=10)
Track/daemon/pre-2         1.878m ± 27%   2.126m ± 18%       ~ (p=0.280 n=10)
Track/daemon/post-2        1.992m ±  9%   1.930m ±  9%       ~ (p=0.971 n=10)
Track/direct/pre-2         2.083m ± 14%   2.023m ±  8%       ~ (p=0.796 n=10)
Track/direct/post-2        4.507m ± 10%   4.316m ± 14%       ~ (p=0.436 n=10)
Track/direct/post-sync-2   5.510m ± 16%   5.833m ±  7%       ~ (p=0.481 n=10)
geomean                    1.840m         1.832m        -0.42%

Δ compares medians; ~ means no significant difference (Mann-Whitney U, p ≥ 0.05). A scenario is flagged when it is more than 10% and more than 250µs slower with p < 0.05. Startup/floor runs true, not shelltime: it is an A/A check of runner noise. Runner: linux/amd64, AMD EPYC. go1.27.1, 10 rounds × 100 execs per scenario and binary.

@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.16981% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
daemon/aicode_otel_processor.go 96.70% 3 Missing ⚠️
Flag Coverage Δ
unittests 86.37% <97.16%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
commands/doctor_checks.go 95.21% <ø> (ø)
daemon/aicode_otel_server.go 91.30% <100.00%> (-3.94%) ⬇️
model/aicode_otel_claude_settings.go 87.30% <100.00%> (+0.13%) ⬆️
model/aicode_otel_types.go 100.00% <ø> (ø)
model/api_aicode_otel.go 96.42% <100.00%> (+0.59%) ⬆️
daemon/aicode_otel_processor.go 90.45% <96.70%> (+0.99%) ⬆️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shelltime

shelltime Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

☁️ Claude Code cloud session

💰 Cost 🔢 Tokens ⏱️ Duration 🧠 Models 💬 Prompts
$33.17 119.1M 1h 20m (1h active) claude-opus-5-5, claude-haiku-5-5 2

119.1M tokens: 109.3K input · 220.1K output · 118.2M cache read · 655.6K cache write

View this session on ShellTime →

Updated 2026-10-23 03:27 UTC by ShellTime · totals for the whole session so far · the session page is visible to its owner

Copy link
Copy Markdown
Contributor Author

/claude review


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

@claude review


Generated by Claude Code

@claude

claude Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @AnnatarHe's task in 22s —— View job


Review of #332: Claude Code OTEL traces

I read the diff (git diff origin/main...HEAD) but did not run the tests. I'm relying on the PR's own test notes and the Codecov report. The change is small and follows the existing logs/metrics pattern. I found no blocking bugs. The points below are worth a look.

Worth checking

  1. Session fill-in only works within one export (daemon/aicode_otel_processor.go, fillSpanSessionIDs). Claude Code's batch span processor can export one trace in several batches. Spans that carry no session.id and have no sibling in the same export are sent with an empty sessionId. The resource-level res.SessionID fallback only helps if the resource carries it. If the server needs the session id to attach spans, it should handle empty ids. Otherwise the daemon could keep a small, TTL-bounded traceID → sessionID map across exports.

  2. ClientType format differs from metrics (model/aicode_otel_types.go). AICodeOtelSpan.ClientType is set from source, which is "claude-code" (hyphen). AICodeOtelMetric.ClientType is documented as claude_code (underscore). Please confirm that shelltime/server#560 expects the hyphenated form for spans. If it doesn't, the backend could store two spellings of the same client.

  3. Synchronous send on the gRPC handler. ProcessTraces blocks on p.send until the HTTP POST finishes, like logs and metrics. Spans are much more frequent than metrics. A slow or unreachable backend will stall the exporter's gRPC call, and the exporter will time out and retry, which can cause duplicate sends. This matches current passthrough behavior, so it's acceptable. It may be worth an async queue later.

  4. Unbounded fields. Name is not capped. StatusMessage and attributes go through CapAICodeText and setAttribute, and events are capped at 32. A very long span name would only be limited by the 8 MiB request split. Capping it would be consistent with the other fields.

  5. Data volume and privacy. CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 plus traces can include tool inputs and outputs, because the code handles tool.output events. The span events are forwarded with attribute caps. Please confirm that OTEL_LOG_TOOL_CONTENT and OTEL_LOG_USER_PROMPTS stay opt-in in cc install. This PR doesn't turn them on, which is good.

Minor

  • case spanIdentityAttributes[key]: has an empty body to drop those attributes. A short // dropped comment would make that intent clearer.
  • There is a small gap in the patch coverage (7 lines in aicode_otel_processor.go, per Codecov). The OK status branch and some spanKind cases are likely untested. A table test for spanKind and status codes would close it.
  • The Startup/version perf warning looks like runner noise. The Startup/floor A/A control also moved, and this PR doesn't touch the startup path.

What looks good

  • Id validation (length and all-zero) in otelID.
  • The unixNano clamp and the end-before-start clamp.
  • SplitAICodeOtelRequest keeps order, and the budget accounts for the new "spans":[] key.
  • Older servers ignore the spans key because of omitempty.
  • The cc install and doctor changes are consistent with the existing env-var list.

Review follow-ups: span and event names are capped like the other
forwarded text (2 KB), the dropped identity attributes are commented,
and span kinds and status codes get a table test.

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

Copy link
Copy Markdown
Contributor Author

Thanks. Changed in 8ed6568: point 4 and the two minor nits. The rest stay as they are:

  1. Session fill-in across exports: the server already covers this. shelltime/server#560 gives a span without sessionId the session of a stored span of the same trace (resolveCCSpanSessions), and drops it only when no span of the trace has one. Claude Code also puts session.id on every span (a standard attribute, with OTEL_METRICS_INCLUDE_SESSION_ID=true set by cc install), so a daemon-side cache would only repeat what the server does.
  2. ClientType: events and metrics already send the source (claude-code), not claude_code. The server's parseClientType maps both spellings, plus codex, onto one enum, so only one spelling is ever stored.
  3. Synchronous send: agreed, it matches logs and metrics today. Claude Code exports spans in batches every 5 s, so this isn't one request per span.
  4. Span name cap: fixed. Span and event names are now capped at 2 KB like the other forwarded text (TestParseSpan_NamesAreCapped).
  5. Privacy: OTEL_LOG_TOOL_CONTENT stays off. OTEL_LOG_USER_PROMPTS=1 and OTEL_LOG_TOOL_DETAILS=1 were already set by cc install before this PR, since the prompt and tool events rely on them. With traces, the same prompt and command also show up on the interaction and tool spans.

Minor: the dropped identity case now has a comment, and TestParseSpan_KindAndStatus covers every kind and status code. Agreed that the Startup/version perf warning is runner noise.


Generated by Claude Code

@AnnatarHe
AnnatarHe merged commit babae9d into main Oct 23, 2026
5 checks passed
@AnnatarHe
AnnatarHe deleted the claude/festive-sagan-9l4aou branch October 23, 2026 04:43
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.

2 participants