Repository navigation
feat(tracing): own the traces with OpenTelemetry instead of Langfuse - #49
Merged
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/GenAiTraceris a LangChain callback, attached per request inagentService.ts. It emits:invoke_agentfor the run,chatfor each model call andexecute_toolfor each tool call; graph nodes and middleware get no span;cameron.approval.requestedevent on the run span, with tool names only;semconv.tsis the only file naminggen_ai.*/cameron.*attributes, because the conventions are still at Development stability.OTEL_EXPORTER_OTLP_ENDPOINTis set. The code has no default destination.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.tsstarts the OTel Node SDK with the OTLP exporter.src/instrumentation.node.ts, so the Edge build never sees it.src/. With ansrc/applayout,next buildonly looks there: a file at the repo root still runs underpnpm 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.fetchspans stay in the same trace, so agent tracing and ordinary request tracing sit side by side.compose.yamlalso passesOPENAI_COMPATIBLE_API_KEYto the production web service, and runs Phoenix as the default backend, split the same way as the data:phoenix:--profile tracing, port 6006;phoenix-prod:--profile full, port 6016, its own volume;PROD_OTEL_*variables, so a dev value in.envcan't redirect production traces.@langfuse/*. Langfuse, Jaeger or any OTLP backend is now an endpoint change; seedocs/OBSERVABILITY.md.docs/OBSERVABILITY.mdis 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) andtsc --noEmitpass;pnpm buildsucceeds.--profile full): agent runs trace tophoenix-prodon 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