@juspay/neurolink 13.4.10 → 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 (145) hide show
  1. package/CHANGELOG.md +2 -3
  2. package/README.md +3 -2
  3. package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
  4. package/dist/adapters/video/videoAnalyzer.js +4 -0
  5. package/dist/autoresearch/index.d.ts +1 -0
  6. package/dist/autoresearch/index.js +4 -0
  7. package/dist/autoresearch/worker.js +4 -0
  8. package/dist/browser/neurolink.min.js +466 -466
  9. package/dist/cli/commands/decide.js +25 -5
  10. package/dist/cli/commands/observability.js +11 -4
  11. package/dist/cli/commands/telemetry.js +36 -6
  12. package/dist/cli/utils/formatters.d.ts +17 -0
  13. package/dist/cli/utils/formatters.js +39 -0
  14. package/dist/constants/langfuse.d.ts +17 -1
  15. package/dist/constants/langfuse.js +23 -1
  16. package/dist/constants/telemetry.d.ts +6 -0
  17. package/dist/constants/telemetry.js +6 -0
  18. package/dist/core/baseProvider.js +2 -0
  19. package/dist/core/geminiLoopAdapter.js +19 -0
  20. package/dist/core/loopEngine.js +10 -7
  21. package/dist/core/modules/StreamHandler.js +5 -4
  22. package/dist/core/modules/TelemetryHandler.d.ts +5 -1
  23. package/dist/core/modules/TelemetryHandler.js +16 -0
  24. package/dist/core/modules/ToolsManager.js +3 -0
  25. package/dist/core/nativeGenerateLoop.js +1 -1
  26. package/dist/evaluation/index.d.ts +1 -0
  27. package/dist/evaluation/index.js +4 -0
  28. package/dist/evaluation/ragasEvaluator.js +1 -0
  29. package/dist/factories/providerDescriptors.d.ts +4 -3
  30. package/dist/factories/providerDescriptors.js +6 -4
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/mcp/externalServerManager.js +6 -2
  34. package/dist/mcp/toolRegistry.js +3 -0
  35. package/dist/neurolink.d.ts +20 -10
  36. package/dist/neurolink.js +134 -65
  37. package/dist/observability/FEATURE-STATUS.md +0 -4
  38. package/dist/observability/exporterRegistry.d.ts +6 -1
  39. package/dist/observability/exporterRegistry.js +18 -0
  40. package/dist/observability/exporterSpanProcessor.d.ts +20 -6
  41. package/dist/observability/exporterSpanProcessor.js +166 -34
  42. package/dist/observability/exporters/baseExporter.d.ts +22 -3
  43. package/dist/observability/exporters/baseExporter.js +87 -5
  44. package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
  45. package/dist/observability/exporters/langfuseExporter.js +251 -41
  46. package/dist/observability/langfuseMasking.d.ts +1 -1
  47. package/dist/observability/langfuseMasking.js +1 -1
  48. package/dist/observability/otlpEncoding.d.ts +18 -5
  49. package/dist/observability/otlpEncoding.js +69 -12
  50. package/dist/observability/utils/spanSerializer.d.ts +3 -2
  51. package/dist/observability/utils/spanSerializer.js +5 -3
  52. package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
  53. package/dist/processors/base/BaseFileProcessor.js +4 -0
  54. package/dist/processors/index.d.ts +1 -0
  55. package/dist/processors/index.js +4 -0
  56. package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
  57. package/dist/processors/registry/ProcessorRegistry.js +5 -0
  58. package/dist/providers/amazonBedrock/client.js +7 -5
  59. package/dist/providers/amazonSagemaker.js +32 -12
  60. package/dist/providers/anthropic/client.js +1 -0
  61. package/dist/providers/anthropic/loopAdapter.js +7 -3
  62. package/dist/providers/catalog/provider-catalog.schema.json +64 -3
  63. package/dist/providers/catalog/schema.js +81 -1
  64. package/dist/providers/catalog/xor.json +30 -10
  65. package/dist/providers/configuredDecision.d.ts +8 -1
  66. package/dist/providers/configuredDecision.js +22 -0
  67. package/dist/providers/decide/dialects/openaiDecisions.js +8 -0
  68. package/dist/providers/decide/dialects/systemOne.js +28 -2
  69. package/dist/providers/decide/guards.d.ts +6 -0
  70. package/dist/providers/decide/guards.js +22 -0
  71. package/dist/providers/googleAiStudio/client.js +2 -0
  72. package/dist/providers/googleVertex/client.d.ts +6 -5
  73. package/dist/providers/googleVertex/client.js +46 -19
  74. package/dist/providers/openaiChatCompletionsBase.js +4 -4
  75. package/dist/providers/systemOneDecision.d.ts +30 -7
  76. package/dist/providers/systemOneDecision.js +71 -14
  77. package/dist/proxy/codexUsage.js +59 -2
  78. package/dist/proxy/proxyTracer.d.ts +25 -1
  79. package/dist/proxy/proxyTracer.js +146 -22
  80. package/dist/proxy/proxyTranslationEngine.js +4 -2
  81. package/dist/proxy/sseInterceptor.js +6 -4
  82. package/dist/rag/chunkers/BaseChunker.js +1 -0
  83. package/dist/rag/index.d.ts +1 -0
  84. package/dist/rag/index.js +4 -0
  85. package/dist/rag/ragIntegration.js +1 -0
  86. package/dist/rag/reranker/reranker.js +2 -0
  87. package/dist/rag/retrieval/vectorQueryTool.js +1 -0
  88. package/dist/server/routes/codexProxyRoutes.js +3 -1
  89. package/dist/server/utils/validation.d.ts +3 -3
  90. package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
  91. package/dist/services/server/ai/observability/instrumentation.js +217 -47
  92. package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
  93. package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
  94. package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
  95. package/dist/services/server/ai/observability/otlpSetup.js +55 -4
  96. package/dist/telemetry/aiMetrics.d.ts +56 -0
  97. package/dist/telemetry/aiMetrics.js +229 -0
  98. package/dist/telemetry/attributes.d.ts +24 -1
  99. package/dist/telemetry/attributes.js +94 -9
  100. package/dist/telemetry/chatMessages.d.ts +6 -0
  101. package/dist/telemetry/chatMessages.js +37 -0
  102. package/dist/telemetry/index.d.ts +1 -1
  103. package/dist/telemetry/index.js +1 -1
  104. package/dist/telemetry/observation.d.ts +5 -5
  105. package/dist/telemetry/observation.js +128 -39
  106. package/dist/telemetry/spanSanitizer.d.ts +2 -2
  107. package/dist/telemetry/spanSanitizer.js +76 -8
  108. package/dist/telemetry/telemetryStarter.d.ts +16 -0
  109. package/dist/telemetry/telemetryStarter.js +28 -0
  110. package/dist/telemetry/traceContext.d.ts +3 -3
  111. package/dist/telemetry/traceContext.js +3 -3
  112. package/dist/telemetry/tracers.d.ts +13 -1
  113. package/dist/telemetry/tracers.js +49 -10
  114. package/dist/telemetry/withSpan.d.ts +19 -3
  115. package/dist/telemetry/withSpan.js +149 -20
  116. package/dist/types/cli.d.ts +2 -0
  117. package/dist/types/decision.d.ts +117 -10
  118. package/dist/types/exporter.d.ts +98 -3
  119. package/dist/types/livekit.d.ts +16 -1
  120. package/dist/types/loopEngine.d.ts +16 -1
  121. package/dist/types/observability.d.ts +38 -5
  122. package/dist/types/providerCatalog.d.ts +12 -1
  123. package/dist/types/providers.d.ts +2 -0
  124. package/dist/types/proxy.d.ts +2 -0
  125. package/dist/types/span.d.ts +26 -0
  126. package/dist/utils/decisionAnswers.d.ts +7 -1
  127. package/dist/utils/decisionAnswers.js +43 -2
  128. package/dist/utils/decisionLimits.d.ts +13 -1
  129. package/dist/utils/decisionLimits.js +52 -1
  130. package/dist/utils/decisionMedia.d.ts +4 -2
  131. package/dist/utils/decisionMedia.js +216 -14
  132. package/dist/utils/timeout.js +4 -2
  133. package/dist/voice/livekit/realtimeMcpTools.js +11 -2
  134. package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
  135. package/dist/voice/livekit/realtimeMemory.js +139 -46
  136. package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
  137. package/dist/voice/livekit/turnRecorder.js +22 -9
  138. package/dist/workflow/core/ensembleExecutor.js +2 -0
  139. package/dist/workflow/core/judgeScorer.js +1 -0
  140. package/dist/workflow/core/responseConditioner.js +1 -0
  141. package/dist/workflow/core/workflowRunner.js +6 -1
  142. package/dist/workflow/index.d.ts +1 -0
  143. package/dist/workflow/index.js +4 -0
  144. package/docs-site/static/search-index.json +56 -48
  145. package/package.json +21 -2
