UNPKG

genkitx-langfuse

Version:

Genkit AI framework plugin for Langfuse observability and tracing.

689 lines (682 loc) 23 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/index.ts import { genkitPlugin } from "genkit/plugin"; import { enableTelemetry } from "genkit/tracing"; // src/telemetry-provider.ts import { Resource } from "@opentelemetry/resources"; import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; // src/exporter.ts import { hrTimeToMilliseconds } from "@opentelemetry/core"; import { ExportResultCode } from "@opentelemetry/core"; import { Langfuse } from "langfuse"; // src/metadata-extractor.ts var SpanMetadataExtractor = class { /** * Extract all relevant metadata from a span. */ static extractMetadata(span) { const attributes = span.attributes; return { // Core Genkit attributes name: attributes["genkit:name"], path: attributes["genkit:path"], spanType: attributes["genkit:type"], input: attributes["genkit:input"], output: attributes["genkit:output"], state: attributes["genkit:state"], isRoot: attributes["genkit:isRoot"] === "true" || attributes["genkit:isRoot"] === true, // Session tracking (available in chat flows) sessionId: attributes["genkit:sessionId"], threadName: attributes["genkit:threadName"], // User context (if available) userId: attributes["genkit:userId"], // Additional metadata metadata: this.extractCustomMetadata(attributes) }; } /** * Extract custom metadata from genkit:metadata:* attributes. */ static extractCustomMetadata(attributes) { const metadata = {}; for (const [key, value] of Object.entries(attributes)) { if (key.startsWith("genkit:metadata:")) { const metadataKey = key.replace("genkit:metadata:", ""); metadata[metadataKey] = value; } } return metadata; } /** * Check if a span represents an LLM/model call. */ static isModelSpan(span) { const attributes = span.attributes; const spanType = attributes["genkit:type"]; const path = attributes["genkit:path"]; return spanType === "model" || Boolean(path && path.includes("/model/")); } /** * Check if a span is a root trace span. */ static isRootSpan(span) { const attributes = span.attributes; return attributes["genkit:isRoot"] === "true"; } /** * Extract usage information from output. */ static extractUsage(span) { const attributes = span.attributes; const output = attributes["genkit:output"]; if (!output) return null; try { const parsed = JSON.parse(output); if (parsed.usage) { return { inputTokens: parsed.usage.inputTokens || 0, outputTokens: parsed.usage.outputTokens || 0, totalTokens: parsed.usage.totalTokens || (parsed.usage.inputTokens || 0) + (parsed.usage.outputTokens || 0) }; } } catch (error) { } return null; } /** * Extract model configuration from input. */ static extractModelConfig(span) { const attributes = span.attributes; const input = attributes["genkit:input"]; if (!input) return null; try { const parsed = JSON.parse(input); return parsed.config || null; } catch (error) { return null; } } }; // src/exporter.ts var LangfuseExporter = class { constructor(config) { this.exportCount = 0; this.config = config; if (config.debug) { console.log("\u{1F527} [DEBUG] Initializing Langfuse exporter with config:", { baseUrl: config.baseUrl, publicKey: config.publicKey ? `${config.publicKey.substring(0, 8)}...` : "undefined", secretKey: config.secretKey ? `${config.secretKey.substring(0, 8)}...` : "undefined", flushAt: config.flushAt || 20, flushInterval: config.flushInterval || 1e4 }); } this.langfuse = new Langfuse({ secretKey: config.secretKey, publicKey: config.publicKey, baseUrl: config.baseUrl, flushAt: config.flushAt || 20, flushInterval: config.flushInterval || 1e4 }); this.langfuse.on("error", (error) => { console.error("\u{1F6A8} [ERROR] Langfuse SDK error:", error); if (config.debug) { console.error("\u{1F50D} [DEBUG] Error details:", { message: error.message, stack: error.stack, name: error.name }); } }); if (config.debug && this.langfuse.on) { try { this.langfuse.on("trace", () => { console.log("\u2705 [DEBUG] Langfuse trace event sent successfully"); }); } catch (e) { } } if (config.debug) { this.langfuse.on("flush", () => { console.log("\u2705 [DEBUG] Langfuse flush completed"); }); } if (config.debug) { console.log("\u2705 [DEBUG] Langfuse exporter initialized successfully"); this.testConnection(); } } /** * Export spans to Langfuse. */ export(spans, resultCallback) { this.exportCount++; if (this.config.debug) { console.log(`\u{1F4E4} [DEBUG] Export #${this.exportCount}: Exporting ${spans.length} spans to Langfuse`); spans.forEach((span, index) => { console.log(` Span ${index + 1}: ${span.name} (${span.spanContext().spanId})`); }); } try { let successCount = 0; let errorCount = 0; for (const span of spans) { try { this.processSpan(span); successCount++; } catch (spanError) { errorCount++; console.error(`\u{1F6A8} [ERROR] Failed to process span ${span.name}:`, spanError); if (this.config.debug) { console.error("\u{1F50D} [DEBUG] Span details:", { spanId: span.spanContext().spanId, traceId: span.spanContext().traceId, name: span.name, attributes: span.attributes }); } } } if (this.config.debug) { console.log(`\u{1F4CA} [DEBUG] Export summary: ${successCount} successful, ${errorCount} failed`); } if (this.config.debug && successCount > 0) { this.langfuse.flushAsync().then(() => { console.log("\u2705 [DEBUG] Post-export flush completed successfully"); }).catch((error) => { console.error("\u{1F6A8} [ERROR] Failed to flush after export:", error); }); } resultCallback({ code: ExportResultCode.SUCCESS }); } catch (error) { console.error("\u{1F6A8} [ERROR] Langfuse export error:", error); if (this.config.debug) { console.error("\u{1F50D} [DEBUG] Export error details:", { message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0 }); } resultCallback({ code: ExportResultCode.FAILED, error: error instanceof Error ? error : new Error(String(error)) }); } } /** * Test connection to Langfuse (for debugging). */ async testConnection() { if (!this.config.debug) return; try { console.log("\u{1F50D} [DEBUG] Testing Langfuse connection..."); console.log("\u{1F50D} [DEBUG] Langfuse API URL:", this.config.baseUrl); const testTrace = { id: "test-connection-" + Date.now(), name: "Connection Test", timestamp: /* @__PURE__ */ new Date(), metadata: { test: true } }; this.langfuse.trace(testTrace); console.log("\u{1F50D} [DEBUG] Flushing connection test trace..."); await this.langfuse.flushAsync(); console.log("\u2705 [DEBUG] Langfuse connection test completed"); } catch (error) { console.error("\u{1F6A8} [ERROR] Langfuse connection test failed:", error); } } /** * Shutdown the exporter. */ async shutdown() { if (this.config.debug) { console.log("\u{1F504} [DEBUG] Shutting down Langfuse exporter..."); } try { await this.langfuse.shutdownAsync(); if (this.config.debug) { console.log("\u2705 [DEBUG] Langfuse exporter shutdown completed"); } } catch (error) { console.error("\u{1F6A8} [ERROR] Error during Langfuse exporter shutdown:", error); throw error; } } /** * Force flush all pending spans. */ async forceFlush() { if (this.config.debug) { console.log("\u{1F504} [DEBUG] Force flushing Langfuse data..."); } try { await this.langfuse.flushAsync(); if (this.config.debug) { console.log("\u2705 [DEBUG] Langfuse force flush completed"); } } catch (error) { console.error("\u{1F6A8} [ERROR] Error during Langfuse force flush:", error); throw error; } } /** * Process a single span and send to Langfuse. */ processSpan(span) { const metadata = SpanMetadataExtractor.extractMetadata(span); const spanType = this.determineSpanType(span, metadata); if (this.config.debug) { console.log(`\u{1F50D} [DEBUG] Processing span: ${span.name}`); console.log(` Type: ${spanType}`); console.log(` Path: ${metadata.path || "unknown"}`); console.log(` SpanID: ${span.spanContext().spanId}`); console.log(` TraceID: ${span.spanContext().traceId}`); console.log(` ParentID: ${span.parentSpanId || "none"}`); console.log(` Duration: ${hrTimeToMilliseconds(span.endTime) - hrTimeToMilliseconds(span.startTime)}ms`); } try { switch (spanType) { case "generation": this.createGeneration(span, metadata); break; case "trace": this.createTrace(span, metadata); break; case "span": this.createSpan(span, metadata); break; default: if (this.config.debug) { console.log(`\u26A0\uFE0F [WARNING] Skipping span with unknown type: ${span.name} (type: ${spanType})`); } return; } if (this.config.debug) { console.log(`\u2705 [DEBUG] Successfully created Langfuse ${spanType}: ${span.name}`); } } catch (error) { console.error(`\u{1F6A8} [ERROR] Failed to create Langfuse ${spanType} for span ${span.name}:`, error); throw error; } } /** * Determine the Langfuse span type based on Genkit span data. */ determineSpanType(span, metadata) { const spanType = metadata.spanType; const path = metadata.path; const name = span.name; if (spanType === "model" || path?.includes("/model/") || name.includes("generate") || name.includes("model")) { return "generation"; } if (metadata.isRoot || spanType === "flow" || path?.includes("/flow/") || !span.parentSpanId || span.parentSpanId === "0000000000000000") { return "trace"; } if (spanType === "tool" || path?.includes("/tool/") || name.includes("tool") || name.includes("Tool")) { return "span"; } return "span"; } /** * Create a Langfuse generation (for LLM calls) using latest SDK v3 patterns. */ createGeneration(span, metadata) { const input = this.parseJSON(metadata.input); const output = this.parseJSON(metadata.output); const modelName = metadata.name || this.extractModelFromPath(metadata.path); const generationData = { id: span.spanContext().spanId, traceId: span.spanContext().traceId, name: span.name, model: modelName, input, // SDK v3 uses input instead of prompt output, // SDK v3 uses output instead of completion startTime: new Date(hrTimeToMilliseconds(span.startTime)), endTime: new Date(hrTimeToMilliseconds(span.endTime)), metadata: { genkit: true, spanType: metadata.spanType, path: metadata.path, state: metadata.state, provider: this.extractProviderFromPath(metadata.path), genkitVersion: "1.x", parentSpanId: span.parentSpanId } }; if (span.parentSpanId && span.parentSpanId !== "0000000000000000") { generationData.parentObservationId = span.parentSpanId; } if (output && output.usage) { generationData.usage = { input: output.usage.inputTokens, output: output.usage.outputTokens, total: output.usage.totalTokens }; if (this.config.calculateCost) { try { generationData.totalCost = this.config.calculateCost( modelName, { inputTokens: output.usage.inputTokens || 0, outputTokens: output.usage.outputTokens || 0, totalTokens: output.usage.totalTokens || 0 } ); } catch (error) { console.warn("\u26A0\uFE0F [WARNING] Failed to calculate cost:", error); } } } if (metadata.sessionId) { generationData.sessionId = metadata.sessionId; } if (metadata.userId) { generationData.userId = metadata.userId; } if (metadata.version) { generationData.version = metadata.version; } if (this.config.debug) { console.log("\u{1F916} [DEBUG] Creating Langfuse generation with data:", { id: generationData.id, traceId: generationData.traceId, name: generationData.name, model: generationData.model, inputSize: JSON.stringify(input || {}).length, outputSize: JSON.stringify(output || {}).length, usage: generationData.usage, hasParent: !!generationData.parentObservationId }); } try { this.langfuse.generation(generationData); if (this.config.debug) { console.log(`\u2705 [DEBUG] Langfuse generation created: ${span.name} (${modelName})`); } } catch (error) { console.error("\u{1F6A8} [ERROR] Failed to create Langfuse generation:", error); throw error; } } /** * Create a Langfuse trace (for root spans/flows). */ createTrace(span, metadata) { const input = this.parseJSON(metadata.input); const output = this.parseJSON(metadata.output); const trace = { id: span.spanContext().traceId, name: span.name, input, output, timestamp: new Date(hrTimeToMilliseconds(span.startTime)), metadata: { spanType: metadata.spanType, path: metadata.path, state: metadata.state, duration: hrTimeToMilliseconds(span.endTime) - hrTimeToMilliseconds(span.startTime), genkit: true } }; const sessionId = metadata.sessionId; if (sessionId) { trace.sessionId = sessionId; } const userId = metadata.userId; if (userId) { trace.userId = userId; } if (this.config.debug) { console.log("\u{1F3C1} [DEBUG] Creating Langfuse trace with data:", { id: trace.id, name: trace.name, inputSize: JSON.stringify(input || {}).length, outputSize: JSON.stringify(output || {}).length, duration: trace.metadata.duration, hasSession: !!sessionId, hasUser: !!userId }); } try { this.langfuse.trace(trace); if (this.config.debug) { console.log(`\u2705 [DEBUG] Langfuse trace created: ${span.name}`); } } catch (error) { console.error("\u{1F6A8} [ERROR] Failed to create Langfuse trace:", error); throw error; } } /** * Create a Langfuse span (for intermediate operations). */ createSpan(span, metadata) { const input = this.parseJSON(metadata.input); const output = this.parseJSON(metadata.output); const langfuseSpan = { id: span.spanContext().spanId, traceId: span.spanContext().traceId, name: span.name, input, output, startTime: new Date(hrTimeToMilliseconds(span.startTime)), endTime: new Date(hrTimeToMilliseconds(span.endTime)), metadata: { genkit: true, spanType: metadata.spanType, path: metadata.path, state: metadata.state, parentSpanId: span.parentSpanId } }; if (span.parentSpanId && span.parentSpanId !== "0000000000000000") { langfuseSpan.parentObservationId = span.parentSpanId; } if (this.config.debug) { console.log("\u{1F517} [DEBUG] Creating Langfuse span with data:", { id: langfuseSpan.id, traceId: langfuseSpan.traceId, name: langfuseSpan.name, inputSize: JSON.stringify(input || {}).length, outputSize: JSON.stringify(output || {}).length, hasParent: !!langfuseSpan.parentObservationId }); } try { this.langfuse.span(langfuseSpan); if (this.config.debug) { console.log(`\u2705 [DEBUG] Langfuse span created: ${span.name} (parent: ${span.parentSpanId || "none"})`); } } catch (error) { console.error("\u{1F6A8} [ERROR] Failed to create Langfuse span:", error); throw error; } } /** * Extract model name from Genkit path. */ extractModelFromPath(path) { if (!path) return "unknown"; const match = path.match(/\/model\/([^\/]+)\/([^\/]+)/); return match ? match[2] : "unknown"; } /** * Extract provider name from Genkit path. */ extractProviderFromPath(path) { if (!path) return "unknown"; const match = path.match(/\/model\/([^\/]+)\//); return match ? match[1] : "unknown"; } /** * Safely parse JSON string. */ parseJSON(jsonString) { if (!jsonString || typeof jsonString !== "string") { return jsonString; } try { return JSON.parse(jsonString); } catch (error) { return jsonString; } } }; // src/telemetry-provider.ts var LangfuseTelemetryProvider = class { constructor(config) { this.config = config; this.validateConfig(); } /** * Get the telemetry configuration for Genkit. */ getConfig() { const config = { resource: this.createResource(), spanProcessors: [this.createSpanProcessor()], // Back to plural like official plugin instrumentations: [] }; if (this.config.debug) { console.log("\u{1F527} [DEBUG] Telemetry config created:", { resourceAttributes: config.resource.attributes, spanProcessorCount: config.spanProcessors.length, instrumentationCount: config.instrumentations.length }); } return config; } /** * Shutdown the telemetry provider. */ async shutdown() { if (this.exporter) { await this.exporter.shutdown(); } } /** * Force flush all pending telemetry data. */ async flush() { if (this.exporter) { await this.exporter.forceFlush(); } } /** * Create OpenTelemetry resource with Langfuse-specific attributes. */ createResource() { return new Resource({ "service.name": "genkit-langfuse", "service.version": "1.0.0", "genkit.plugin": "@genkit-ai/langfuse", "langfuse.version": this.getLangfuseVersion() }); } /** * Create the span processor with Langfuse exporter. */ createSpanProcessor() { this.exporter = new LangfuseExporter(this.config); const isDevelopment = this.config.forceDevExport || process.env.NODE_ENV === "development"; const processor = new BatchSpanProcessor(this.exporter, { maxExportBatchSize: isDevelopment ? this.config.flushAt || 1 : this.config.flushAt || 20, scheduledDelayMillis: isDevelopment ? this.config.flushInterval || 1e3 : this.config.flushInterval || 1e4, exportTimeoutMillis: this.config.exportTimeoutMillis || 3e4, maxQueueSize: this.config.maxQueueSize || 1e3 }); if (this.config.debug) { console.log("\u{1F527} [DEBUG] BatchSpanProcessor created with config:", { isDevelopment, maxExportBatchSize: processor["_maxExportBatchSize"] || (isDevelopment ? 1 : 20), scheduledDelayMillis: processor["_scheduledDelayMillis"] || (isDevelopment ? 1e3 : 1e4), exportTimeoutMillis: this.config.exportTimeoutMillis || 3e4, maxQueueSize: this.config.maxQueueSize || 1e3 }); const originalOnStart = processor.onStart; const originalOnEnd = processor.onEnd; processor.onStart = function(span, parentContext) { console.log(`\u{1F525} [DEBUG] Span started: ${span.name} (${span.spanContext().spanId})`); return originalOnStart.call(this, span, parentContext); }; processor.onEnd = function(span) { console.log(`\u{1F3C1} [DEBUG] Span ended: ${span.name} (${span.spanContext().spanId})`); return originalOnEnd.call(this, span); }; } return processor; } /** * Validate the configuration. */ validateConfig() { if (!this.config.secretKey) { throw new Error("Langfuse secret key is required"); } if (!this.config.publicKey) { throw new Error("Langfuse public key is required"); } const isDevelopment = this.config.forceDevExport || process.env.NODE_ENV === "development"; this.config.baseUrl = this.config.baseUrl || "https://cloud.langfuse.com"; this.config.debug = this.config.debug || false; if (isDevelopment) { this.config.flushAt = this.config.flushAt ?? 1; this.config.flushInterval = this.config.flushInterval ?? 1e3; } else { this.config.flushAt = this.config.flushAt ?? 20; this.config.flushInterval = this.config.flushInterval ?? 1e4; } this.config.exportTimeoutMillis = this.config.exportTimeoutMillis ?? 3e4; this.config.maxQueueSize = this.config.maxQueueSize ?? 1e3; } /** * Get the Langfuse SDK version. */ getLangfuseVersion() { try { const packageJson = __require("langfuse/package.json"); return packageJson.version; } catch (error) { return "unknown"; } } }; // src/index.ts function langfuse(config) { return genkitPlugin("langfuse", async () => { if (config.debug) { console.log("\u{1F527} [DEBUG] Initializing Langfuse plugin"); } const telemetryProvider = new LangfuseTelemetryProvider(config); await enableTelemetry(telemetryProvider.getConfig()); if (config.debug) { console.log("\u2705 [DEBUG] Langfuse plugin initialization complete"); } }); } async function enableLangfuseTelemetry(config) { console.warn("enableLangfuseTelemetry is deprecated. Use langfuse() plugin instead."); const telemetryProvider = new LangfuseTelemetryProvider(config); const { enableTelemetry: enableTelemetry2 } = await import("genkit/tracing"); return enableTelemetry2(telemetryProvider.getConfig()); } function createLangfuseTelemetryProvider(config) { return new LangfuseTelemetryProvider(config); } export { LangfuseExporter, LangfuseTelemetryProvider, SpanMetadataExtractor, createLangfuseTelemetryProvider, enableLangfuseTelemetry, langfuse }; //# sourceMappingURL=index.mjs.map