@juspay/neurolink 13.5.0 → 13.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -3
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
- package/dist/adapters/video/videoAnalyzer.js +4 -0
- package/dist/autoresearch/index.d.ts +1 -0
- package/dist/autoresearch/index.js +4 -0
- package/dist/autoresearch/worker.js +4 -0
- package/dist/browser/neurolink.min.js +467 -467
- package/dist/cli/commands/observability.js +11 -4
- package/dist/cli/commands/telemetry.js +36 -6
- package/dist/cli/utils/formatters.d.ts +17 -0
- package/dist/cli/utils/formatters.js +39 -0
- package/dist/constants/langfuse.d.ts +17 -1
- package/dist/constants/langfuse.js +23 -1
- package/dist/constants/telemetry.d.ts +6 -0
- package/dist/constants/telemetry.js +6 -0
- package/dist/core/baseProvider.js +2 -0
- package/dist/core/geminiLoopAdapter.js +19 -0
- package/dist/core/loopEngine.js +10 -7
- package/dist/core/modules/StreamHandler.js +5 -4
- package/dist/core/modules/TelemetryHandler.d.ts +5 -1
- package/dist/core/modules/TelemetryHandler.js +16 -0
- package/dist/core/modules/ToolsManager.js +3 -0
- package/dist/core/nativeGenerateLoop.js +1 -1
- package/dist/evaluation/index.d.ts +1 -0
- package/dist/evaluation/index.js +4 -0
- package/dist/evaluation/ragasEvaluator.js +1 -0
- package/dist/mcp/externalServerManager.js +6 -2
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.d.ts +16 -8
- package/dist/neurolink.js +115 -61
- package/dist/observability/FEATURE-STATUS.md +0 -4
- package/dist/observability/exporterRegistry.d.ts +6 -1
- package/dist/observability/exporterRegistry.js +18 -0
- package/dist/observability/exporterSpanProcessor.d.ts +20 -6
- package/dist/observability/exporterSpanProcessor.js +166 -34
- package/dist/observability/exporters/baseExporter.d.ts +22 -3
- package/dist/observability/exporters/baseExporter.js +87 -5
- package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
- package/dist/observability/exporters/langfuseExporter.js +251 -41
- package/dist/observability/langfuseMasking.d.ts +1 -1
- package/dist/observability/langfuseMasking.js +1 -1
- package/dist/observability/otlpEncoding.d.ts +18 -5
- package/dist/observability/otlpEncoding.js +69 -12
- package/dist/observability/utils/spanSerializer.d.ts +3 -2
- package/dist/observability/utils/spanSerializer.js +5 -3
- package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
- package/dist/processors/base/BaseFileProcessor.js +4 -0
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +4 -0
- package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
- package/dist/processors/registry/ProcessorRegistry.js +5 -0
- package/dist/providers/amazonBedrock/client.js +7 -5
- package/dist/providers/amazonSagemaker.js +32 -12
- package/dist/providers/anthropic/client.js +1 -0
- package/dist/providers/anthropic/loopAdapter.js +7 -3
- package/dist/providers/googleAiStudio/client.js +2 -0
- package/dist/providers/googleVertex/client.d.ts +6 -5
- package/dist/providers/googleVertex/client.js +46 -19
- package/dist/providers/openaiChatCompletionsBase.js +4 -4
- package/dist/proxy/codexUsage.js +59 -2
- package/dist/proxy/proxyTracer.d.ts +25 -1
- package/dist/proxy/proxyTracer.js +146 -22
- package/dist/proxy/proxyTranslationEngine.js +4 -2
- package/dist/proxy/sseInterceptor.js +6 -4
- package/dist/rag/chunkers/BaseChunker.js +1 -0
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +4 -0
- package/dist/rag/ragIntegration.js +1 -0
- package/dist/rag/reranker/reranker.js +2 -0
- package/dist/rag/retrieval/vectorQueryTool.js +1 -0
- package/dist/server/routes/codexProxyRoutes.js +3 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
- package/dist/services/server/ai/observability/instrumentation.js +217 -47
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
- package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
- package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
- package/dist/services/server/ai/observability/otlpSetup.js +55 -4
- package/dist/telemetry/aiMetrics.d.ts +56 -0
- package/dist/telemetry/aiMetrics.js +229 -0
- package/dist/telemetry/attributes.d.ts +24 -1
- package/dist/telemetry/attributes.js +94 -9
- package/dist/telemetry/chatMessages.d.ts +6 -0
- package/dist/telemetry/chatMessages.js +37 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/telemetry/observation.d.ts +5 -5
- package/dist/telemetry/observation.js +128 -39
- package/dist/telemetry/spanSanitizer.d.ts +2 -2
- package/dist/telemetry/spanSanitizer.js +76 -8
- package/dist/telemetry/telemetryStarter.d.ts +16 -0
- package/dist/telemetry/telemetryStarter.js +28 -0
- package/dist/telemetry/traceContext.d.ts +3 -3
- package/dist/telemetry/traceContext.js +3 -3
- package/dist/telemetry/tracers.d.ts +13 -1
- package/dist/telemetry/tracers.js +49 -10
- package/dist/telemetry/withSpan.d.ts +19 -3
- package/dist/telemetry/withSpan.js +149 -20
- package/dist/types/exporter.d.ts +98 -3
- package/dist/types/livekit.d.ts +16 -1
- package/dist/types/loopEngine.d.ts +16 -1
- package/dist/types/observability.d.ts +38 -5
- package/dist/types/proxy.d.ts +2 -0
- package/dist/types/span.d.ts +26 -0
- package/dist/utils/timeout.js +4 -2
- package/dist/voice/livekit/realtimeMcpTools.js +11 -2
- package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
- package/dist/voice/livekit/realtimeMemory.js +139 -46
- package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
- package/dist/voice/livekit/turnRecorder.js +22 -9
- package/dist/workflow/core/ensembleExecutor.js +2 -0
- package/dist/workflow/core/judgeScorer.js +1 -0
- package/dist/workflow/core/responseConditioner.js +1 -0
- package/dist/workflow/core/workflowRunner.js +6 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +4 -0
- package/docs-site/static/search-index.json +32 -27
- package/package.json +21 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getPropagatedAttributesFromContext } from "@langfuse/core";
|
|
2
2
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
3
|
-
import { MAX_REMEMBERED_SPANS } from "../constants/telemetry.js";
|
|
3
|
+
import { INTERNAL_SPAN_ATTRIBUTE, MAX_REMEMBERED_SPANS, PROXY_TRACER_SCOPE, } from "../constants/telemetry.js";
|
|
4
4
|
import { SpanStatus, SpanType } from "../types/index.js";
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
6
|
import { getExporterRegistry } from "./exporterRegistry.js";
|
|
@@ -15,10 +15,36 @@ const OBSERVATION_SPAN_TYPES = {
|
|
|
15
15
|
evaluator: SpanType.EVALUATION,
|
|
16
16
|
embedding: SpanType.EMBEDDING,
|
|
17
17
|
};
|
|
18
|
+
// OpenTelemetry SDK 1.x spans expose `parentSpanId` and `instrumentationLibrary`;
|
|
19
|
+
// 2.x replaced them with `parentSpanContext` and `instrumentationScope`.
|
|
20
|
+
function legacy(span) {
|
|
21
|
+
return span;
|
|
22
|
+
}
|
|
23
|
+
function parentIdOf(span) {
|
|
24
|
+
return span.parentSpanContext?.spanId ?? legacy(span).parentSpanId;
|
|
25
|
+
}
|
|
26
|
+
function scopeOf(span) {
|
|
27
|
+
const scope = span.instrumentationScope ?? legacy(span).instrumentationLibrary;
|
|
28
|
+
return (scope && {
|
|
29
|
+
name: scope.name,
|
|
30
|
+
...(scope.version && { version: scope.version }),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
18
33
|
function isObservation(span) {
|
|
19
34
|
return (span.attributes[LANGFUSE_OBSERVATION_TYPE] !== undefined ||
|
|
20
35
|
span.attributes[GEN_AI_OBSERVATION_TYPE] !== undefined ||
|
|
21
|
-
|
|
36
|
+
// The proxy writes gen_ai.* keys on its spans for collectors; of those
|
|
37
|
+
// spans only the ones it marks with observe() are observations.
|
|
38
|
+
(scopeOf(span)?.name !== PROXY_TRACER_SCOPE &&
|
|
39
|
+
Object.keys(span.attributes).some((key) => key.startsWith("gen_ai."))));
|
|
40
|
+
}
|
|
41
|
+
// Only observations are exported, whatever tracer scope they come from: a
|
|
42
|
+
// scope name is not NeuroLink's alone (a host can create a tracer called
|
|
43
|
+
// "http" or "rag"), and the helper spans of NeuroLink's own tracers (http,
|
|
44
|
+
// mcp, memory, file and the rest) were never sent. Wrapper spans marked
|
|
45
|
+
// internal are never exported, observation or not.
|
|
46
|
+
function isExported(span) {
|
|
47
|
+
return (span.attributes[INTERNAL_SPAN_ATTRIBUTE] !== true && isObservation(span));
|
|
22
48
|
}
|
|
23
49
|
function spanTypeOf(span) {
|
|
24
50
|
const observationType = String(span.attributes[LANGFUSE_OBSERVATION_TYPE] ?? "").toLowerCase();
|
|
@@ -40,8 +66,12 @@ function toMillis(time) {
|
|
|
40
66
|
function toIso(time) {
|
|
41
67
|
return new Date(toMillis(time)).toISOString();
|
|
42
68
|
}
|
|
69
|
+
function toNanos(time) {
|
|
70
|
+
return (BigInt(time[0]) * 1000000000n + BigInt(time[1])).toString();
|
|
71
|
+
}
|
|
43
72
|
function toSpanData(span, parentSpanId) {
|
|
44
73
|
const { traceId, spanId } = span.spanContext();
|
|
74
|
+
const scope = scopeOf(span);
|
|
45
75
|
return {
|
|
46
76
|
spanId,
|
|
47
77
|
traceId,
|
|
@@ -50,13 +80,21 @@ function toSpanData(span, parentSpanId) {
|
|
|
50
80
|
name: span.name,
|
|
51
81
|
startTime: toIso(span.startTime),
|
|
52
82
|
endTime: toIso(span.endTime),
|
|
83
|
+
startTimeUnixNano: toNanos(span.startTime),
|
|
84
|
+
endTimeUnixNano: toNanos(span.endTime),
|
|
53
85
|
durationMs: toMillis(span.duration),
|
|
86
|
+
kind: span.kind,
|
|
87
|
+
...(span.resource?.attributes && {
|
|
88
|
+
resource: { ...span.resource.attributes },
|
|
89
|
+
}),
|
|
90
|
+
...(scope && { scope }),
|
|
54
91
|
status: STATUS_BY_CODE[span.status.code],
|
|
55
92
|
...(span.status.message && { statusMessage: span.status.message }),
|
|
56
93
|
attributes: { ...span.attributes },
|
|
57
94
|
events: span.events.map((event) => ({
|
|
58
95
|
name: event.name,
|
|
59
96
|
timestamp: toIso(event.time),
|
|
97
|
+
timestampUnixNano: toNanos(event.time),
|
|
60
98
|
...(event.attributes && { attributes: { ...event.attributes } }),
|
|
61
99
|
})),
|
|
62
100
|
links: span.links.map((link) => ({
|
|
@@ -67,18 +105,27 @@ function toSpanData(span, parentSpanId) {
|
|
|
67
105
|
};
|
|
68
106
|
}
|
|
69
107
|
/**
|
|
70
|
-
* @description OpenTelemetry span processor that turns every observation span into `SpanData` and hands it to an `ExporterRegistry`, so one instrumentation feeds Langfuse, Datadog, Sentry and every other registered exporter. Spans that are not observations are skipped and their children are re-parented onto the nearest exported ancestor. Delivery never blocks the span's thread.
|
|
108
|
+
* @description OpenTelemetry span processor that turns every observation span into `SpanData` and hands it to an `ExporterRegistry`, so one instrumentation feeds Langfuse, Datadog, Sentry and every other registered exporter. Spans that are not observations (health checks, auth, middleware, NeuroLink's helper spans), wrapper spans marked `langfuse.internal` and spans from other libraries are skipped and their children are re-parented onto the nearest exported ancestor. `shutdown()` shuts the registry's exporters down, as it always did, unless the processor was created to leave them running. Parent ids are read from OpenTelemetry SDK 2.x (`parentSpanContext`) and 1.x (`parentSpanId`) spans alike. At most `MAX_REMEMBERED_SPANS` open and ended spans are remembered for re-parenting; an ancestor of a span that is still open is kept past that limit. Delivery never blocks the span's thread, and a span that cannot be converted is skipped without throwing into the code that ended it.
|
|
71
109
|
*/
|
|
72
110
|
export class ExporterSpanProcessor {
|
|
73
111
|
registry;
|
|
112
|
+
shutsDownExporters;
|
|
74
113
|
open = new Map();
|
|
75
114
|
ended = new Map();
|
|
115
|
+
// Holds per span id: one for each child that is open or still holds its own
|
|
116
|
+
// children. An ended span with holds stays resolvable, and keeps its parent
|
|
117
|
+
// held, until the last open descendant ends, however many unrelated spans
|
|
118
|
+
// end in between.
|
|
119
|
+
pins = new Map();
|
|
120
|
+
evictionLogged = false;
|
|
76
121
|
/**
|
|
77
122
|
* @description Creates a processor bound to a registry.
|
|
78
123
|
* @param registry {ExporterRegistry} - Registry that receives every exported span.
|
|
124
|
+
* @param shutsDownExporters {boolean} - Whether `shutdown()` also shuts down the registry's exporters; defaults to `true`. Pass `false` for a host provider that is shut down and rebuilt while the exporters stay shared.
|
|
79
125
|
*/
|
|
80
|
-
constructor(registry) {
|
|
126
|
+
constructor(registry, shutsDownExporters = true) {
|
|
81
127
|
this.registry = registry;
|
|
128
|
+
this.shutsDownExporters = shutsDownExporters;
|
|
82
129
|
}
|
|
83
130
|
/**
|
|
84
131
|
* @description Copies the propagated Langfuse trace attributes (user, session, trace name, tags, metadata) from the parent context onto the new span and remembers it until it ends.
|
|
@@ -87,14 +134,17 @@ export class ExporterSpanProcessor {
|
|
|
87
134
|
* @returns {void} Nothing.
|
|
88
135
|
*/
|
|
89
136
|
onStart(span, parentContext) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
this.
|
|
137
|
+
try {
|
|
138
|
+
span.setAttributes(getPropagatedAttributesFromContext(parentContext));
|
|
139
|
+
this.open.set(span.spanContext().spanId, span);
|
|
140
|
+
this.hold(parentIdOf(span));
|
|
141
|
+
if (this.open.size > MAX_REMEMBERED_SPANS) {
|
|
142
|
+
this.evictOldestOpen();
|
|
96
143
|
}
|
|
97
144
|
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
this.contain("start", span.name, error);
|
|
147
|
+
}
|
|
98
148
|
}
|
|
99
149
|
/**
|
|
100
150
|
* @description Exports an ended observation span without waiting for the exporters.
|
|
@@ -102,15 +152,26 @@ export class ExporterSpanProcessor {
|
|
|
102
152
|
* @returns {void} Nothing.
|
|
103
153
|
*/
|
|
104
154
|
onEnd(span) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
155
|
+
try {
|
|
156
|
+
const { spanId } = span.spanContext();
|
|
157
|
+
const exported = isExported(span);
|
|
158
|
+
// Resolved before this span lets go of its ancestors, which the next
|
|
159
|
+
// trim may then drop.
|
|
160
|
+
const parentSpanId = exported
|
|
161
|
+
? this.nearestExportedAncestor(span)
|
|
162
|
+
: undefined;
|
|
163
|
+
if (this.open.delete(spanId) && !this.pins.has(spanId)) {
|
|
164
|
+
this.release(parentIdOf(span));
|
|
165
|
+
}
|
|
166
|
+
this.remember(spanId, parentIdOf(span), exported);
|
|
167
|
+
if (!exported) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
void this.exportQuietly(toSpanData(span, parentSpanId));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
this.contain("end", span.name, error);
|
|
111
174
|
}
|
|
112
|
-
const spanData = toSpanData(span, this.nearestExportedAncestor(span));
|
|
113
|
-
void this.exportQuietly(spanData);
|
|
114
175
|
}
|
|
115
176
|
/**
|
|
116
177
|
* @description Flushes every exporter in the registry.
|
|
@@ -120,11 +181,21 @@ export class ExporterSpanProcessor {
|
|
|
120
181
|
return this.registry.flushAll();
|
|
121
182
|
}
|
|
122
183
|
/**
|
|
123
|
-
* @description Shuts down every exporter in the registry.
|
|
124
|
-
* @returns {Promise<void>} Resolves when
|
|
184
|
+
* @description Shuts down every exporter in the registry, or only flushes them when the processor was created to leave them running.
|
|
185
|
+
* @returns {Promise<void>} Resolves when the exporters have shut down or flushed.
|
|
125
186
|
*/
|
|
126
187
|
shutdown() {
|
|
127
|
-
return this.
|
|
188
|
+
return this.shutsDownExporters
|
|
189
|
+
? this.registry.shutdownAll()
|
|
190
|
+
: this.registry.flushAll();
|
|
191
|
+
}
|
|
192
|
+
// Telemetry must not fail the code that created the span: the SDK calls
|
|
193
|
+
// these hooks from span.start() and span.end() without a guard.
|
|
194
|
+
contain(stage, spanName, error) {
|
|
195
|
+
logger.debug(`[ExporterSpanProcessor] Could not process span on ${stage}`, {
|
|
196
|
+
span: spanName,
|
|
197
|
+
error: error instanceof Error ? error.message : String(error),
|
|
198
|
+
});
|
|
128
199
|
}
|
|
129
200
|
async exportQuietly(spanData) {
|
|
130
201
|
try {
|
|
@@ -137,31 +208,88 @@ export class ExporterSpanProcessor {
|
|
|
137
208
|
});
|
|
138
209
|
}
|
|
139
210
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
211
|
+
// An open span holds its parent from its start; an ended span holds its
|
|
212
|
+
// parent while it has holds of its own. The first hold on an ended span
|
|
213
|
+
// therefore takes a hold on its parent, and so on up the chain.
|
|
214
|
+
hold(spanId) {
|
|
215
|
+
for (let current = spanId; current;) {
|
|
216
|
+
const held = this.pins.get(current) ?? 0;
|
|
217
|
+
this.pins.set(current, held + 1);
|
|
218
|
+
if (held > 0 || this.open.has(current)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
current = this.ended.get(current)?.parentSpanId;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// The last hold on an ended span releases its hold on its parent.
|
|
225
|
+
release(spanId) {
|
|
226
|
+
for (let current = spanId; current;) {
|
|
227
|
+
const remaining = (this.pins.get(current) ?? 0) - 1;
|
|
228
|
+
if (remaining > 0) {
|
|
229
|
+
this.pins.set(current, remaining);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.pins.delete(current);
|
|
233
|
+
if (this.open.has(current)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
current = this.ended.get(current)?.parentSpanId;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// The oldest open span, usually a long-lived root, moves to the ended map
|
|
240
|
+
// with what its descendants need, instead of vanishing and orphaning them.
|
|
241
|
+
evictOldestOpen() {
|
|
242
|
+
const oldest = this.open.entries().next().value;
|
|
243
|
+
if (oldest === undefined) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const [spanId, live] = oldest;
|
|
247
|
+
this.open.delete(spanId);
|
|
248
|
+
this.remember(spanId, parentIdOf(live), isExported(live));
|
|
249
|
+
if (!this.pins.has(spanId)) {
|
|
250
|
+
this.release(parentIdOf(live));
|
|
251
|
+
}
|
|
252
|
+
if (!this.evictionLogged) {
|
|
253
|
+
this.evictionLogged = true;
|
|
254
|
+
logger.debug(`[ExporterSpanProcessor] More than ${MAX_REMEMBERED_SPANS} spans are open; the oldest are remembered only for re-parenting`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
remember(spanId, parentSpanId, exported) {
|
|
258
|
+
this.ended.delete(spanId);
|
|
259
|
+
this.ended.set(spanId, { parentSpanId, exported });
|
|
260
|
+
this.trimEnded();
|
|
261
|
+
}
|
|
262
|
+
// Drops the oldest entries that no open span descends from; a pinned entry
|
|
263
|
+
// goes to the back so the next trim does not meet it first again.
|
|
264
|
+
trimEnded() {
|
|
265
|
+
for (let attempts = this.ended.size; this.ended.size > MAX_REMEMBERED_SPANS && attempts > 0; attempts -= 1) {
|
|
266
|
+
const oldest = this.ended.entries().next().value;
|
|
267
|
+
if (oldest === undefined) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const [spanId, entry] = oldest;
|
|
271
|
+
this.ended.delete(spanId);
|
|
272
|
+
if (this.pins.has(spanId)) {
|
|
273
|
+
this.ended.set(spanId, entry);
|
|
146
274
|
}
|
|
147
275
|
}
|
|
148
276
|
}
|
|
149
277
|
nearestExportedAncestor(span) {
|
|
150
|
-
let candidate = span
|
|
278
|
+
let candidate = parentIdOf(span);
|
|
151
279
|
while (candidate) {
|
|
152
280
|
const live = this.open.get(candidate);
|
|
153
281
|
if (live) {
|
|
154
|
-
if (
|
|
282
|
+
if (isExported(live)) {
|
|
155
283
|
return candidate;
|
|
156
284
|
}
|
|
157
|
-
candidate = live
|
|
285
|
+
candidate = parentIdOf(live);
|
|
158
286
|
continue;
|
|
159
287
|
}
|
|
160
288
|
const finished = this.ended.get(candidate);
|
|
161
289
|
if (!finished) {
|
|
162
290
|
return undefined;
|
|
163
291
|
}
|
|
164
|
-
if (finished.
|
|
292
|
+
if (finished.exported) {
|
|
165
293
|
return candidate;
|
|
166
294
|
}
|
|
167
295
|
candidate = finished.parentSpanId;
|
|
@@ -170,12 +298,16 @@ export class ExporterSpanProcessor {
|
|
|
170
298
|
}
|
|
171
299
|
}
|
|
172
300
|
/**
|
|
173
|
-
* @description Creates a span processor that feeds NeuroLink's observation spans to an exporter registry. Add it to a host `TracerProvider` to deliver spans through the registered exporters (such as `LangfuseExporter`) instead of `LangfuseSpanProcessor`.
|
|
301
|
+
* @description Creates a span processor that feeds NeuroLink's observation spans to an exporter registry. Add it to a host `TracerProvider` to deliver spans through the registered exporters (such as `LangfuseExporter`) instead of `LangfuseSpanProcessor`. Shutting that provider down shuts the registry's exporters down; pass `{ shutdownExporters: false }` to flush them and leave them running, so a provider the host rebuilds keeps exporting.
|
|
174
302
|
* @param registry {ExporterRegistry} - Registry to export to; defaults to the process-wide registry.
|
|
303
|
+
* @param options {ExporterSpanProcessorOptions} - `shutdownExporters` (default `true`) says whether the processor's `shutdown()` also shuts down the registry's exporters.
|
|
175
304
|
* @returns {ExporterSpanProcessor} A processor ready to be added to a TracerProvider.
|
|
176
305
|
* @example
|
|
177
306
|
* const provider = new NodeTracerProvider({ spanProcessors: [createExporterSpanProcessor()] });
|
|
307
|
+
* @example
|
|
308
|
+
* // A host provider that is shut down and rebuilt over the same exporters
|
|
309
|
+
* const processor = createExporterSpanProcessor(getExporterRegistry(), { shutdownExporters: false });
|
|
178
310
|
*/
|
|
179
|
-
export function createExporterSpanProcessor(registry = getExporterRegistry()) {
|
|
180
|
-
return new ExporterSpanProcessor(registry);
|
|
311
|
+
export function createExporterSpanProcessor(registry = getExporterRegistry(), options = {}) {
|
|
312
|
+
return new ExporterSpanProcessor(registry, options.shutdownExporters ?? true);
|
|
181
313
|
}
|
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
* Abstract base class for all observability exporters
|
|
3
3
|
* Follows NeuroLink's factory pattern with a unified exporter interface
|
|
4
4
|
*/
|
|
5
|
-
import type { ExporterConfig, ExporterHealthStatus, ExportResult, SpanData } from "../../types/index.js";
|
|
5
|
+
import type { ExporterConfig, ExporterDeliveryCounts, ExporterHealthStatus, ExporterStatus, ExportResult, SpanData } from "../../types/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Backoff of `withRetry` unless its caller passes another: one second, doubled
|
|
8
|
+
* for every attempt that failed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function exponentialRetryDelayMs(_error: unknown, attempt: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Lets an exporter that counts its own deliveries, instead of through
|
|
13
|
+
* `createSuccessResult` and `createFailureResult`, say what it counted.
|
|
14
|
+
*/
|
|
15
|
+
export declare function reportDeliveriesWith(exporter: BaseExporter, read: () => ExporterDeliveryCounts): void;
|
|
16
|
+
/**
|
|
17
|
+
* Snapshot of a registered exporter for status reporting, without a network
|
|
18
|
+
* call. It reads only what every exporter has had since the first release
|
|
19
|
+
* (`getName`, `isInitialized`, `getPendingCount`, `getLastExportTime`), so an
|
|
20
|
+
* exporter written in plain JavaScript against that set is reported too.
|
|
21
|
+
*/
|
|
22
|
+
export declare function exporterStatusOf(exporter: BaseExporter): ExporterStatus;
|
|
6
23
|
/**
|
|
7
24
|
* Abstract base class for all observability exporters
|
|
8
25
|
* Provides common functionality: buffering, flush intervals, health checks, retry logic
|
|
@@ -97,10 +114,12 @@ export declare abstract class BaseExporter {
|
|
|
97
114
|
* Execute an operation with exponential backoff retry
|
|
98
115
|
* @param operation - The async operation to execute
|
|
99
116
|
* @param operationName - Name for logging purposes
|
|
117
|
+
* @param signal - Stops further attempts and cuts a backoff wait short when aborted
|
|
118
|
+
* @param delayMs - Milliseconds to wait before the next attempt, given why the attempt failed and its zero-based number; one second doubled per attempt by default
|
|
100
119
|
* @returns The result of the operation
|
|
101
|
-
* @throws The last error if all retries fail
|
|
120
|
+
* @throws The last error if all retries fail, or the abort reason when the signal fires
|
|
102
121
|
*/
|
|
103
|
-
protected withRetry<T>(operation: () => Promise<T>, operationName: string): Promise<T>;
|
|
122
|
+
protected withRetry<T>(operation: () => Promise<T>, operationName: string, signal?: AbortSignal, delayMs?: (error: unknown, attempt: number) => number): Promise<T>;
|
|
104
123
|
}
|
|
105
124
|
/**
|
|
106
125
|
* No-op exporter for when observability is disabled
|
|
@@ -3,6 +3,56 @@
|
|
|
3
3
|
* Follows NeuroLink's factory pattern with a unified exporter interface
|
|
4
4
|
*/
|
|
5
5
|
import { logger } from "../../utils/logger.js";
|
|
6
|
+
// What each exporter delivered and dropped is kept here, not on the instance:
|
|
7
|
+
// a subclass written before these counts existed may declare a member of any
|
|
8
|
+
// name, and a plain-JavaScript exporter has no counts at all.
|
|
9
|
+
const deliveryTallies = new WeakMap();
|
|
10
|
+
const deliveryReaders = new WeakMap();
|
|
11
|
+
const NO_DELIVERIES = {
|
|
12
|
+
delivered: 0,
|
|
13
|
+
failed: 0,
|
|
14
|
+
lastError: undefined,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Backoff of `withRetry` unless its caller passes another: one second, doubled
|
|
18
|
+
* for every attempt that failed.
|
|
19
|
+
*/
|
|
20
|
+
export function exponentialRetryDelayMs(_error, attempt) {
|
|
21
|
+
return 1000 * 2 ** attempt;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Lets an exporter that counts its own deliveries, instead of through
|
|
25
|
+
* `createSuccessResult` and `createFailureResult`, say what it counted.
|
|
26
|
+
*/
|
|
27
|
+
export function reportDeliveriesWith(exporter, read) {
|
|
28
|
+
deliveryReaders.set(exporter, read);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Snapshot of a registered exporter for status reporting, without a network
|
|
32
|
+
* call. It reads only what every exporter has had since the first release
|
|
33
|
+
* (`getName`, `isInitialized`, `getPendingCount`, `getLastExportTime`), so an
|
|
34
|
+
* exporter written in plain JavaScript against that set is reported too.
|
|
35
|
+
*/
|
|
36
|
+
export function exporterStatusOf(exporter) {
|
|
37
|
+
const { delivered, failed, lastError } = (deliveryReaders.get(exporter) ??
|
|
38
|
+
(() => deliveryTallies.get(exporter) ?? NO_DELIVERIES))();
|
|
39
|
+
const initialized = exporter.isInitialized();
|
|
40
|
+
const lastExportTime = typeof exporter.getLastExportTime === "function"
|
|
41
|
+
? exporter.getLastExportTime()
|
|
42
|
+
: 0;
|
|
43
|
+
return {
|
|
44
|
+
name: exporter.getName(),
|
|
45
|
+
enabled: initialized,
|
|
46
|
+
healthy: initialized && lastError === undefined,
|
|
47
|
+
pendingSpans: exporter.getPendingCount(),
|
|
48
|
+
deliveredSpans: delivered,
|
|
49
|
+
failedSpans: failed,
|
|
50
|
+
...(lastExportTime > 0 && {
|
|
51
|
+
lastExportTime: new Date(lastExportTime).toISOString(),
|
|
52
|
+
}),
|
|
53
|
+
...(lastError !== undefined && { errors: [lastError] }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
6
56
|
/**
|
|
7
57
|
* Abstract base class for all observability exporters
|
|
8
58
|
* Provides common functionality: buffering, flush intervals, health checks, retry logic
|
|
@@ -95,6 +145,12 @@ export class BaseExporter {
|
|
|
95
145
|
*/
|
|
96
146
|
createSuccessResult(exportedCount, durationMs) {
|
|
97
147
|
this.lastExportTime = Date.now();
|
|
148
|
+
const tally = deliveryTallies.get(this) ?? NO_DELIVERIES;
|
|
149
|
+
deliveryTallies.set(this, {
|
|
150
|
+
delivered: tally.delivered + exportedCount,
|
|
151
|
+
failed: tally.failed,
|
|
152
|
+
lastError: undefined,
|
|
153
|
+
});
|
|
98
154
|
return {
|
|
99
155
|
success: true,
|
|
100
156
|
exportedCount,
|
|
@@ -106,6 +162,12 @@ export class BaseExporter {
|
|
|
106
162
|
* Create a standard export result for failure
|
|
107
163
|
*/
|
|
108
164
|
createFailureResult(spanIds, error, durationMs, retryable = true) {
|
|
165
|
+
const tally = deliveryTallies.get(this) ?? NO_DELIVERIES;
|
|
166
|
+
deliveryTallies.set(this, {
|
|
167
|
+
delivered: tally.delivered,
|
|
168
|
+
failed: tally.failed + spanIds.length,
|
|
169
|
+
lastError: error,
|
|
170
|
+
});
|
|
109
171
|
return {
|
|
110
172
|
success: false,
|
|
111
173
|
exportedCount: 0,
|
|
@@ -134,33 +196,53 @@ export class BaseExporter {
|
|
|
134
196
|
* Execute an operation with exponential backoff retry
|
|
135
197
|
* @param operation - The async operation to execute
|
|
136
198
|
* @param operationName - Name for logging purposes
|
|
199
|
+
* @param signal - Stops further attempts and cuts a backoff wait short when aborted
|
|
200
|
+
* @param delayMs - Milliseconds to wait before the next attempt, given why the attempt failed and its zero-based number; one second doubled per attempt by default
|
|
137
201
|
* @returns The result of the operation
|
|
138
|
-
* @throws The last error if all retries fail
|
|
202
|
+
* @throws The last error if all retries fail, or the abort reason when the signal fires
|
|
139
203
|
*/
|
|
140
|
-
async withRetry(operation, operationName) {
|
|
204
|
+
async withRetry(operation, operationName, signal, delayMs = exponentialRetryDelayMs) {
|
|
141
205
|
const maxRetries = this.retries;
|
|
142
|
-
const baseDelay = 1000;
|
|
143
206
|
let lastError;
|
|
144
207
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
208
|
+
signal?.throwIfAborted();
|
|
145
209
|
try {
|
|
146
210
|
return await operation();
|
|
147
211
|
}
|
|
148
212
|
catch (error) {
|
|
149
213
|
lastError = error;
|
|
214
|
+
if (signal?.aborted) {
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
150
217
|
if (attempt < maxRetries) {
|
|
151
|
-
const delay =
|
|
218
|
+
const delay = delayMs(lastError, attempt);
|
|
152
219
|
logger.warn(`[${this.name}] ${operationName} failed, retrying in ${delay}ms`, {
|
|
153
220
|
attempt: attempt + 1,
|
|
154
221
|
maxRetries,
|
|
155
222
|
error: lastError.message,
|
|
156
223
|
});
|
|
157
|
-
await
|
|
224
|
+
await sleepUnlessAborted(delay, signal);
|
|
158
225
|
}
|
|
159
226
|
}
|
|
160
227
|
}
|
|
161
228
|
throw lastError;
|
|
162
229
|
}
|
|
163
230
|
}
|
|
231
|
+
function sleepUnlessAborted(delayMs, signal) {
|
|
232
|
+
return new Promise((resolve) => {
|
|
233
|
+
if (signal?.aborted) {
|
|
234
|
+
resolve();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const finish = () => {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
signal?.removeEventListener("abort", finish);
|
|
240
|
+
resolve();
|
|
241
|
+
};
|
|
242
|
+
const timer = setTimeout(finish, delayMs);
|
|
243
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
244
|
+
});
|
|
245
|
+
}
|
|
164
246
|
/**
|
|
165
247
|
* No-op exporter for when observability is disabled
|
|
166
248
|
* Provides zero-overhead behavior
|
|
@@ -1,27 +1,53 @@
|
|
|
1
|
-
import type { ExporterHealthStatus, ExportResult, LangfuseExporterConfig, SpanData } from "../../types/index.js";
|
|
1
|
+
import type { ExporterHealthStatus, ExporterStatus, ExportResult, LangfuseExporterConfig, SpanData } from "../../types/index.js";
|
|
2
2
|
import { BaseExporter } from "./baseExporter.js";
|
|
3
3
|
/**
|
|
4
|
-
* @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;
|
|
4
|
+
* @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, with at most `MAX_CONCURRENT_DELIVERIES` requests in flight; spans that end while every one is busy wait, at most `MAX_QUEUED_SPANS` of them, and the oldest beyond that are dropped, counted and warned about once a minute. Failed requests are retried with backoff when the failure is transient (429, 5xx, network), after the `Retry-After` of a 429 or 503 when it names one (at most `MAX_EXPORT_RETRY_AFTER_MS`). `flush()` and `shutdown()` wait at most `flushBudgetMs` and then drop what is still in flight, and a batch that could not be delivered is reported by `exportBatch`, logged at warn level and counted in the health check; when Langfuse took none of the latest batch (refused, failed or every span rejected), `getStatus()` and `healthCheck()` read unhealthy with the dropped count until a batch is accepted again. `exportSpan` reports only the buffer step, so a failed delivery never opens the registry's circuit breaker and spans that end after an outage are still buffered and sent.
|
|
5
5
|
*/
|
|
6
6
|
export declare class LangfuseExporter extends BaseExporter {
|
|
7
7
|
private readonly langfuse;
|
|
8
8
|
private readonly authorization;
|
|
9
9
|
private readonly baseUrl;
|
|
10
10
|
private readonly policy;
|
|
11
|
+
private readonly flushBudgetMs;
|
|
11
12
|
private readonly inflight;
|
|
13
|
+
private delivery;
|
|
14
|
+
private droppedSpans;
|
|
15
|
+
private acceptedSpans;
|
|
16
|
+
private lastDeliveryError;
|
|
17
|
+
private unwarnedQueueDrops;
|
|
18
|
+
private lastQueueDropWarning;
|
|
12
19
|
/**
|
|
13
20
|
* @description Creates an exporter for one Langfuse project.
|
|
14
|
-
* @param config {LangfuseExporterConfig} - Credentials, endpoint, environment, release, masking and
|
|
21
|
+
* @param config {LangfuseExporterConfig} - Credentials, endpoint, environment, release, masking, batching and flush-budget options. `environment` and `release` fall back to `LANGFUSE_TRACING_ENVIRONMENT` and `LANGFUSE_RELEASE`, and only fill spans that do not carry them already.
|
|
15
22
|
*/
|
|
16
23
|
constructor(langfuse: LangfuseExporterConfig);
|
|
17
24
|
initialize(): Promise<void>;
|
|
18
25
|
exportSpan(span: SpanData): Promise<ExportResult>;
|
|
19
26
|
exportBatch(spans: SpanData[]): Promise<ExportResult>;
|
|
27
|
+
/**
|
|
28
|
+
* @description Sends everything buffered and waits for delivery, but no longer than the flush budget; spans still in flight when it runs out are dropped and counted, and spans still waiting for a delivery slot stay buffered.
|
|
29
|
+
* @returns {Promise<void>} Resolves when delivery finished or the budget ran out.
|
|
30
|
+
*/
|
|
20
31
|
flush(): Promise<void>;
|
|
32
|
+
protected bufferSpan(span: SpanData): void;
|
|
33
|
+
private dispatch;
|
|
34
|
+
private dropOldestWaiting;
|
|
35
|
+
private settle;
|
|
36
|
+
private nextDelivery;
|
|
21
37
|
private exportQuietly;
|
|
38
|
+
/**
|
|
39
|
+
* @description Delivers what is buffered, including spans that end while it drains, within the flush budget, then stops the exporter. Spans that could not be sent by then are dropped and counted, so none is left pending.
|
|
40
|
+
* @returns {Promise<void>} Resolves when the exporter has stopped.
|
|
41
|
+
*/
|
|
22
42
|
shutdown(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* @description Snapshot of this exporter for status reporting, without a network call: what is queued, what Langfuse accepted and what was dropped, and unhealthy while the latest delivery failed.
|
|
45
|
+
* @returns {ExporterStatus} The exporter's current state.
|
|
46
|
+
*/
|
|
47
|
+
getStatus(): ExporterStatus;
|
|
23
48
|
healthCheck(): Promise<ExporterHealthStatus>;
|
|
24
49
|
protected ping(): Promise<void>;
|
|
50
|
+
private record;
|
|
25
51
|
private deliver;
|
|
26
52
|
private post;
|
|
27
53
|
}
|