@@ -1,38 +1,168 @@
1
1
  import { propagateAttributes, setLangfuseTracerProvider, } from "@langfuse/tracing";
2
- import { context, createContextKey, metrics, ProxyTracerProvider, trace, } from "@opentelemetry/api";
2
+ import { context, createContextKey, metrics, } from "@opentelemetry/api";
3
3
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
4
- import { resourceFromAttributes } from "@opentelemetry/resources";
4
+ import { resourceFromAttributes, } from "@opentelemetry/resources";
5
5
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
6
- import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
6
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
7
+ import { DEFAULT_FLUSH_BUDGET_MS } from "../../../../constants/langfuse.js";
7
8
  import { OTEL_LOG_PREFIX, TRACE_METADATA_CONTEXT_KEY, } from "../../../../constants/telemetry.js";
8
9
  import { getExporterRegistry } from "../../../../observability/exporterRegistry.js";
9
10
  import { ExporterSpanProcessor } from "../../../../observability/exporterSpanProcessor.js";
10
11
  import { LangfuseExporter } from "../../../../observability/exporters/langfuseExporter.js";
11
12
  import { spanJsonAttribute } from "../../../../telemetry/attributes.js";
13
+ import { hasHostTracerProvider, registerTelemetryStarter, } from "../../../../telemetry/telemetryStarter.js";
12
14
  import { logger } from "../../../../utils/logger.js";
13
- import { createLangfuseExporter, isLangfuseEnabled } from "./langfuseSetup.js";
14
- import { createOtlpMeterProvider, createOtlpSpanProcessor, } from "./otlpSetup.js";
15
+ import { createLangfuseExporter, isLangfuseEnabled, isOff, resolveEnvironment, resolveRelease, } from "./langfuseSetup.js";
16
+ import { createOtlpConnections, createOtlpMeterProvider, createOtlpSpanProcessor, } from "./otlpSetup.js";
15
17
  const MAX_PROPAGATED_CHARS = 200;
16
18
  const LANGFUSE_EXPORTER_NAME = "langfuse";
17
19
  const TRACE_METADATA_KEY = createContextKey(TRACE_METADATA_CONTEXT_KEY);
20
+ const CONTEXT_PROBE_KEY = createContextKey("neurolink.context-manager-probe");
21
+ // `false`, `0`, `off` or `no` skips the flush at process exit; the released build had none.
22
+ const EXIT_FLUSH_SWITCH = "NEUROLINK_TELEMETRY_EXIT_FLUSH";
18
23
  let tracerProvider = null;
