@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
  3. package/dist/adapters/video/videoAnalyzer.js +4 -0
  4. package/dist/autoresearch/index.d.ts +1 -0
  5. package/dist/autoresearch/index.js +4 -0
  6. package/dist/autoresearch/worker.js +4 -0
  7. package/dist/browser/neurolink.min.js +467 -467
  8. package/dist/cli/commands/observability.js +11 -4
  9. package/dist/cli/commands/telemetry.js +36 -6
  10. package/dist/cli/utils/formatters.d.ts +17 -0
  11. package/dist/cli/utils/formatters.js +39 -0
  12. package/dist/constants/langfuse.d.ts +17 -1
  13. package/dist/constants/langfuse.js +23 -1
  14. package/dist/constants/telemetry.d.ts +6 -0
  15. package/dist/constants/telemetry.js +6 -0
  16. package/dist/core/baseProvider.js +2 -0
  17. package/dist/core/geminiLoopAdapter.js +19 -0
  18. package/dist/core/loopEngine.js +10 -7
  19. package/dist/core/modules/StreamHandler.js +5 -4
  20. package/dist/core/modules/TelemetryHandler.d.ts +5 -1
  21. package/dist/core/modules/TelemetryHandler.js +16 -0
  22. package/dist/core/modules/ToolsManager.js +3 -0
  23. package/dist/core/nativeGenerateLoop.js +1 -1
  24. package/dist/evaluation/index.d.ts +1 -0
  25. package/dist/evaluation/index.js +4 -0
  26. package/dist/evaluation/ragasEvaluator.js +1 -0
  27. package/dist/mcp/externalServerManager.js +6 -2
  28. package/dist/mcp/toolRegistry.js +3 -0
  29. package/dist/neurolink.d.ts +16 -8
  30. package/dist/neurolink.js +115 -61
  31. package/dist/observability/FEATURE-STATUS.md +0 -4
  32. package/dist/observability/exporterRegistry.d.ts +6 -1
  33. package/dist/observability/exporterRegistry.js +18 -0
  34. package/dist/observability/exporterSpanProcessor.d.ts +20 -6
  35. package/dist/observability/exporterSpanProcessor.js +166 -34
  36. package/dist/observability/exporters/baseExporter.d.ts +22 -3
  37. package/dist/observability/exporters/baseExporter.js +87 -5
  38. package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
  39. package/dist/observability/exporters/langfuseExporter.js +251 -41
  40. package/dist/observability/langfuseMasking.d.ts +1 -1
  41. package/dist/observability/langfuseMasking.js +1 -1
  42. package/dist/observability/otlpEncoding.d.ts +18 -5
  43. package/dist/observability/otlpEncoding.js +69 -12
  44. package/dist/observability/utils/spanSerializer.d.ts +3 -2
  45. package/dist/observability/utils/spanSerializer.js +5 -3
  46. package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
  47. package/dist/processors/base/BaseFileProcessor.js +4 -0
  48. package/dist/processors/index.d.ts +1 -0
  49. package/dist/processors/index.js +4 -0
  50. package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
  51. package/dist/processors/registry/ProcessorRegistry.js +5 -0
  52. package/dist/providers/amazonBedrock/client.js +7 -5
  53. package/dist/providers/amazonSagemaker.js +32 -12
  54. package/dist/providers/anthropic/client.js +1 -0
  55. package/dist/providers/anthropic/loopAdapter.js +7 -3
  56. package/dist/providers/googleAiStudio/client.js +2 -0
  57. package/dist/providers/googleVertex/client.d.ts +6 -5
  58. package/dist/providers/googleVertex/client.js +46 -19
  59. package/dist/providers/openaiChatCompletionsBase.js +4 -4
  60. package/dist/proxy/codexUsage.js +59 -2
  61. package/dist/proxy/proxyTracer.d.ts +25 -1
  62. package/dist/proxy/proxyTracer.js +146 -22
  63. package/dist/proxy/proxyTranslationEngine.js +4 -2
  64. package/dist/proxy/sseInterceptor.js +6 -4
  65. package/dist/rag/chunkers/BaseChunker.js +1 -0
  66. package/dist/rag/index.d.ts +1 -0
  67. package/dist/rag/index.js +4 -0
  68. package/dist/rag/ragIntegration.js +1 -0
  69. package/dist/rag/reranker/reranker.js +2 -0
  70. package/dist/rag/retrieval/vectorQueryTool.js +1 -0
  71. package/dist/server/routes/codexProxyRoutes.js +3 -1
  72. package/dist/server/utils/validation.d.ts +3 -3
  73. package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
  74. package/dist/services/server/ai/observability/instrumentation.js +217 -47
  75. package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
  76. package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
  77. package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
  78. package/dist/services/server/ai/observability/otlpSetup.js +55 -4
  79. package/dist/telemetry/aiMetrics.d.ts +56 -0
  80. package/dist/telemetry/aiMetrics.js +229 -0
  81. package/dist/telemetry/attributes.d.ts +24 -1
  82. package/dist/telemetry/attributes.js +94 -9
  83. package/dist/telemetry/chatMessages.d.ts +6 -0
  84. package/dist/telemetry/chatMessages.js +37 -0
  85. package/dist/telemetry/index.d.ts +1 -1
  86. package/dist/telemetry/index.js +1 -1
  87. package/dist/telemetry/observation.d.ts +5 -5
  88. package/dist/telemetry/observation.js +128 -39
  89. package/dist/telemetry/spanSanitizer.d.ts +2 -2
  90. package/dist/telemetry/spanSanitizer.js +76 -8
  91. package/dist/telemetry/telemetryStarter.d.ts +16 -0
  92. package/dist/telemetry/telemetryStarter.js +28 -0
  93. package/dist/telemetry/traceContext.d.ts +3 -3
  94. package/dist/telemetry/traceContext.js +3 -3
  95. package/dist/telemetry/tracers.d.ts +13 -1
  96. package/dist/telemetry/tracers.js +49 -10
  97. package/dist/telemetry/withSpan.d.ts +19 -3
  98. package/dist/telemetry/withSpan.js +149 -20
  99. package/dist/types/exporter.d.ts +98 -3
  100. package/dist/types/livekit.d.ts +16 -1
  101. package/dist/types/loopEngine.d.ts +16 -1
  102. package/dist/types/observability.d.ts +38 -5
  103. package/dist/types/proxy.d.ts +2 -0
  104. package/dist/types/span.d.ts +26 -0
  105. package/dist/utils/timeout.js +4 -2
  106. package/dist/voice/livekit/realtimeMcpTools.js +11 -2
  107. package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
  108. package/dist/voice/livekit/realtimeMemory.js +139 -46
  109. package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
  110. package/dist/voice/livekit/turnRecorder.js +22 -9
  111. package/dist/workflow/core/ensembleExecutor.js +2 -0
  112. package/dist/workflow/core/judgeScorer.js +1 -0
  113. package/dist/workflow/core/responseConditioner.js +1 -0
  114. package/dist/workflow/core/workflowRunner.js +6 -1
  115. package/dist/workflow/index.d.ts +1 -0
  116. package/dist/workflow/index.js +4 -0
  117. package/docs-site/static/search-index.json +32 -27
  118. 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
