genkitx-langfuse
Version:
Genkit AI framework plugin for Langfuse observability and tracing.
689 lines (682 loc) • 23 kB
JavaScript
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