24
+ let registryProcessor = null;
25
+ let otlpProcessor = null;
19
26
  let meterProvider = null;
27
+ let otlpConnections = null;
28
+ let flushBudgetMs = DEFAULT_FLUSH_BUDGET_MS;
20
29
  let langfuseExporter = null;
21
30
  let usingExternalProvider = false;
22
31
  let started = false;
32
+ let startedConfig;
23
33
  let initialized = false;
24
- let ignoredConfigLogged = false;
34
+ let ignoredConfigDebugLogged = false;
35
+ const ignoredConfigWarnings = new Set();
36
+ let exitFlushRegistered = false;
25
37
  let initializationError = null;
26
38
  let terminated = false;
27
39
  let terminationLogged = false;
28
40
  let unconfiguredLogged = false;
41
+ let missingKeysLogged = false;
29
42
  let holders = 0;
30
- function hasHostTracerProvider() {
31
- const global = trace.getTracerProvider();
32
- if (!(global instanceof ProxyTracerProvider)) {
33
- return true;
43
+ function differingConfigFields(first, later) {
44
+ const firstLangfuse = first?.langfuse;
45
+ const laterLangfuse = later.langfuse;
46
+ if ((firstLangfuse === false) !== (laterLangfuse === false)) {
47
+ return ["langfuse"];
48
+ }
49
+ const left = typeof firstLangfuse === "object" ? firstLangfuse : {};
50
+ const right = typeof laterLangfuse === "object" ? laterLangfuse : {};
51
+ return [...new Set([...Object.keys(left), ...Object.keys(right)])]
52
+ .filter((key) => left[key] !== right[key])
53
+ .sort();
54
+ }
55
+ function reportIgnoredConfig(config) {
56
+ const differing = differingConfigFields(startedConfig, config);
57
+ if (differing.length === 0) {
58
+ if (!ignoredConfigDebugLogged) {
59
+ ignoredConfigDebugLogged = true;
60
+ logger.debug(`${OTEL_LOG_PREFIX} Telemetry already started; ignoring observability config`);
61
+ }
62
+ return;
34
63
  }
35
- return global.getDelegateTracer("ai") !== undefined;
64
+ const fields = differing.join(", ");
65
+ if (ignoredConfigWarnings.has(fields)) {
66
+ return;
67
+ }
68
+ ignoredConfigWarnings.add(fields);
69
+ logger.warn(`${OTEL_LOG_PREFIX} Telemetry already started; ignoring the observability config of a later NeuroLink instance. The first config stays in effect; the ignored one differs in: ${fields}`);
70
+ }
71
+ function flushBudgetOf(config) {
72
+ const budget = typeof config?.langfuse === "object"
73
+ ? config.langfuse.flushBudgetMs
74
+ : undefined;
75
+ return budget !== undefined && budget > 0 ? budget : DEFAULT_FLUSH_BUDGET_MS;
76
+ }
77
+ function hasLangfuseKeys(config) {
78
+ const options = typeof config?.langfuse === "object" ? config.langfuse : {};
79
+ return Boolean((options.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY) &&
80
+ (options.secretKey ?? process.env.LANGFUSE_SECRET_KEY));
81
+ }
82
+ // An unreachable collector must not hold the caller past the budget. When the
83
+ // budget runs out `onExpiry` closes what the work still has open, so the export
84
+ // does not carry on to its own timeout and hold the process open.
85
+ async function withinBudget(work, budgetMs, onExpiry) {
86
+ if (!work) {
87
+ return;
88
+ }
89
+ let timer;
90
+ const expired = new Promise((_, reject) => {
91
+ timer = setTimeout(() => {
92
+ onExpiry?.();
93
+ reject(new Error(`did not finish within ${budgetMs}ms`));
94
+ }, budgetMs);
95
+ });
96
+ try {
97
+ await Promise.race([work, expired]);
98
+ }
99
+ finally {
100
+ clearTimeout(timer);
101
+ }
102
+ }
103
+ function cutOtlpConnections() {
104
+ otlpConnections?.cut();
105
+ }
106
+ function reportUnsettled(action, results) {
107
+ const errors = results.flatMap((result) => result.status === "rejected"
108
+ ? [
109
+ result.reason instanceof Error
110
+ ? result.reason.message
111
+ : String(result.reason),
112
+ ]
113
+ : []);
114
+ if (errors.length > 0) {
115
+ logger.warn(`${OTEL_LOG_PREFIX} ${action} did not complete; spans or metrics still pending may be lost`, { errors });
116
+ }
117
+ }
118
+ function flushBeforeExit() {
119
+ if (isOff(process.env[EXIT_FLUSH_SWITCH])) {
120
+ return;
121
+ }
122
+ flushOpenTelemetry().catch((error) => {
123
+ logger.debug(`${OTEL_LOG_PREFIX} Flush at process exit failed`, {
124
+ error: error instanceof Error ? error.message : String(error),
125
+ });
126
+ });
127
+ }
128
+ function registerExitFlush() {
129
+ if (exitFlushRegistered) {
130
+ return;
131
+ }
132
+ exitFlushRegistered = true;
133
+ process.once("beforeExit", flushBeforeExit);
134
+ }
135
+ // Without a registered context manager `context.with` runs the callback but
136
+ // `context.active()` stays the root context, so nothing set on a context
137
+ // (user, session, the parent span) reaches the spans created inside.
138
+ function hasContextManager() {
139
+ return context.with(context.active().setValue(CONTEXT_PROBE_KEY, true), () => context.active().getValue(CONTEXT_PROBE_KEY) === true);
140
+ }
141
+ // The OTLP traces and metrics keep the service name existing dashboards and
142
+ // alerts match on. The provider's own resource, which the registry exporters
143
+ // (Langfuse) read, is built in `initialize` with its own default.
144
+ function otlpResourceOf(config) {
145
+ const environment = resolveEnvironment(config);
146
+ return resourceFromAttributes({
147
+ [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "ai-service",
148
+ [ATTR_SERVICE_VERSION]: resolveRelease(config) || process.env.npm_package_version || "v1.0.0",
149
+ ...(environment && { "deployment.environment": environment }),
150
+ });
151
+ }
152
+ // A view over each ended span rather than a copy, so the methods and fields the
153
+ // exporter reads stay intact, with the sink's own resource on top.
154
+ function withResource(processor, resource) {
155
+ return {
156
+ onStart: (span, parentContext) => processor.onStart(span, parentContext),
157
+ onEnd: (span) => {
158
+ const view = Object.create(span, {
159
+ resource: { value: resource, enumerable: true },
160
+ });
161
+ processor.onEnd(view);
162
+ },
163
+ forceFlush: () => processor.forceFlush(),
164
+ shutdown: () => processor.shutdown(),
165
+ };
36
166
  }
37
167
  function registerLangfuseExporter(config) {
38
168
  const registry = getExporterRegistry();
@@ -47,68 +177,107 @@ function registerLangfuseExporter(config) {
47
177
  return exporter;
48
178
  }
49
179
  function initialize(config) {
50
- const langfuse = isLangfuseEnabled(config);
180
+ const langfuseRequested = isLangfuseEnabled(config);
181
+ // Without both keys there is nothing to export to, so Langfuse counts as not
182
+ // configured and a later config that has them can still start tracing.
183
+ const langfuse = langfuseRequested && hasLangfuseKeys(config);
184
+ if (langfuseRequested && !langfuse && !missingKeysLogged) {
185
+ missingKeysLogged = true;
186
+ logger.warn(`${OTEL_LOG_PREFIX} Langfuse is enabled but publicKey and secretKey are not both set in config or LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY; not exporting to Langfuse`);
187
+ }
51
188
  const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
52
- if (!langfuse && !otlpEndpoint) {
189
+ // An options object starts the pipeline even without keys, as it always did:
190
+ // exporters registered on the shared registry still receive the spans.
191
+ const optionsObject = langfuseRequested && typeof config?.langfuse === "object";
192
+ if (!langfuse && !optionsObject && !otlpEndpoint) {
53
193
  if (!unconfiguredLogged) {
54
194
  unconfiguredLogged = true;
55
195
  logger.debug(`${OTEL_LOG_PREFIX} Langfuse and OTLP not configured; skipping`);
56
196
  }
57
197
  return config?.langfuse === false;
58
198
  }
199
+ flushBudgetMs = flushBudgetOf(config);
59
200
  if (hasHostTracerProvider()) {
60
201
  usingExternalProvider = true;
61
202
  initialized = true;
62
203
  langfuseExporter = langfuse ? registerLangfuseExporter(config) : null;
63
204
  if (langfuseExporter) {
64
- logger.info(`${OTEL_LOG_PREFIX} Host TracerProvider detected; Langfuse exporter registered. Add \`createExporterSpanProcessor()\` to the host provider to export NeuroLink spans to it.`);
205
+ logger.info(`${OTEL_LOG_PREFIX} Host TracerProvider detected; Langfuse exporter registered. Add \`createExporterSpanProcessor()\` to the host provider to export NeuroLink spans to it, and register a context manager (\`NodeTracerProvider.register()\` and \`NodeSDK\` do) so user, session and parent spans reach the spans NeuroLink creates.`);
206
+ if (!hasContextManager()) {
207
+ logger.warn(`${OTEL_LOG_PREFIX} The host TracerProvider has no context manager registered: setLangfuseContext() user, session and metadata and parent/child span nesting are lost. Register one, for example with \`NodeTracerProvider.register()\`, or start the host with \`NodeSDK\`.`);
208
+ }
65
209
  }
66
210
  else {
67
211
  logger.info(`${OTEL_LOG_PREFIX} Host TracerProvider detected; NeuroLink registers no provider and no exporter.`);
68
212
  }
213
+ if (otlpEndpoint) {
214
+ logger.warn(`${OTEL_LOG_PREFIX} OTEL_EXPORTER_OTLP_ENDPOINT is ignored because a host TracerProvider is registered; NeuroLink exports no traces or metrics to it. Add an OTLP exporter to the host's own providers. NeuroLink's ai_* metrics are recorded on the host's MeterProvider.`);
215
+ }
216
+ if (langfuseExporter) {
217
+ registerExitFlush();
218
+ }
69
219
  return true;
70
220
  }
221
+ const environment = resolveEnvironment(config);
71
222
  const resource = resourceFromAttributes({
72
- [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "ai-service",
223
+ [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "neurolink",
224
+ [ATTR_SERVICE_VERSION]: resolveRelease(config) || process.env.npm_package_version || "v1.0.0",
225
+ ...(environment && { "deployment.environment": environment }),
73
226
  });
74
227
  const exporter = langfuse ? registerLangfuseExporter(config) : null;
75
- const otlpProcessor = otlpEndpoint
76
- ? createOtlpSpanProcessor(otlpEndpoint)
228
+ const otlpResource = otlpResourceOf(config);
229
+ const connections = otlpEndpoint ? createOtlpConnections() : null;
230
+ const otlpExporter = otlpEndpoint
231
+ ? createOtlpSpanProcessor(otlpEndpoint, flushBudgetMs, connections ?? undefined)
77
232
  : null;
78
- const spanProcessors = [
79
- new ExporterSpanProcessor(getExporterRegistry()),
80
- ];
81
- if (otlpProcessor) {
82
- spanProcessors.push(otlpProcessor);
233
+ const otlpSpanProcessor = otlpExporter && withResource(otlpExporter, otlpResource);
234
+ const exporterSpanProcessor = new ExporterSpanProcessor(getExporterRegistry(), true);
235
+ const spanProcessors = [exporterSpanProcessor];
236
+ if (otlpSpanProcessor) {
237
+ spanProcessors.push(otlpSpanProcessor);
83
238
  }
84
239
  const provider = new NodeTracerProvider({ resource, spanProcessors });
85
240
  provider.register({ propagator: new W3CTraceContextPropagator() });
86
241
  setLangfuseTracerProvider(provider);
87
242
  tracerProvider = provider;
243
+ registryProcessor = exporterSpanProcessor;
244
+ otlpProcessor = otlpSpanProcessor;
88
245
  langfuseExporter = exporter;
89
246
  meterProvider = otlpEndpoint
90
- ? createOtlpMeterProvider(otlpEndpoint, resource)
247
+ ? createOtlpMeterProvider(otlpEndpoint, otlpResource, flushBudgetMs, connections ?? undefined)
91
248
  : null;
249
+ otlpConnections = connections;
92
250
  usingExternalProvider = false;
93
251
  initialized = true;
252
+ registerExitFlush();
94
253
  logger.info(`${OTEL_LOG_PREFIX} Observability initialized`, {
95
254
  langfuseEnabled: exporter !== null,
96
- otlpEnabled: otlpProcessor !== null,
255
+ otlpEnabled: otlpSpanProcessor !== null,
97
256
  });
98
257
  return true;
99
258
  }
100
259
  async function stop() {
101
260
  const provider = tracerProvider;
261
+ const registry = registryProcessor;
262
+ const otlp = otlpProcessor;
102
263
  const meters = meterProvider;
264
+ const connections = otlpConnections;
103
265
  const hostExporter = usingExternalProvider ? langfuseExporter : null;
266
+ const budgetMs = flushBudgetMs;
104
267
  terminated = true;
105
268
  tracerProvider = null;
269
+ registryProcessor = null;
270
+ otlpProcessor = null;
106
271
  meterProvider = null;
272
+ otlpConnections = null;
107
273
  langfuseExporter = null;
108
274
  usingExternalProvider = false;
109
275
  started = false;
276
+ startedConfig = undefined;
110
277
  initialized = false;
111
278
  initializationError = null;
279
+ process.off("beforeExit", flushBeforeExit);
280
+ exitFlushRegistered = false;
112
281
  if (provider) {
113
282
  setLangfuseTracerProvider(null);
114
283
  }
@@ -118,23 +287,18 @@ async function stop() {
118
287
  if (hostExporter) {
119
288
  getExporterRegistry().unregister(hostExporter.getName());
120
289
  }
290
+ // The provider's own shutdown only shuts down these processors; each is shut
291
+ // down here so the OTLP one is bounded and Langfuse keeps its own budget.
121
292
  const results = await Promise.allSettled([
122
- provider?.shutdown(),
293
+ registry?.shutdown(),
294
+ withinBudget(otlp?.shutdown(), budgetMs, () => connections?.cut()),
123
295
  hostExporter?.shutdown(),
124
- meters?.shutdown(),
296
+ withinBudget(meters?.shutdown(), budgetMs, () => connections?.cut()),
125
297
  ]);
126
- for (const result of results) {
127
- if (result.status === "rejected") {
128
- logger.error(`${OTEL_LOG_PREFIX} Shutdown failed`, {
129
- error: result.reason instanceof Error
130
- ? result.reason.message
131
- : String(result.reason),
132
- });
133
- }
134
- }
298
+ reportUnsettled("Shutdown", results);
135
299
  }
136
300
  /**
137
- * @description Starts NeuroLink's tracing once per process, synchronously. Every span created through the shared tracers calls it first, so direct provider calls without a NeuroLink instance are traced too. Langfuse is enabled unless `config.langfuse` is `false`, when an options object is given or `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` are set; an OTLP exporter is added when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. When the host already registered a TracerProvider no provider is created; the Langfuse exporter, if configured, is registered on the shared exporter registry and the host adds `createExporterSpanProcessor()` to its provider. The first call that finds tracing configured decides the setup and a later `config` is ignored; calls made before anything is configured start nothing, so a later config or environment can still start tracing, unless `langfuse: false` was passed. After `shutdownOpenTelemetry` it does nothing.
301
+ * @description Starts NeuroLink's tracing once per process, synchronously. Every span created through the shared tracers calls it first once this module is loaded, as the main, server and livekit entries do, so direct provider calls without a NeuroLink instance are traced too. Langfuse is enabled unless `config.langfuse` is `false` or has `enabled: false`, when `config.langfuse` is `true` or an options object, or when `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` are set and `LANGFUSE_ENABLED` is not `false`, `0`, `off` or `no`; an OTLP exporter is added when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. When the host already registered a TracerProvider no provider is created and `OTEL_EXPORTER_OTLP_ENDPOINT` is ignored with a warning; the Langfuse exporter, if configured, is registered on the shared exporter registry and the host adds `createExporterSpanProcessor()` to its provider. The first call that finds tracing configured decides the setup and a later `config` is ignored, with a warning naming the options that differ from the first when it does differ; calls made before anything is configured start nothing, so a later config or environment can still start tracing, unless `langfuse: false` was passed. Langfuse enabled without both keys (in config or the environment) exports nothing to Langfuse, and a warning says so once. With `langfuse: true` or the environment alone that counts as not configured, so only an OTLP endpoint starts tracing; an options object starts tracing anyway, as it always did, so exporters registered on the shared registry receive the spans. Pending spans and metrics are flushed when the process runs out of work (`beforeExit`), within the same budget as `flushOpenTelemetry`, which an explicit `process.exit()` skips; `NEUROLINK_TELEMETRY_EXIT_FLUSH` set to `false`, `0`, `off` or `no` skips that flush too, and the process ends as soon as it has no work. After `shutdownOpenTelemetry` it does nothing.
138
302
  * @param config {ObservabilityConfig | undefined} - Observability options, honoured only when this is the first call.
139
303
  * @returns {void} Nothing; a failed setup is logged and recorded in `getLangfuseHealthStatus`.
140
304
  */
@@ -147,18 +311,19 @@ export function ensureTelemetry(config) {
147
311
  return;
148
312
  }
149
313
  if (started) {
150
- if (config && !ignoredConfigLogged) {
151
- ignoredConfigLogged = true;
152
- logger.debug(`${OTEL_LOG_PREFIX} Telemetry already started; ignoring observability config`);
314
+ if (config) {
315
+ reportIgnoredConfig(config);
153
316
  }
154
317
  return;
155
318
  }
156
319
  initializationError = null;
157
320
  try {
158
321
  started = initialize(config);
322
+ startedConfig = started ? config : undefined;
159
323
  }
160
324
  catch (error) {
161
325
  started = true;
326
+ startedConfig = config;
162
327
  initializationError =
163
328
  error instanceof Error ? error.message : String(error);
164
329
  logger.error(`${OTEL_LOG_PREFIX} Initialization failed`, {
@@ -166,6 +331,7 @@ export function ensureTelemetry(config) {
166
331
  });
167
332
  }
168
333
  }
334
+ registerTelemetryStarter(ensureTelemetry);
169
335
  /**
170
336
  * @description Registers a holder of the process-wide telemetry and starts it if needed. Pair every call with `releaseTelemetry`.
171
337
  * @param config {ObservabilityConfig | undefined} - Observability options forwarded to `ensureTelemetry`.
@@ -176,25 +342,28 @@ export function acquireTelemetry(config) {
176
342
  ensureTelemetry(config);
177
343
  }
178
344
  /**
179
- * @description Releases one holder registered by `acquireTelemetry` and flushes pending spans. The provider stays registered for the process so cached tracers keep working and later instances reuse it.
345
+ * @description Releases one holder registered by `acquireTelemetry` and flushes pending spans as `flushOpenTelemetry` does. The provider stays registered for the process so cached tracers keep working and later instances reuse it.
180
346
  * @returns {Promise<void>} Resolves when pending spans are flushed.
181
- * @throws {Error} When any provider fails to flush.
347
+ * @throws {Error} When any provider fails to flush or does not finish within the flush budget.
182
348
  */
183
349
  export async function releaseTelemetry() {
184
350
  holders = Math.max(0, holders - 1);
185
351
  await flushOpenTelemetry();
186
352
  }
187
353
  /**
188
- * @description Exports pending spans and metrics from the providers NeuroLink owns and flushes its Langfuse exporter. A host provider is never flushed.
354
+ * @description Exports pending spans and metrics from the providers NeuroLink owns and flushes its Langfuse exporter. A host provider is never flushed. The OTLP span and metric exports are waited for at most the flush budget (`observability.langfuse.flushBudgetMs`, default 3000 ms) and the Langfuse exporter within its own budget. A provider that fails to flush, or does not finish within the budget (a collector that cannot be reached, for one), is logged as a warning and makes the call reject once every provider has settled.
189
355
  * @returns {Promise<void>} Resolves when every owned provider has flushed.
190
- * @throws {Error} When any provider fails to flush.
356
+ * @throws {Error} When any provider fails to flush or does not finish within the flush budget.
191
357
  */
192
358
  export async function flushOpenTelemetry() {
359
+ const budgetMs = flushBudgetMs;
193
360
  const results = await Promise.allSettled([
194
- tracerProvider?.forceFlush(),
361
+ registryProcessor?.forceFlush(),
195
362
  usingExternalProvider ? langfuseExporter?.flush() : undefined,
196
- meterProvider?.forceFlush(),
363
+ withinBudget(otlpProcessor?.forceFlush(), budgetMs, cutOtlpConnections),
364
+ withinBudget(meterProvider?.forceFlush(), budgetMs, cutOtlpConnections),
197
365
  ]);
366
+ reportUnsettled("Flush", results);
198
367
  const failed = results.filter((result) => result.status === "rejected");
199
368
  if (failed.length > 0) {
200
369
  throw new Error(`${OTEL_LOG_PREFIX} Flush failed`, {
@@ -203,8 +372,9 @@ export async function flushOpenTelemetry() {
203
372
  }
204
373
  }
205
374
  /**
206
- * @description Terminally shuts down the providers NeuroLink owns once no `acquireTelemetry` holder remains; while holders remain it only flushes. Telemetry cannot be restarted in this process afterwards. A host provider is never shut down; with one, only NeuroLink's Langfuse exporter is shut down and unregistered.
207
- * @returns {Promise<void>} Resolves when telemetry is flushed or shut down.
375
+ * @description Terminally shuts down the providers NeuroLink owns once no `acquireTelemetry` holder remains; while holders remain it only flushes, as `flushOpenTelemetry` does. Telemetry cannot be restarted in this process afterwards. A host provider is never shut down; with one, only NeuroLink's Langfuse exporter is shut down and unregistered. The OTLP exporters get the same budget as `flushOpenTelemetry`, and a failure of the shutdown itself is logged as a warning.
376
+ * @returns {Promise<void>} Resolves when telemetry is flushed or shut down, or the flush budget has run out.
377
+ * @throws {Error} When holders remain and the flush they get fails.
208
378
  */
209
379
  export async function shutdownOpenTelemetry() {
210
380
  if (holders > 0) {
@@ -1,7 +1,25 @@
1
1
  import { LangfuseExporter } from "../../../../observability/exporters/langfuseExporter.js";
2
2
  import type { ObservabilityConfig } from "../../../../types/index.js";
3
3
  /**
4
- * @description Decides whether Langfuse export is on: off when `config.langfuse` is `false`, on when it is an options object or both Langfuse keys are in the environment.
4
+ * @description Tells whether a setting says "off": `false`, or the text `false`, `0`, `off` or `no` in any case.
5
+ * @param value {unknown} - A config value or an environment variable.
6
+ * @returns {boolean} `true` for an explicit off setting; `false` for anything else, an unset value included.
7
+ */
8
+ export declare function isOff(value: unknown): boolean;
9
+ /**
10
+ * @description Resolves the environment name the OTLP resource reports as `deployment.environment`: the config value, then `LANGFUSE_TRACING_ENVIRONMENT`, `LANGFUSE_ENVIRONMENT` and `PUBLIC_APP_ENVIRONMENT`. Langfuse traces are labelled from the config value and `LANGFUSE_TRACING_ENVIRONMENT` alone, as before.
11
+ * @param config {ObservabilityConfig | undefined} - Observability options.
12
+ * @returns {string | undefined} The environment name, or `undefined` when none is set.
13
+ */
14
+ export declare function resolveEnvironment(config?: ObservabilityConfig): string | undefined;
15
+ /**
16
+ * @description Resolves the release the OTLP resource reports as `service.version`: the config value, then `LANGFUSE_RELEASE` and `PUBLIC_APP_VERSION`. Langfuse traces are labelled from the config value and `LANGFUSE_RELEASE` alone, as before.
17
+ * @param config {ObservabilityConfig | undefined} - Observability options.
18
+ * @returns {string | undefined} The release identifier, or `undefined` when none is set.
19
+ */
20
+ export declare function resolveRelease(config?: ObservabilityConfig): string | undefined;
21
+ /**
22
+ * @description Decides whether Langfuse export is on. Off when `config.langfuse` is `false` or an options object with `enabled: false`. On for `true` and for an options object with `enabled: true` or with both keys. Any other options object, and no options at all with both keys in the environment, is on unless `LANGFUSE_ENABLED` is `false`, `0`, `off` or `no`: the switch governs the keys wherever they come from the environment, so an options object that leaves them there (only a mask or an environment name) obeys it too, while keys or `enabled: true` given in code are an explicit choice and win over it.
5
23
  * @param config {ObservabilityConfig | undefined} - Observability options.
6
24
  * @returns {boolean} `true` when a Langfuse exporter should be created.
7
25
  */
@@ -11,8 +11,48 @@ async function initializeQuietly(exporter) {
11
11
  });
12
12
  }
13
13
  }
14
+ const OFF_VALUES = new Set(["false", "0", "off", "no"]);
14
15
  /**
15
- * @description Decides whether Langfuse export is on: off when `config.langfuse` is `false`, on when it is an options object or both Langfuse keys are in the environment.
16
+ * @description Tells whether a setting says "off": `false`, or the text `false`, `0`, `off` or `no` in any case.
17
+ * @param value {unknown} - A config value or an environment variable.
18
+ * @returns {boolean} `true` for an explicit off setting; `false` for anything else, an unset value included.
19
+ */
20
+ export function isOff(value) {
21
+ return (value === false ||
22
+ (typeof value === "string" && OFF_VALUES.has(value.trim().toLowerCase())));
23
+ }
24
+ function firstEnv(...names) {
25
+ for (const name of names) {
26
+ const value = process.env[name]?.trim();
27
+ if (value) {
28
+ return value;
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ function langfuseOptions(config) {
34
+ return typeof config?.langfuse === "object" ? config.langfuse : {};
35
+ }
36
+ /**
37
+ * @description Resolves the environment name the OTLP resource reports as `deployment.environment`: the config value, then `LANGFUSE_TRACING_ENVIRONMENT`, `LANGFUSE_ENVIRONMENT` and `PUBLIC_APP_ENVIRONMENT`. Langfuse traces are labelled from the config value and `LANGFUSE_TRACING_ENVIRONMENT` alone, as before.
38
+ * @param config {ObservabilityConfig | undefined} - Observability options.
39
+ * @returns {string | undefined} The environment name, or `undefined` when none is set.
40
+ */
41
+ export function resolveEnvironment(config) {
42
+ return (langfuseOptions(config).environment ||
43
+ firstEnv("LANGFUSE_TRACING_ENVIRONMENT", "LANGFUSE_ENVIRONMENT", "PUBLIC_APP_ENVIRONMENT"));
44
+ }
45
+ /**
46
+ * @description Resolves the release the OTLP resource reports as `service.version`: the config value, then `LANGFUSE_RELEASE` and `PUBLIC_APP_VERSION`. Langfuse traces are labelled from the config value and `LANGFUSE_RELEASE` alone, as before.
47
+ * @param config {ObservabilityConfig | undefined} - Observability options.
48
+ * @returns {string | undefined} The release identifier, or `undefined` when none is set.
49
+ */
50
+ export function resolveRelease(config) {
51
+ return (langfuseOptions(config).release ||
52
+ firstEnv("LANGFUSE_RELEASE", "PUBLIC_APP_VERSION"));
53
+ }
54
+ /**
55
+ * @description Decides whether Langfuse export is on. Off when `config.langfuse` is `false` or an options object with `enabled: false`. On for `true` and for an options object with `enabled: true` or with both keys. Any other options object, and no options at all with both keys in the environment, is on unless `LANGFUSE_ENABLED` is `false`, `0`, `off` or `no`: the switch governs the keys wherever they come from the environment, so an options object that leaves them there (only a mask or an environment name) obeys it too, while keys or `enabled: true` given in code are an explicit choice and win over it.
16
56
  * @param config {ObservabilityConfig | undefined} - Observability options.
17
57
  * @returns {boolean} `true` when a Langfuse exporter should be created.
18
58
  */
@@ -21,8 +61,21 @@ export function isLangfuseEnabled(config) {
21
61
  if (langfuse === false) {
22
62
  return false;
23
63
  }
24
- return (typeof langfuse === "object" ||
25
- Boolean(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY));
64
+ if (typeof langfuse === "object") {
65
+ if (isOff(langfuse.enabled)) {
66
+ return false;
67
+ }
68
+ // Keys or `enabled: true` given in code are an explicit choice and win over the
69
+ // environment switch. An object that leaves the keys to the environment (only a
70
+ // mask or an environment name) is governed by it like the keys themselves.
71
+ return (langfuse.enabled === true ||
72
+ Boolean(langfuse.publicKey && langfuse.secretKey) ||
73
+ !isOff(process.env.LANGFUSE_ENABLED));
74
+ }
75
+ if (langfuse === true) {
76
+ return true;
77
+ }
78
+ return (Boolean(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY) && !isOff(process.env.LANGFUSE_ENABLED));
26
79
  }
27
80
  /**
28
81
  * @description Builds and starts a Langfuse exporter from config, falling back to the `LANGFUSE_*` environment variables.
@@ -30,7 +83,7 @@ export function isLangfuseEnabled(config) {
30
83
  * @returns {LangfuseExporter | null} The exporter, or `null` (after a warning) when the public or secret key is missing.
31
84
  */
32
85
  export function createLangfuseExporter(config) {
33
- const options = typeof config?.langfuse === "object" ? config.langfuse : {};
86
+ const options = langfuseOptions(config);
34
87
  const publicKey = options.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY;
35
88
  const secretKey = options.secretKey ?? process.env.LANGFUSE_SECRET_KEY;
36
89
  if (!publicKey || !secretKey) {
@@ -42,9 +95,14 @@ export function createLangfuseExporter(config) {
42
95
  publicKey,
43
96
  secretKey,
44
97
  baseUrl: options.baseUrl ?? process.env.LANGFUSE_BASE_URL,
98
+ // The released build labelled Langfuse traces from the option and
99
+ // LANGFUSE_TRACING_ENVIRONMENT / LANGFUSE_RELEASE only (the exporter reads
100
+ // those two itself); the longer chain feeds the OTLP resource alone.
45
101
  environment: options.environment,
46
102
  release: options.release,
103
+ flushBudgetMs: options.flushBudgetMs,
47
104
  mask: options.mask,
105
+ redactIO: options.redactIO,
48
106
  maxBufferSize: options.exportMode === "immediate" ? 1 : options.flushAt,
49
107
  flushIntervalMs: options.flushInterval === undefined
50
108
  ? undefined
@@ -1,16 +1,26 @@
1
1
  import type { Resource } from "@opentelemetry/resources";
2
2
  import { MeterProvider } from "@opentelemetry/sdk-metrics";
3
3
  import { type SpanProcessor } from "@opentelemetry/sdk-trace-base";
4
+ import type { OtlpConnections } from "../../../../types/index.js";
5
+ /**
6
+ * @description Tracks the connections the OTLP exporters open, so a flush or shutdown that has run out of its budget can close them instead of leaving a request in flight that holds the process open.
7
+ * @returns {OtlpConnections} The agent factory to give the exporters and the function that closes what they opened.
8
+ */
9
+ export declare function createOtlpConnections(): OtlpConnections;
4
10
  /**
5
11
  * @description Builds a batching span processor that ships spans to an OTLP/HTTP endpoint.
6
12
  * @param endpoint {string} - OTLP base URL; `/v1/traces` is appended.
13
+ * @param budgetMs {number | undefined} - Request timeout in milliseconds, used unless `OTEL_EXPORTER_OTLP_TIMEOUT` or `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` is set; the exporter's default when omitted.
14
+ * @param connections {OtlpConnections | undefined} - Tracks the connections the exporter opens so they can be closed when a flush runs out of its budget.
7
15
  * @returns {SpanProcessor | null} The processor, or `null` (after a warning) when the exporter cannot be created.
8
16
  */
9
- export declare function createOtlpSpanProcessor(endpoint: string): SpanProcessor | null;
17
+ export declare function createOtlpSpanProcessor(endpoint: string, budgetMs?: number, connections?: OtlpConnections): SpanProcessor | null;
10
18
  /**
11
19
  * @description Builds a meter provider that exports metrics to an OTLP/HTTP endpoint and registers it as the global meter provider.
12
20
  * @param endpoint {string} - OTLP base URL; `/v1/metrics` is appended.
13
21
  * @param resource {Resource} - Resource describing the service.
22
+ * @param budgetMs {number | undefined} - Request timeout in milliseconds, used unless `OTEL_EXPORTER_OTLP_TIMEOUT` or `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` is set; the exporter's default when omitted.
23
+ * @param connections {OtlpConnections | undefined} - Tracks the connections the exporter opens so they can be closed when a flush runs out of its budget.
14
24
  * @returns {MeterProvider | null} The provider, or `null` (after a warning) when it cannot be created.
15
25
  */
16
- export declare function createOtlpMeterProvider(endpoint: string, resource: Resource): MeterProvider | null;
26
+ export declare function createOtlpMeterProvider(endpoint: string, resource: Resource, budgetMs?: number, connections?: OtlpConnections): MeterProvider | null;