- Object.keys(span.attributes).some((key) => key.startsWith("gen_ai.")));
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
- span.setAttributes(getPropagatedAttributesFromContext(parentContext));
91
- this.open.set(span.spanContext().spanId, span);
92
- if (this.open.size > MAX_REMEMBERED_SPANS) {
93
- const oldest = this.open.keys().next().value;
94
- if (oldest !== undefined) {
95
- this.open.delete(oldest);
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
- const { spanId } = span.spanContext();
106
- this.open.delete(spanId);
107
- const observation = isObservation(span);
108
- this.remember(spanId, span.parentSpanContext?.spanId, observation);
109
- if (!observation) {
110
- return;
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 all exporters have shut down.
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.registry.shutdownAll();
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
- remember(spanId, parentSpanId, observation) {
141
- this.ended.set(spanId, { parentSpanId, observation });
142
- if (this.ended.size > MAX_REMEMBERED_SPANS) {
143
- const oldest = this.ended.keys().next().value;
144
- if (oldest !== undefined) {
145
- this.ended.delete(oldest);
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.parentSpanContext?.spanId;
278
+ let candidate = parentIdOf(span);
151
279
  while (candidate) {
152
280
  const live = this.open.get(candidate);
153
281
  if (live) {
154
- if (isObservation(live)) {
282
+ if (isExported(live)) {
155
283
  return candidate;
156
284
  }
157
- candidate = live.parentSpanContext?.spanId;
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.observation) {
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 = baseDelay * 2 ** attempt;
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 new Promise((resolve) => setTimeout(resolve, delay));
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; failed requests are retried with backoff when the failure is transient (429, 5xx, network).
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 batching options. `environment` and `release` fall back to `LANGFUSE_TRACING_ENVIRONMENT` and `LANGFUSE_RELEASE`, and only fill spans that do not carry them already.
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
  }