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
This commit is contained in:
2026-04-27 18:31:19 +05:30
parent 40b439307e
commit 627b80008e
9 changed files with 466 additions and 97 deletions
@@ -0,0 +1,153 @@
# 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<string, unknown> {
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<string, unknown>;
}
```
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