# Langfuse Integration Plan ## Dependencies ```json "@langfuse/tracing": "^1.0.0", "@langfuse/otel": "^1.0.0", "@opentelemetry/sdk-node": "^0.50.0" ``` ## Files ### `src/lib/langfuse-transport.ts` (new) ```typescript import { NodeTracerProvider } from "@opentelemetry/sdk-node"; import { LangfuseSpanProcessor } from "@langfuse/otel"; import pkg from "../../package.json" assert { type: "json" }; let provider: NodeTracerProvider | null = null; export function initializeLangfuseTransport(): void { if (process.env.ASK262_LANGFUSE_ENABLED !== "true") return; if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) { console.error("[LANGFUSE] Missing credentials, skipping"); return; } provider = new NodeTracerProvider({ spanProcessors: [new LangfuseSpanProcessor()], }); provider.register(); } export function getLangfuseSessionMetadata(transport: "http" | "stdio"): Record { return { version: pkg.version, transport, }; } ``` ### `src/lib/tracing.ts` Add `metadata` to `TraceContext`: ```typescript interface TraceContext { traceId: string; spanId: string; parentSpanId: string | null; depth: number; metadata?: Record; } ``` Add `setupTracing()` function: ```typescript import { initializeLangfuseTransport } from "./langfuse-transport.js"; export function setupTracing(): void { initializeLangfuseTransport(); } ``` Propagate metadata to child contexts in `withSpan()` and `withSpanContext()`. ### `src/lib/logger.ts` Include metadata in Pino mixin: ```typescript mixin() { const traceCtx = getTraceContext(); if (!traceCtx) return {}; return { trace_id: traceCtx.traceId, span_id: traceCtx.spanId, parent_span_id: traceCtx.parentSpanId, ...(traceCtx.metadata && { metadata: traceCtx.metadata }), }; } ``` ### `src/mcp-server-http.ts` Add at top of `main()`: ```typescript import { setupTracing } from "./lib/tracing.js"; setupTracing(); ``` In `/mcp` handler, set session metadata on root span via `withSpan` attributes. ### `src/mcp-server-stdio.ts` Add at top of `main()`: ```typescript import { setupTracing } from "./lib/tracing.js"; setupTracing(); ``` Set session metadata on the process-scoped trace. ### `src/lib/fireworks-embeddings.ts` Wrap actual Fireworks API call in `withSpan()` with generation attributes: ```typescript import { withSpan } from "./tracing.js"; import { trace } from "@opentelemetry/api"; return await withSpan( LogOperation.EMBEDDING_DOCUMENTS, { model: this.modelName, batch_size: this.batchSize }, async () => { const activeSpan = trace.getActiveSpan(); activeSpan?.setAttribute("gen_ai.system", "fireworks"); activeSpan?.setAttribute("gen_ai.request.model", this.modelName); activeSpan?.setAttribute("gen_ai.usage.input_tokens", estimatedTokens); return embeddings; } ); ``` ### `.env.example` ```bash # Langfuse observability (opt-in) # ASK262_LANGFUSE_ENABLED=true # LANGFUSE_PUBLIC_KEY=pk-lf-... # LANGFUSE_SECRET_KEY=sk-lf-... # LANGFUSE_BASE_URL=https://cloud.langfuse.com ``` ## Session Strategy | Transport | Session ID | Metadata | |-----------|-----------|----------| | **Stdio** | `sessionTraceId` (existing process-scoped trace ID) | `{ version: "0.0.15", transport: "stdio" }` | | **HTTP** | `x-request-id` or generated UUID | `{ version: "0.0.15", transport: "http" }` | Metadata flows to **both** destinations: - **JSON logs**: via `TraceContext.metadata` → logger mixin - **Langfuse**: via `langfuse.trace.metadata` OTel span attribute ## What stays unchanged - `src/agent-tools/*` — no changes, spans flow automatically - All tool internals — no changes needed ## Trace structure - **Stdio**: one trace per session, all tool calls share `sessionTraceId` - **HTTP**: one trace per request - **Embedding calls**: marked as `gen_ai` generations for cost tracking - **All traces and logs include**: version and transport metadata