Skip to content

feat(tracing): own the traces with OpenTelemetry instead of Langfuse - #49

Merged
IBJunior merged 6 commits into
mainfrom
feat/otel-tracing
Oct 16, 2026
Merged

IBJunior merged 6 commits into
mainfrom
feat/otel-tracing

Conversation

@IBJunior

@IBJunior IBJunior commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Why

Cameron inherited Langfuse tracing from the starter: a vendor callback pointed at a hosted backend by default. A trace carries every prompt, tool argument and transaction, so that setup quietly broke Cameron's third rule — financial data stays on infrastructure the owner controls. Tracing now uses OpenTelemetry's GenAI conventions and goes to a backend the owner runs.

What changed

  • src/lib/tracing/
    • GenAiTracer is a LangChain callback, attached per request in agentService.ts. It emits:
      • invoke_agent for the run, chat for each model call and execute_tool for each tool call; graph nodes and middleware get no span;
      • an approval pause as a cameron.approval.requested event on the run span, with tool names only;
      • a resumed run's approve/reject counts.
    • semconv.ts is the only file naming gen_ai.* / cameron.* attributes, because the conventions are still at Development stability.
  • Off by default, two opt-ins
    • Tracing turns on only when OTEL_EXPORTER_OTLP_ENDPOINT is set. The code has no default destination.
    • Content capture (OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true) is a separate switch. With it off, no prompt, message, tool argument or result, or error message leaves the process.
  • src/instrumentation.ts starts the OTel Node SDK with the OTLP exporter.
    • The Node-only part is in src/instrumentation.node.ts, so the Edge build never sees it.
    • Both files have to live in src/. With an src/app layout, next build only looks there: a file at the repo root still runs under pnpm dev, but the production image leaves it out without any error. The Langfuse setup inherited from the starter had the same problem, so it never traced in Docker.
    • Next.js's own route and fetch spans stay in the same trace, so agent tracing and ordinary request tracing sit side by side.
  • compose.yaml also passes OPENAI_COMPATIBLE_API_KEY to the production web service, and runs Phoenix as the default backend, split the same way as the data:
    • dev phoenix: --profile tracing, port 6006;
    • production phoenix-prod: --profile full, port 6016, its own volume;
    • both are bound to localhost only, with Phoenix's usage telemetry off;
    • production uses PROD_OTEL_* variables, so a dev value in .env can't redirect production traces.
  • Removed @langfuse/*. Langfuse, Jaeger or any OTLP backend is now an endpoint change; see docs/OBSERVABILITY.md.
  • Docs: docs/OBSERVABILITY.md is rewritten, the AGENTS.md "Two stacks" table lists Phoenix, and the Tracing section replaces the Langfuse one.

Verification

  • pnpm test (379 tests, 22 of them new and offline, run against an in-memory exporter) and tsc --noEmit pass; pnpm build succeeds.
  • Live on the dev stack, against the OpenAI-compatible provider:
    • a read, an approved write and a denied write all traced correctly, including the approval event and resume counts;
    • each thread is one Phoenix session;
    • with capture off, no content attributes appeared;
    • pointing the endpoint at Jaeger 2.11 delivered the same spans with no code change.
  • Production stack (--profile full): agent runs trace to phoenix-prod on 6016, including messages when capture is on. The production Docker image needs fix(fonts): self-host Geist so the production build needs no network #50, which is already merged into this branch.

No evals: nothing the model reads changed.

🤖 Generated with Claude Code

IBJunior and others added 2 commits October 16, 2026 14:40
Cameron inherited Langfuse tracing from the starter: a vendor callback pointed at a hosted
backend by default. A trace carries prompts, tool arguments and transactions, so that quietly
broke the rule that financial data stays on infrastructure the owner controls.

- GenAiTracer: a LangChain callback that emits OTel GenAI spans (invoke_agent, chat,
  execute_tool), records the approval gate as an event (tool names only) and a resume's
  approve/reject counts. Attribute names live in semconv.ts alone.
- Tracing is off unless OTEL_EXPORTER_OTLP_ENDPOINT is set; content capture is a separate
  opt-in (OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT). Off, no prompt, message, tool
  argument/result or error message leaves the process.
- compose: Phoenix as the default backend, split like the data — dev `phoenix`
  (--profile tracing, 6006) and prod `phoenix-prod` (--profile full, 6016, own volume),
  loopback-only, telemetry off. Prod vars are PROD_OTEL_* so a dev value can't redirect them.
- Removes @langfuse/*; any OTLP backend (Langfuse, Jaeger, ...) is an endpoint change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Next.js analyzes instrumentation.ts for the Edge runtime as well, so process.once() there
warned in the production build even behind the NEXT_RUNTIME check. The setup moves to
instrumentation.node.ts, imported only on Node.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
IBJunior and others added 4 commits October 16, 2026 17:00
Brings in the self-hosted Geist fonts (#50) so the production image builds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With an src/app layout, `next build` only detects instrumentation.ts inside src/. At the repo
root it still ran under `pnpm dev`, but the standalone build left the hook out entirely, so
the production image never started the SDK. Verified: the standalone output now contains
instrumentation.js and its trace reaches Phoenix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rvice

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
isBaseMessage → BaseMessage.isInstance, getType() → .type, as their deprecation notes direct.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@IBJunior
IBJunior merged commit d1a0652 into main Oct 16, 2026
1 check passed
@IBJunior
IBJunior deleted the feat/otel-tracing branch October 16, 2026 17:28
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