mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 13:21:55 +00:00
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:
@@ -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
|
||||
Reference in New Issue
Block a user