Files
ask262/.opencode/plans/1777288192730-brave-harbor.md
T
bendtherules 627b80008e feat(langfuse): add opt-in Langfuse observability integration
Add Langfuse as an OTel trace transport for MCP request lifecycle tracking.

Changes:
- New src/lib/langfuse-transport.ts: initializes LangfuseSpanProcessor when
  ASK262_LANGFUSE_ENABLED=true with valid credentials
- src/lib/tracing.ts: add metadata to TraceContext, propagate it to child spans,
  use startActiveSpan for proper OTel context, add setupTracing() entry point,
  add getSessionMetadata() for version/transport metadata
- src/lib/logger.ts: include TraceContext.metadata in JSON log mixin
- src/mcp-server-{http,stdio}.ts: call setupTracing() at startup, pass
  version/transport metadata through withSpan/withSpanContext
- src/lib/fireworks-embeddings.ts: wrap API calls in withSpan() with gen_ai
  attributes for Langfuse generation/cost tracking
- .env.example: add LANGFUSE_* configuration variables
- AGENTS.md: document Langfuse integration
- package.json: add @langfuse/tracing, @langfuse/otel, @opentelemetry/sdk-node
2026-04-27 18:31:19 +05:30

3.9 KiB

Langfuse Integration Plan

Dependencies

"@langfuse/tracing": "^1.0.0",
"@langfuse/otel": "^1.0.0",
"@opentelemetry/sdk-node": "^0.50.0"

Files

src/lib/langfuse-transport.ts (new)

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<string, unknown> {
  return {
    version: pkg.version,
    transport,
  };
}

src/lib/tracing.ts

Add metadata to TraceContext:

interface TraceContext {
  traceId: string;
  spanId: string;
  parentSpanId: string | null;
  depth: number;
  metadata?: Record<string, unknown>;
}

Add setupTracing() function:

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:

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():

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():

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:

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

# 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