@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
157 lines (156 loc) • 6.32 kB
JavaScript
import { DEFAULT_BASE_URL, DEFAULT_FLUSH_AT, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_TIMEOUT_MS, HEALTH_PATH, TRACES_PATH, } from "../../constants/langfuse.js";
import { logger } from "../../utils/logger.js";
import { chunkOtlpSpans, toOtlpAttributes, toOtlpSpan, } from "../otlpEncoding.js";
import { BaseExporter } from "./baseExporter.js";
let credentialsWarned = false;
function isRetryable(status) {
return status === 429 || status >= 500;
}
/**
* @description Exporter that delivers observation spans to Langfuse's OTLP/HTTP JSON endpoint (`POST /api/public/otel/v1/traces`), which keeps native observation types and identity on every observation. Spans are queued without blocking and sent in batches; failed requests are retried with backoff when the failure is transient (429, 5xx, network).
*/
export class LangfuseExporter extends BaseExporter {
langfuse;
authorization;
baseUrl;
policy;
inflight = new Set();
/**
* @description Creates an exporter for one Langfuse project.
* @param config {LangfuseExporterConfig} - Credentials, endpoint, environment, release, masking and batching options. `environment` and `release` fall back to `LANGFUSE_TRACING_ENVIRONMENT` and `LANGFUSE_RELEASE`, and only fill spans that do not carry them already.
*/
constructor(langfuse) {
super("langfuse", {
...langfuse,
maxBufferSize: langfuse.maxBufferSize ?? DEFAULT_FLUSH_AT,
});
this.langfuse = langfuse;
this.authorization = `Basic ${Buffer.from(`${langfuse.publicKey}:${langfuse.secretKey}`).toString("base64")}`;
this.baseUrl = (langfuse.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
this.policy = {
environment: langfuse.environment ?? process.env.LANGFUSE_TRACING_ENVIRONMENT,
release: langfuse.release ?? process.env.LANGFUSE_RELEASE,
redactIO: langfuse.redactIO ?? false,
mask: langfuse.mask,
};
}
async initialize() {
if (this.initialized) {
return;
}
if (!this.langfuse.publicKey || !this.langfuse.secretKey) {
throw new Error("Langfuse publicKey and secretKey are required");
}
this.initialized = true;
this.startFlushInterval(this.config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS);
this.flushInterval?.unref();
}
async exportSpan(span) {
this.bufferSpan(span);
return this.createSuccessResult(1, 0);
}
async exportBatch(spans) {
const startTime = Date.now();
await this.deliver(spans.map((span) => toOtlpSpan(span, this.policy)));
return this.createSuccessResult(spans.length, Date.now() - startTime);
}
async flush() {
const spans = this.buffer;
this.buffer = [];
if (spans.length > 0) {
const pending = this.exportQuietly(spans);
this.inflight.add(pending);
void pending.finally(() => this.inflight.delete(pending));
}
await Promise.all(this.inflight);
}
async exportQuietly(spans) {
try {
await this.exportBatch(spans);
}
catch (error) {
logger.warn("[langfuse] Export failed", {
error: error instanceof Error ? error.message : String(error),
});
}
}
async shutdown() {
await this.flush();
this.stopFlushInterval();
this.initialized = false;
}
async healthCheck() {
try {
await this.withRetry(() => this.ping(), "health check");
return this.createHealthStatus(true);
}
catch {
return this.createHealthStatus(false, ["Health check failed"]);
}
}
async ping() {
const response = await fetch(`${this.baseUrl}${HEALTH_PATH}`, {
headers: { Authorization: this.authorization },
signal: AbortSignal.timeout(this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`Langfuse API unreachable: ${response.status}`);
}
}
async deliver(spans) {
for (const chunk of chunkOtlpSpans(spans)) {
try {
await this.withRetry(() => this.post(chunk), "export");
}
catch (error) {
logger.warn(`[langfuse] Dropped ${chunk.length} spans`, {
error: error instanceof Error ? error.message : String(error),
});
}
}
}
async post(spans) {
const response = await fetch(`${this.baseUrl}${TRACES_PATH}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: this.authorization,
"x-langfuse-sdk-name": "javascript",
"x-langfuse-ingestion-version": "4",
"x-langfuse-public-key": this.langfuse.publicKey,
},
body: JSON.stringify({
resourceSpans: [
{
resource: {
attributes: toOtlpAttributes({ "service.name": "service" }),
},
scopeSpans: [{ scope: { name: "sdk" }, spans }],
},
],
}),
signal: AbortSignal.timeout(this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS),
});
if (response.status === 401 || response.status === 403) {
if (!credentialsWarned) {
credentialsWarned = true;
logger.warn("[langfuse] Langfuse credentials rejected");
}
return;
}
if (isRetryable(response.status)) {
throw new Error(`Langfuse API error: ${response.status}`);
}
if (!response.ok) {
logger.warn(`[langfuse] Export rejected with ${response.status}`);
return;
}
const body = (await response.json().catch(() => ({})));
const rejected = Number(body.partialSuccess?.rejectedSpans ?? 0);
if (rejected > 0) {
logger.warn(`[langfuse] ${rejected} spans rejected`, {
message: body.partialSuccess?.errorMessage,
});
}
}
}