|
| 1 | +import { getEnv, getGlobalLogger } from "@langfuse/core"; |
| 2 | +import { ExportResultCode } from "@opentelemetry/core"; |
| 3 | +import { JsonTraceSerializer } from "@opentelemetry/otlp-transformer"; |
| 4 | +import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base"; |
| 5 | + |
| 6 | +export const DEFAULT_MAX_BATCH_SIZE_BYTES = 64 * 1024 * 1024; |
| 7 | + |
| 8 | +export function getSerializedBatchSizeBytes( |
| 9 | + spans: ReadableSpan[], |
| 10 | +): number | undefined { |
| 11 | + return JsonTraceSerializer.serializeRequest(spans)?.byteLength; |
| 12 | +} |
| 13 | + |
| 14 | +export function resolveMaxBatchSizeBytes(rawValue: string | undefined): number { |
| 15 | + if (rawValue === undefined) return DEFAULT_MAX_BATCH_SIZE_BYTES; |
| 16 | + |
| 17 | + const normalizedValue = rawValue.trim(); |
| 18 | + if (!/^\d+$/.test(normalizedValue)) { |
| 19 | + getGlobalLogger().warn( |
| 20 | + "Invalid LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES. Using the default limit.", |
| 21 | + { defaultMaxBatchSizeBytes: DEFAULT_MAX_BATCH_SIZE_BYTES }, |
| 22 | + ); |
| 23 | + return DEFAULT_MAX_BATCH_SIZE_BYTES; |
| 24 | + } |
| 25 | + |
| 26 | + const parsedValue = Number(normalizedValue); |
| 27 | + if (!Number.isSafeInteger(parsedValue) || parsedValue <= 0) { |
| 28 | + getGlobalLogger().warn( |
| 29 | + "Invalid LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES. Using the default limit.", |
| 30 | + { defaultMaxBatchSizeBytes: DEFAULT_MAX_BATCH_SIZE_BYTES }, |
| 31 | + ); |
| 32 | + return DEFAULT_MAX_BATCH_SIZE_BYTES; |
| 33 | + } |
| 34 | + |
| 35 | + return parsedValue; |
| 36 | +} |
| 37 | + |
| 38 | +export function resolveMaxBatchSizeBytesFromEnvironment(): number { |
| 39 | + const processValue = |
| 40 | + typeof process !== "undefined" |
| 41 | + ? process.env.LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES |
| 42 | + : undefined; |
| 43 | + |
| 44 | + return resolveMaxBatchSizeBytes( |
| 45 | + processValue !== undefined |
| 46 | + ? processValue |
| 47 | + : getEnv("LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES"), |
| 48 | + ); |
| 49 | +} |
| 50 | + |
| 51 | +export class SizeLimitedSpanExporter implements SpanExporter { |
| 52 | + private readonly delegate: SpanExporter; |
| 53 | + private readonly maxBatchSizeBytes: number; |
| 54 | + private readonly serializeRequest: ( |
| 55 | + spans: ReadableSpan[], |
| 56 | + ) => { byteLength: number } | undefined; |
| 57 | + |
| 58 | + constructor(params: { |
| 59 | + delegate: SpanExporter; |
| 60 | + maxBatchSizeBytes: number; |
| 61 | + serializeRequest?: ( |
| 62 | + spans: ReadableSpan[], |
| 63 | + ) => { byteLength: number } | undefined; |
| 64 | + }) { |
| 65 | + this.delegate = params.delegate; |
| 66 | + this.maxBatchSizeBytes = params.maxBatchSizeBytes; |
| 67 | + this.serializeRequest = |
| 68 | + params.serializeRequest ?? JsonTraceSerializer.serializeRequest; |
| 69 | + } |
| 70 | + |
| 71 | + export( |
| 72 | + spans: ReadableSpan[], |
| 73 | + resultCallback: Parameters<SpanExporter["export"]>[1], |
| 74 | + ): void { |
| 75 | + let serializedSizeBytes: number | undefined; |
| 76 | + |
| 77 | + try { |
| 78 | + serializedSizeBytes = this.serializeRequest(spans)?.byteLength; |
| 79 | + } catch (error) { |
| 80 | + resultCallback({ |
| 81 | + code: ExportResultCode.FAILED, |
| 82 | + error: error instanceof Error ? error : new Error(String(error)), |
| 83 | + }); |
| 84 | + return; |
| 85 | + } |
| 86 | + |
| 87 | + if (serializedSizeBytes === undefined) { |
| 88 | + resultCallback({ |
| 89 | + code: ExportResultCode.FAILED, |
| 90 | + error: new Error("Failed to serialize OpenTelemetry span batch."), |
| 91 | + }); |
| 92 | + return; |
| 93 | + } |
| 94 | + |
| 95 | + if (serializedSizeBytes > this.maxBatchSizeBytes) { |
| 96 | + getGlobalLogger().warn( |
| 97 | + "Dropping OpenTelemetry span batch because its serialized request exceeds the configured byte limit.", |
| 98 | + { |
| 99 | + maxBatchSizeBytes: this.maxBatchSizeBytes, |
| 100 | + serializedSizeBytes, |
| 101 | + spanCount: spans.length, |
| 102 | + }, |
| 103 | + ); |
| 104 | + resultCallback({ |
| 105 | + code: ExportResultCode.FAILED, |
| 106 | + error: new Error( |
| 107 | + `Serialized OpenTelemetry span batch size ${serializedSizeBytes} bytes exceeds the configured limit of ${this.maxBatchSizeBytes} bytes.`, |
| 108 | + ), |
| 109 | + }); |
| 110 | + return; |
| 111 | + } |
| 112 | + |
| 113 | + this.delegate.export(spans, resultCallback); |
| 114 | + } |
| 115 | + |
| 116 | + forceFlush(): Promise<void> { |
| 117 | + return this.delegate.forceFlush?.() ?? Promise.resolve(); |
| 118 | + } |
| 119 | + |
| 120 | + shutdown(): Promise<void> { |
| 121 | + return this.delegate.shutdown(); |
| 122 | + } |
| 123 | +} |
0 commit comments