@juspay/neurolink 9.42.0 → 9.43.0
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 +8 -0
- package/dist/auth/anthropicOAuth.js +12 -0
- package/dist/browser/neurolink.min.js +335 -334
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +200 -184
- package/dist/cli/commands/proxy.js +560 -518
- package/dist/core/baseProvider.d.ts +6 -1
- package/dist/core/baseProvider.js +219 -232
- package/dist/core/factory.d.ts +3 -0
- package/dist/core/factory.js +140 -190
- package/dist/core/modules/ToolsManager.d.ts +1 -0
- package/dist/core/modules/ToolsManager.js +40 -42
- package/dist/core/toolEvents.d.ts +3 -0
- package/dist/core/toolEvents.js +7 -0
- package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/evaluation/scorers/scorerRegistry.js +356 -284
- package/dist/lib/auth/anthropicOAuth.js +12 -0
- package/dist/lib/core/baseProvider.d.ts +6 -1
- package/dist/lib/core/baseProvider.js +219 -232
- package/dist/lib/core/factory.d.ts +3 -0
- package/dist/lib/core/factory.js +140 -190
- package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
- package/dist/lib/core/modules/ToolsManager.js +40 -42
- package/dist/lib/core/toolEvents.d.ts +3 -0
- package/dist/lib/core/toolEvents.js +8 -0
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/lib/evaluation/scorers/scorerRegistry.js +356 -284
- package/dist/lib/mcp/toolRegistry.d.ts +2 -0
- package/dist/lib/mcp/toolRegistry.js +32 -31
- package/dist/lib/neurolink.d.ts +38 -0
- package/dist/lib/neurolink.js +1890 -1707
- package/dist/lib/providers/googleAiStudio.js +0 -5
- package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/lib/providers/googleNativeGemini3.js +39 -1
- package/dist/lib/providers/googleVertex.d.ts +10 -0
- package/dist/lib/providers/googleVertex.js +445 -445
- package/dist/lib/providers/litellm.d.ts +1 -0
- package/dist/lib/providers/litellm.js +73 -64
- package/dist/lib/providers/ollama.js +17 -4
- package/dist/lib/providers/openAI.d.ts +2 -0
- package/dist/lib/providers/openAI.js +139 -140
- package/dist/lib/proxy/claudeFormat.js +14 -5
- package/dist/lib/proxy/oauthFetch.js +298 -318
- package/dist/lib/proxy/proxyConfig.js +3 -1
- package/dist/lib/proxy/proxyFetch.js +250 -222
- package/dist/lib/proxy/proxyHealth.d.ts +17 -0
- package/dist/lib/proxy/proxyHealth.js +55 -0
- package/dist/lib/proxy/requestLogger.js +140 -48
- package/dist/lib/proxy/routingPolicy.d.ts +33 -0
- package/dist/lib/proxy/routingPolicy.js +255 -0
- package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/lib/proxy/snapshotPersistence.js +41 -0
- package/dist/lib/proxy/sseInterceptor.js +36 -11
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
- package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
- package/dist/lib/tasks/store/redisTaskStore.js +42 -17
- package/dist/lib/tasks/taskManager.d.ts +2 -0
- package/dist/lib/tasks/taskManager.js +100 -5
- package/dist/lib/telemetry/telemetryService.js +9 -5
- package/dist/lib/types/cli.d.ts +4 -0
- package/dist/lib/types/proxyTypes.d.ts +211 -1
- package/dist/lib/types/tools.d.ts +18 -0
- package/dist/lib/utils/providerHealth.d.ts +1 -0
- package/dist/lib/utils/providerHealth.js +46 -31
- package/dist/lib/utils/providerUtils.js +11 -22
- package/dist/lib/utils/schemaConversion.d.ts +1 -0
- package/dist/lib/utils/schemaConversion.js +3 -0
- package/dist/mcp/toolRegistry.d.ts +2 -0
- package/dist/mcp/toolRegistry.js +32 -31
- package/dist/neurolink.d.ts +38 -0
- package/dist/neurolink.js +1890 -1707
- package/dist/providers/googleAiStudio.js +0 -5
- package/dist/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/providers/googleNativeGemini3.js +39 -1
- package/dist/providers/googleVertex.d.ts +10 -0
- package/dist/providers/googleVertex.js +445 -445
- package/dist/providers/litellm.d.ts +1 -0
- package/dist/providers/litellm.js +73 -64
- package/dist/providers/ollama.js +17 -4
- package/dist/providers/openAI.d.ts +2 -0
- package/dist/providers/openAI.js +139 -140
- package/dist/proxy/claudeFormat.js +14 -5
- package/dist/proxy/oauthFetch.js +298 -318
- package/dist/proxy/proxyConfig.js +3 -1
- package/dist/proxy/proxyFetch.js +250 -222
- package/dist/proxy/proxyHealth.d.ts +17 -0
- package/dist/proxy/proxyHealth.js +54 -0
- package/dist/proxy/requestLogger.js +140 -48
- package/dist/proxy/routingPolicy.d.ts +33 -0
- package/dist/proxy/routingPolicy.js +254 -0
- package/dist/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/proxy/snapshotPersistence.js +40 -0
- package/dist/proxy/sseInterceptor.js +36 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
- package/dist/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/tasks/backends/bullmqBackend.js +24 -18
- package/dist/tasks/store/redisTaskStore.js +42 -17
- package/dist/tasks/taskManager.d.ts +2 -0
- package/dist/tasks/taskManager.js +100 -5
- package/dist/telemetry/telemetryService.js +9 -5
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/proxyTypes.d.ts +211 -1
- package/dist/types/tools.d.ts +18 -0
- package/dist/utils/providerHealth.d.ts +1 -0
- package/dist/utils/providerHealth.js +46 -31
- package/dist/utils/providerUtils.js +12 -22
- package/dist/utils/schemaConversion.d.ts +1 -0
- package/dist/utils/schemaConversion.js +3 -0
- package/package.json +3 -2
- package/scripts/observability/check-proxy-telemetry.mjs +1 -1
- package/scripts/observability/manage-local-openobserve.sh +36 -5
|
@@ -21,6 +21,72 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semanti
|
|
|
21
21
|
import { AsyncLocalStorage } from "async_hooks";
|
|
22
22
|
import { logger } from "../../../../utils/logger.js";
|
|
23
23
|
const LOG_PREFIX = "[OpenTelemetry]";
|
|
24
|
+
function createOtelResource(config, serviceName) {
|
|
25
|
+
return resourceFromAttributes({
|
|
26
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
27
|
+
[ATTR_SERVICE_VERSION]: config.release || "v1.0.0",
|
|
28
|
+
"deployment.environment": config.environment || "dev",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName) {
|
|
32
|
+
if (!otlpEndpoint) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const metricExporter = new OTLPMetricExporter({
|
|
37
|
+
url: `${otlpEndpoint}/v1/metrics`,
|
|
38
|
+
});
|
|
39
|
+
const metricReader = new PeriodicExportingMetricReader({
|
|
40
|
+
exporter: metricExporter,
|
|
41
|
+
exportIntervalMillis: 15000,
|
|
42
|
+
exportTimeoutMillis: 10000,
|
|
43
|
+
});
|
|
44
|
+
meterProvider = new MeterProvider({
|
|
45
|
+
resource,
|
|
46
|
+
readers: [metricReader],
|
|
47
|
+
});
|
|
48
|
+
metrics.setGlobalMeterProvider(meterProvider);
|
|
49
|
+
logger.info(`${LOG_PREFIX} OTLP metric exporter added — MeterProvider registered globally`, {
|
|
50
|
+
endpoint: `${otlpEndpoint}/v1/metrics`,
|
|
51
|
+
exportIntervalMs: 15000,
|
|
52
|
+
serviceName,
|
|
53
|
+
meterProviderType: meterProvider.constructor.name,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (metricsError) {
|
|
57
|
+
logger.warn(`${LOG_PREFIX} Failed to create OTLP metric exporter (non-fatal)`, {
|
|
58
|
+
error: metricsError instanceof Error
|
|
59
|
+
? metricsError.message
|
|
60
|
+
: String(metricsError),
|
|
61
|
+
endpoint: otlpEndpoint,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const logExporter = new OTLPLogExporter({
|
|
66
|
+
url: `${otlpEndpoint}/v1/logs`,
|
|
67
|
+
});
|
|
68
|
+
const logProcessor = new BatchLogRecordProcessor(logExporter, {
|
|
69
|
+
maxQueueSize: 2048,
|
|
70
|
+
maxExportBatchSize: 512,
|
|
71
|
+
scheduledDelayMillis: 2000,
|
|
72
|
+
exportTimeoutMillis: 30000,
|
|
73
|
+
});
|
|
74
|
+
loggerProvider = new LoggerProvider({
|
|
75
|
+
resource,
|
|
76
|
+
processors: [logProcessor],
|
|
77
|
+
});
|
|
78
|
+
logger.info(`${LOG_PREFIX} OTLP log exporter added — LoggerProvider created`, {
|
|
79
|
+
endpoint: `${otlpEndpoint}/v1/logs`,
|
|
80
|
+
serviceName,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (logsError) {
|
|
84
|
+
logger.warn(`${LOG_PREFIX} Failed to create OTLP log exporter (non-fatal)`, {
|
|
85
|
+
error: logsError instanceof Error ? logsError.message : String(logsError),
|
|
86
|
+
endpoint: otlpEndpoint,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
24
90
|
const contextStorage = new AsyncLocalStorage();
|
|
25
91
|
let tracerProvider = null;
|
|
26
92
|
let meterProvider = null;
|
|
@@ -337,48 +403,20 @@ class ContextEnricher {
|
|
|
337
403
|
return Promise.resolve();
|
|
338
404
|
}
|
|
339
405
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
export function initializeOpenTelemetry(config) {
|
|
355
|
-
// Guard against multiple initializations — but always update config
|
|
356
|
-
// so that later NeuroLink instances can change traceNameFormat,
|
|
357
|
-
// autoDetectOperationName, and other configuration preferences
|
|
358
|
-
// without re-initializing the OTEL infrastructure.
|
|
359
|
-
if (isInitialized) {
|
|
360
|
-
currentConfig = config;
|
|
361
|
-
logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
|
|
362
|
-
usingExternalProvider,
|
|
363
|
-
hasLangfuseProcessor: !!langfuseProcessor,
|
|
364
|
-
hasTraceNameFormat: typeof config.traceNameFormat === "function",
|
|
365
|
-
});
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
// FIRST: Check for external provider mode - bypasses enabled check
|
|
369
|
-
// NOTE: When autoDetectExternalProvider is true, we trust the flag directly rather than
|
|
370
|
-
// calling hasExternalTracerProvider(). This is because Neurolink may bundle its own copy
|
|
371
|
-
// of @opentelemetry/api, which has a separate global state from the host application.
|
|
372
|
-
// The hasExternalTracerProvider() check would query Neurolink's bundled @opentelemetry/api
|
|
373
|
-
// global state (which has no provider registered), not the host's global state.
|
|
374
|
-
// By trusting autoDetectExternalProvider=true, we let the host application signal that
|
|
375
|
-
// it has already registered a TracerProvider.
|
|
376
|
-
const shouldUseExternal = config?.useExternalTracerProvider === true ||
|
|
377
|
-
config?.autoDetectExternalProvider === true;
|
|
378
|
-
if (shouldUseExternal) {
|
|
379
|
-
// Validate credentials even in external mode
|
|
380
|
-
if (!config?.publicKey || !config?.secretKey) {
|
|
381
|
-
logger.warn(`${LOG_PREFIX} External provider mode but missing credentials, skipping initialization`, {
|
|
406
|
+
function createLangfuseProcessor(config) {
|
|
407
|
+
return new LangfuseSpanProcessor({
|
|
408
|
+
publicKey: config.publicKey,
|
|
409
|
+
secretKey: config.secretKey,
|
|
410
|
+
baseUrl: config.baseUrl || "https://cloud.langfuse.com",
|
|
411
|
+
environment: config.environment || "dev",
|
|
412
|
+
release: config.release || "v1.0.0",
|
|
413
|
+
shouldExportSpan: () => true,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function initializeExternalOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds) {
|
|
417
|
+
if (langfuseRequested && !hasLangfuseCreds) {
|
|
418
|
+
if (!otlpEndpoint) {
|
|
419
|
+
logger.warn(`${LOG_PREFIX} External provider mode requested Langfuse but credentials are missing, and no OTLP endpoint is configured; skipping initialization`, {
|
|
382
420
|
hasPublicKey: !!config?.publicKey,
|
|
383
421
|
hasSecretKey: !!config?.secretKey,
|
|
384
422
|
});
|
|
@@ -386,86 +424,71 @@ export function initializeOpenTelemetry(config) {
|
|
|
386
424
|
isCredentialsValid = false;
|
|
387
425
|
return;
|
|
388
426
|
}
|
|
427
|
+
logger.warn(`${LOG_PREFIX} External provider mode missing Langfuse credentials; continuing with OTLP-only metrics/logs`, {
|
|
428
|
+
hasPublicKey: !!config?.publicKey,
|
|
429
|
+
hasSecretKey: !!config?.secretKey,
|
|
430
|
+
otlpEnabled: true,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
currentConfig = config;
|
|
435
|
+
isCredentialsValid = hasLangfuseCreds;
|
|
436
|
+
langfuseProcessor =
|
|
437
|
+
langfuseRequested && hasLangfuseCreds
|
|
438
|
+
? createLangfuseProcessor(config)
|
|
439
|
+
: null;
|
|
440
|
+
usingExternalProvider = true;
|
|
441
|
+
isInitialized = true;
|
|
442
|
+
initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
|
|
389
443
|
try {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
baseUrl: config.baseUrl || "https://cloud.langfuse.com",
|
|
398
|
-
environment: config.environment || "dev",
|
|
399
|
-
release: config.release || "v1.0.0",
|
|
400
|
-
shouldExportSpan: () => true,
|
|
401
|
-
});
|
|
402
|
-
usingExternalProvider = true;
|
|
403
|
-
isInitialized = true;
|
|
404
|
-
// Auto-register ContextEnricher with the global TracerProvider
|
|
405
|
-
// This ensures trace names are set even when host doesn't call getSpanProcessors()
|
|
406
|
-
try {
|
|
407
|
-
const globalProvider = trace.getTracerProvider();
|
|
408
|
-
// Check if it's a real provider with addSpanProcessor method (not the no-op default)
|
|
409
|
-
if (globalProvider &&
|
|
410
|
-
typeof globalProvider
|
|
411
|
-
.addSpanProcessor === "function") {
|
|
412
|
-
const provider = globalProvider;
|
|
413
|
-
// Add ContextEnricher for trace name enrichment
|
|
414
|
-
provider.addSpanProcessor(new ContextEnricher());
|
|
415
|
-
// Only add LangfuseSpanProcessor if the host has not already registered one.
|
|
416
|
-
// When skipLangfuseSpanProcessor is true, the host (e.g. Curator) already
|
|
417
|
-
// registers its own LangfuseSpanProcessor via a DeferredSpanProcessor, so
|
|
418
|
-
// adding another one here would cause duplicate trace exports to Langfuse.
|
|
419
|
-
const skipLangfuse = config.skipLangfuseSpanProcessor === true;
|
|
420
|
-
if (!skipLangfuse) {
|
|
421
|
-
provider.addSpanProcessor(langfuseProcessor);
|
|
422
|
-
}
|
|
423
|
-
logger.info(`${LOG_PREFIX} Auto-registered processors with global TracerProvider`, {
|
|
424
|
-
processors: skipLangfuse
|
|
425
|
-
? ["ContextEnricher"]
|
|
426
|
-
: ["ContextEnricher", "LangfuseSpanProcessor"],
|
|
427
|
-
reason: "External provider mode with auto-registration",
|
|
428
|
-
skippedLangfuseSpanProcessor: skipLangfuse,
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
// No real provider found - host will need to add processors manually
|
|
433
|
-
logger.info(`${LOG_PREFIX} Using external TracerProvider mode`, {
|
|
434
|
-
reason: config.useExternalTracerProvider
|
|
435
|
-
? "useExternalTracerProvider=true"
|
|
436
|
-
: "autoDetectExternalProvider=true (trusting host signal)",
|
|
437
|
-
instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
|
|
438
|
-
});
|
|
439
|
-
logger.info(`${LOG_PREFIX} Span processors ready for external use`, {
|
|
440
|
-
processors: ["ContextEnricher", "LangfuseSpanProcessor"],
|
|
441
|
-
usage: "import { getSpanProcessors } from '@juspay/neurolink'",
|
|
442
|
-
});
|
|
444
|
+
const globalProvider = trace.getTracerProvider();
|
|
445
|
+
const provider = globalProvider;
|
|
446
|
+
if (globalProvider && typeof provider.addSpanProcessor === "function") {
|
|
447
|
+
provider.addSpanProcessor(new ContextEnricher());
|
|
448
|
+
const skipLangfuse = config.skipLangfuseSpanProcessor === true || !langfuseProcessor;
|
|
449
|
+
if (!skipLangfuse && langfuseProcessor) {
|
|
450
|
+
provider.addSpanProcessor(langfuseProcessor);
|
|
443
451
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
: String(autoRegisterError),
|
|
451
|
-
instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
|
|
452
|
+
logger.info(`${LOG_PREFIX} Auto-registered processors with global TracerProvider`, {
|
|
453
|
+
processors: skipLangfuse
|
|
454
|
+
? ["ContextEnricher"]
|
|
455
|
+
: ["ContextEnricher", "LangfuseSpanProcessor"],
|
|
456
|
+
reason: "External provider mode with auto-registration",
|
|
457
|
+
skippedLangfuseSpanProcessor: skipLangfuse,
|
|
452
458
|
});
|
|
459
|
+
return;
|
|
453
460
|
}
|
|
454
|
-
|
|
461
|
+
logger.info(`${LOG_PREFIX} Using external TracerProvider mode`, {
|
|
462
|
+
reason: config.useExternalTracerProvider
|
|
463
|
+
? "useExternalTracerProvider=true"
|
|
464
|
+
: "autoDetectExternalProvider=true (trusting host signal)",
|
|
465
|
+
instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
|
|
466
|
+
});
|
|
467
|
+
logger.info(`${LOG_PREFIX} Span processors ready for external use`, {
|
|
468
|
+
processors: langfuseProcessor
|
|
469
|
+
? ["ContextEnricher", "LangfuseSpanProcessor"]
|
|
470
|
+
: ["ContextEnricher"],
|
|
471
|
+
usage: "import { getSpanProcessors } from '@juspay/neurolink'",
|
|
472
|
+
});
|
|
455
473
|
}
|
|
456
|
-
catch (
|
|
457
|
-
logger.
|
|
458
|
-
error:
|
|
459
|
-
|
|
474
|
+
catch (autoRegisterError) {
|
|
475
|
+
logger.warn(`${LOG_PREFIX} Auto-registration failed, manual registration required`, {
|
|
476
|
+
error: autoRegisterError instanceof Error
|
|
477
|
+
? autoRegisterError.message
|
|
478
|
+
: String(autoRegisterError),
|
|
479
|
+
instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
|
|
460
480
|
});
|
|
461
|
-
isInitialized = true;
|
|
462
|
-
return;
|
|
463
481
|
}
|
|
464
482
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
483
|
+
catch (error) {
|
|
484
|
+
logger.error(`${LOG_PREFIX} Failed to create span processor for external mode`, {
|
|
485
|
+
error: error instanceof Error ? error.message : String(error),
|
|
486
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
487
|
+
});
|
|
488
|
+
isInitialized = true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds) {
|
|
469
492
|
if ((!langfuseRequested || !hasLangfuseCreds) && !otlpEndpoint) {
|
|
470
493
|
if (langfuseRequested && !hasLangfuseCreds) {
|
|
471
494
|
logger.warn(`${LOG_PREFIX} Langfuse requested but credentials are missing, and no OTLP endpoint is configured; skipping initialization`, {
|
|
@@ -489,55 +512,31 @@ export function initializeOpenTelemetry(config) {
|
|
|
489
512
|
try {
|
|
490
513
|
currentConfig = config;
|
|
491
514
|
isCredentialsValid = hasLangfuseCreds;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
langfuseProcessor = new LangfuseSpanProcessor({
|
|
497
|
-
publicKey: config.publicKey,
|
|
498
|
-
secretKey: config.secretKey,
|
|
499
|
-
baseUrl: config.baseUrl || "https://cloud.langfuse.com",
|
|
500
|
-
environment: config.environment || "dev",
|
|
501
|
-
release: config.release || "v1.0.0",
|
|
502
|
-
shouldExportSpan: () => true,
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
else {
|
|
506
|
-
langfuseProcessor = null;
|
|
507
|
-
}
|
|
515
|
+
langfuseProcessor =
|
|
516
|
+
langfuseRequested && hasLangfuseCreds
|
|
517
|
+
? createLangfuseProcessor(config)
|
|
518
|
+
: null;
|
|
508
519
|
logger.debug(`${LOG_PREFIX} Standalone observability mode`, {
|
|
509
520
|
langfuseEnabled: !!langfuseProcessor,
|
|
510
521
|
otlpEnabled: !!otlpEndpoint,
|
|
511
522
|
baseUrl: config.baseUrl || "https://cloud.langfuse.com",
|
|
512
523
|
environment: config.environment || "dev",
|
|
513
524
|
});
|
|
514
|
-
// Step 2: Create our own TracerProvider (standalone behavior)
|
|
515
|
-
// Use OTEL_SERVICE_NAME env var if available, otherwise "neurolink"
|
|
516
|
-
const serviceName = process.env.OTEL_SERVICE_NAME || "neurolink";
|
|
517
|
-
const resource = resourceFromAttributes({
|
|
518
|
-
[ATTR_SERVICE_NAME]: serviceName,
|
|
519
|
-
[ATTR_SERVICE_VERSION]: config.release || "v1.0.0",
|
|
520
|
-
"deployment.environment": config.environment || "dev",
|
|
521
|
-
});
|
|
522
|
-
// Build span processor list
|
|
523
525
|
const spanProcessors = [new ContextEnricher()];
|
|
524
526
|
if (langfuseProcessor) {
|
|
525
527
|
spanProcessors.push(langfuseProcessor);
|
|
526
528
|
}
|
|
527
|
-
// Step 2b: If OTEL_EXPORTER_OTLP_ENDPOINT is set, also export via OTLP HTTP
|
|
528
|
-
// This allows sending traces to an OpenTelemetry Collector (e.g. for OpenObserve)
|
|
529
529
|
if (otlpEndpoint) {
|
|
530
530
|
try {
|
|
531
531
|
const otlpExporter = new OTLPTraceExporter({
|
|
532
532
|
url: `${otlpEndpoint}/v1/traces`,
|
|
533
533
|
});
|
|
534
|
-
|
|
534
|
+
spanProcessors.push(new BatchSpanProcessor(otlpExporter, {
|
|
535
535
|
maxQueueSize: 2048,
|
|
536
536
|
maxExportBatchSize: 512,
|
|
537
537
|
scheduledDelayMillis: 1000,
|
|
538
538
|
exportTimeoutMillis: 30000,
|
|
539
|
-
});
|
|
540
|
-
spanProcessors.push(otlpBatchProcessor);
|
|
539
|
+
}));
|
|
541
540
|
logger.info(`${LOG_PREFIX} OTLP trace exporter added`, {
|
|
542
541
|
endpoint: `${otlpEndpoint}/v1/traces`,
|
|
543
542
|
serviceName,
|
|
@@ -552,81 +551,13 @@ export function initializeOpenTelemetry(config) {
|
|
|
552
551
|
});
|
|
553
552
|
}
|
|
554
553
|
}
|
|
555
|
-
tracerProvider = new NodeTracerProvider({
|
|
556
|
-
resource,
|
|
557
|
-
spanProcessors,
|
|
558
|
-
});
|
|
559
|
-
// Step 4: Register globally with explicit W3C propagator
|
|
560
|
-
// This ensures traceparent headers from calling SDKs are extracted correctly,
|
|
561
|
-
// even if another library registers a no-op propagator before us.
|
|
554
|
+
tracerProvider = new NodeTracerProvider({ resource, spanProcessors });
|
|
562
555
|
tracerProvider.register({
|
|
563
556
|
propagator: new W3CTraceContextPropagator(),
|
|
564
557
|
});
|
|
565
558
|
usingExternalProvider = false;
|
|
566
559
|
isInitialized = true;
|
|
567
|
-
|
|
568
|
-
// This enables TelemetryService's metrics.getMeter() instruments to export via OTLP
|
|
569
|
-
if (otlpEndpoint) {
|
|
570
|
-
try {
|
|
571
|
-
const metricExporter = new OTLPMetricExporter({
|
|
572
|
-
url: `${otlpEndpoint}/v1/metrics`,
|
|
573
|
-
});
|
|
574
|
-
const metricReader = new PeriodicExportingMetricReader({
|
|
575
|
-
exporter: metricExporter,
|
|
576
|
-
exportIntervalMillis: 15000, // Export every 15 seconds
|
|
577
|
-
exportTimeoutMillis: 10000,
|
|
578
|
-
});
|
|
579
|
-
meterProvider = new MeterProvider({
|
|
580
|
-
resource,
|
|
581
|
-
readers: [metricReader],
|
|
582
|
-
});
|
|
583
|
-
// Register globally so TelemetryService's metrics.getMeter() picks it up
|
|
584
|
-
metrics.setGlobalMeterProvider(meterProvider);
|
|
585
|
-
logger.info(`${LOG_PREFIX} OTLP metric exporter added — MeterProvider registered globally`, {
|
|
586
|
-
endpoint: `${otlpEndpoint}/v1/metrics`,
|
|
587
|
-
exportIntervalMs: 15000,
|
|
588
|
-
serviceName,
|
|
589
|
-
meterProviderType: meterProvider.constructor.name,
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
catch (metricsError) {
|
|
593
|
-
logger.warn(`${LOG_PREFIX} Failed to create OTLP metric exporter (non-fatal)`, {
|
|
594
|
-
error: metricsError instanceof Error
|
|
595
|
-
? metricsError.message
|
|
596
|
-
: String(metricsError),
|
|
597
|
-
endpoint: otlpEndpoint,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
// Step 6: Set up LoggerProvider for OTLP log export
|
|
601
|
-
// This enables logRequest() to emit structured log records via OTLP
|
|
602
|
-
try {
|
|
603
|
-
const logExporter = new OTLPLogExporter({
|
|
604
|
-
url: `${otlpEndpoint}/v1/logs`,
|
|
605
|
-
});
|
|
606
|
-
const logProcessor = new BatchLogRecordProcessor(logExporter, {
|
|
607
|
-
maxQueueSize: 2048,
|
|
608
|
-
maxExportBatchSize: 512,
|
|
609
|
-
scheduledDelayMillis: 2000,
|
|
610
|
-
exportTimeoutMillis: 30000,
|
|
611
|
-
});
|
|
612
|
-
loggerProvider = new LoggerProvider({
|
|
613
|
-
resource,
|
|
614
|
-
processors: [logProcessor],
|
|
615
|
-
});
|
|
616
|
-
logger.info(`${LOG_PREFIX} OTLP log exporter added — LoggerProvider created`, {
|
|
617
|
-
endpoint: `${otlpEndpoint}/v1/logs`,
|
|
618
|
-
serviceName,
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
catch (logsError) {
|
|
622
|
-
logger.warn(`${LOG_PREFIX} Failed to create OTLP log exporter (non-fatal)`, {
|
|
623
|
-
error: logsError instanceof Error
|
|
624
|
-
? logsError.message
|
|
625
|
-
: String(logsError),
|
|
626
|
-
endpoint: otlpEndpoint,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
}
|
|
560
|
+
initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
|
|
630
561
|
logger.info(`${LOG_PREFIX} Observability initialized`, {
|
|
631
562
|
baseUrl: config.baseUrl || "https://cloud.langfuse.com",
|
|
632
563
|
environment: config.environment || "dev",
|
|
@@ -638,23 +569,19 @@ export function initializeOpenTelemetry(config) {
|
|
|
638
569
|
});
|
|
639
570
|
}
|
|
640
571
|
catch (error) {
|
|
641
|
-
// Check if this is a duplicate registration error
|
|
642
572
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
643
573
|
const isDuplicateError = errorMessage.includes("duplicate registration") ||
|
|
644
574
|
errorMessage.includes("already registered") ||
|
|
645
575
|
errorMessage.includes("already set");
|
|
646
576
|
if (isDuplicateError) {
|
|
647
|
-
// Graceful handling: switch to external mode
|
|
648
577
|
logger.warn(`${LOG_PREFIX} TracerProvider already registered, switching to external mode`, {
|
|
649
578
|
error: errorMessage,
|
|
650
579
|
recommendation: "Set useExternalTracerProvider=true or autoDetectExternalProvider=true in config",
|
|
651
580
|
});
|
|
652
581
|
usingExternalProvider = true;
|
|
653
582
|
isInitialized = true;
|
|
654
|
-
// Don't throw - processors are still usable
|
|
655
583
|
return;
|
|
656
584
|
}
|
|
657
|
-
// Other errors: log and re-throw
|
|
658
585
|
logger.error(`${LOG_PREFIX} Initialization failed`, {
|
|
659
586
|
error: errorMessage,
|
|
660
587
|
stack: error instanceof Error ? error.stack : undefined,
|
|
@@ -662,6 +589,55 @@ export function initializeOpenTelemetry(config) {
|
|
|
662
589
|
throw error;
|
|
663
590
|
}
|
|
664
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* Initialize OpenTelemetry with Langfuse span processor
|
|
594
|
+
*
|
|
595
|
+
* This connects Vercel AI SDK's experimental_telemetry to Langfuse by:
|
|
596
|
+
* 1. Creating LangfuseSpanProcessor with Langfuse credentials
|
|
597
|
+
* 2. Creating a NodeTracerProvider with service metadata and span processor
|
|
598
|
+
* 3. Registering the provider globally for AI SDK to use
|
|
599
|
+
*
|
|
600
|
+
* NEW: If useExternalTracerProvider is true or autoDetectExternalProvider detects
|
|
601
|
+
* an existing provider, steps 2 and 3 are skipped. The span processors are still
|
|
602
|
+
* created and can be retrieved via getSpanProcessors().
|
|
603
|
+
*
|
|
604
|
+
* @param config - Langfuse configuration passed from parent application
|
|
605
|
+
*/
|
|
606
|
+
export function initializeOpenTelemetry(config) {
|
|
607
|
+
// Guard against multiple initializations — but always update config
|
|
608
|
+
// so that later NeuroLink instances can change traceNameFormat,
|
|
609
|
+
// autoDetectOperationName, and other configuration preferences
|
|
610
|
+
// without re-initializing the OTEL infrastructure.
|
|
611
|
+
if (isInitialized) {
|
|
612
|
+
currentConfig = config;
|
|
613
|
+
logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
|
|
614
|
+
usingExternalProvider,
|
|
615
|
+
hasLangfuseProcessor: !!langfuseProcessor,
|
|
616
|
+
hasTraceNameFormat: typeof config.traceNameFormat === "function",
|
|
617
|
+
});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// FIRST: Check for external provider mode - bypasses enabled check
|
|
621
|
+
// NOTE: When autoDetectExternalProvider is true, we trust the flag directly rather than
|
|
622
|
+
// calling hasExternalTracerProvider(). This is because Neurolink may bundle its own copy
|
|
623
|
+
// of @opentelemetry/api, which has a separate global state from the host application.
|
|
624
|
+
// The hasExternalTracerProvider() check would query Neurolink's bundled @opentelemetry/api
|
|
625
|
+
// global state (which has no provider registered), not the host's global state.
|
|
626
|
+
// By trusting autoDetectExternalProvider=true, we let the host application signal that
|
|
627
|
+
// it has already registered a TracerProvider.
|
|
628
|
+
const shouldUseExternal = config?.useExternalTracerProvider === true ||
|
|
629
|
+
config?.autoDetectExternalProvider === true;
|
|
630
|
+
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
631
|
+
const langfuseRequested = config?.enabled === true;
|
|
632
|
+
const hasLangfuseCreds = !!config.publicKey && !!config.secretKey;
|
|
633
|
+
const serviceName = process.env.OTEL_SERVICE_NAME || "neurolink";
|
|
634
|
+
const resource = createOtelResource(config, serviceName);
|
|
635
|
+
if (shouldUseExternal) {
|
|
636
|
+
initializeExternalOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds);
|
|
640
|
+
}
|
|
665
641
|
/**
|
|
666
642
|
* Flush all pending spans to Langfuse
|
|
667
643
|
*/
|
|
@@ -64,28 +64,34 @@ export class BullMQBackend {
|
|
|
64
64
|
async schedule(task, executor) {
|
|
65
65
|
const queue = this.getQueue();
|
|
66
66
|
this.executors.set(task.id, executor);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
try {
|
|
68
|
+
const jobData = { taskId: task.id, task };
|
|
69
|
+
const schedule = task.schedule;
|
|
70
|
+
if (schedule.type === "cron") {
|
|
71
|
+
await queue.upsertJobScheduler(task.id, {
|
|
72
|
+
pattern: schedule.expression,
|
|
73
|
+
...(schedule.timezone ? { tz: schedule.timezone } : {}),
|
|
74
|
+
}, { name: task.name, data: jobData });
|
|
75
|
+
}
|
|
76
|
+
else if (schedule.type === "interval") {
|
|
77
|
+
await queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
|
|
78
|
+
}
|
|
79
|
+
else if (schedule.type === "once") {
|
|
80
|
+
const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
|
|
81
|
+
const delay = Math.max(0, at.getTime() - Date.now());
|
|
82
|
+
await queue.add(task.name, jobData, {
|
|
83
|
+
jobId: task.id,
|
|
84
|
+
delay,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
77
87
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await queue.add(task.name, jobData, {
|
|
82
|
-
jobId: task.id,
|
|
83
|
-
delay,
|
|
84
|
-
});
|
|
88
|
+
catch (error) {
|
|
89
|
+
this.executors.delete(task.id);
|
|
90
|
+
throw error;
|
|
85
91
|
}
|
|
86
92
|
logger.info("[BullMQ] Task scheduled", {
|
|
87
93
|
taskId: task.id,
|
|
88
|
-
type: schedule.type,
|
|
94
|
+
type: task.schedule.type,
|
|
89
95
|
});
|
|
90
96
|
}
|
|
91
97
|
async cancel(taskId) {
|
|
@@ -63,7 +63,7 @@ export class RedisTaskStore {
|
|
|
63
63
|
async save(task) {
|
|
64
64
|
const client = this.getClient();
|
|
65
65
|
await client.hSet(TASKS_HASH, task.id, JSON.stringify(task));
|
|
66
|
-
this.applyRetentionTTL(task);
|
|
66
|
+
this.applyRetentionTTL(task, client);
|
|
67
67
|
}
|
|
68
68
|
async get(taskId) {
|
|
69
69
|
const client = this.getClient();
|
|
@@ -95,7 +95,7 @@ export class RedisTaskStore {
|
|
|
95
95
|
updatedAt: new Date().toISOString(),
|
|
96
96
|
};
|
|
97
97
|
await client.hSet(TASKS_HASH, taskId, JSON.stringify(updated));
|
|
98
|
-
this.applyRetentionTTL(updated);
|
|
98
|
+
this.applyRetentionTTL(updated, client);
|
|
99
99
|
return updated;
|
|
100
100
|
}
|
|
101
101
|
async delete(taskId) {
|
|
@@ -167,7 +167,7 @@ export class RedisTaskStore {
|
|
|
167
167
|
* Set Redis TTL on terminal-state tasks so they auto-expire.
|
|
168
168
|
* Active and paused tasks never expire.
|
|
169
169
|
*/
|
|
170
|
-
applyRetentionTTL(task) {
|
|
170
|
+
applyRetentionTTL(task, client) {
|
|
171
171
|
// We don't set EXPIRE on the hash field directly (Redis doesn't support per-field TTL).
|
|
172
172
|
// Instead, run logs and history keys get TTL. The task hash field itself must be
|
|
173
173
|
// cleaned up via manual deletion or BullMQ's built-in job cleanup.
|
|
@@ -177,21 +177,46 @@ export class RedisTaskStore {
|
|
|
177
177
|
cancelled: this.retentionConfig.cancelledTTL,
|
|
178
178
|
};
|
|
179
179
|
const ttlMs = ttlMap[task.status];
|
|
180
|
-
if (ttlMs) {
|
|
181
|
-
|
|
182
|
-
const ttlSeconds = Math.ceil(ttlMs / 1000);
|
|
183
|
-
// Set TTL on associated keys
|
|
184
|
-
client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => {
|
|
185
|
-
logger.debug("[TaskStore:Redis] Failed to set TTL", {
|
|
186
|
-
error: String(err),
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => {
|
|
190
|
-
logger.debug("[TaskStore:Redis] Failed to set TTL", {
|
|
191
|
-
error: String(err),
|
|
192
|
-
});
|
|
193
|
-
});
|
|
180
|
+
if (!ttlMs || !client.isOpen) {
|
|
181
|
+
return;
|
|
194
182
|
}
|
|
183
|
+
const ttlSeconds = Math.ceil(ttlMs / 1000);
|
|
184
|
+
// Set TTL on associated keys best-effort. A successful task write should not
|
|
185
|
+
// be surfaced as a failure just because the retention metadata could not be updated.
|
|
186
|
+
void (async () => {
|
|
187
|
+
const runsKey = taskRunsKey(task.id);
|
|
188
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
189
|
+
try {
|
|
190
|
+
await client.expire(runsKey, ttlSeconds);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
if (attempt === 3) {
|
|
195
|
+
logger.warn("[TaskStore:Redis] expire failed after 3 attempts on task runs key — task data may outlive retention window", { taskId: task.id, key: runsKey, ttlSeconds, err: String(err) });
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
await new Promise((r) => setTimeout(r, 100 * attempt));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
})();
|
|
203
|
+
void (async () => {
|
|
204
|
+
const histKey = taskHistoryKey(task.id);
|
|
205
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
206
|
+
try {
|
|
207
|
+
await client.expire(histKey, ttlSeconds);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (attempt === 3) {
|
|
212
|
+
logger.warn("[TaskStore:Redis] expire failed after 3 attempts on task history key — task data may outlive retention window", { taskId: task.id, key: histKey, ttlSeconds, err: String(err) });
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
await new Promise((r) => setTimeout(r, 100 * attempt));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
})();
|
|
195
220
|
}
|
|
196
221
|
}
|
|
197
222
|
//# sourceMappingURL=redisTaskStore.js.map
|
|
@@ -49,6 +49,8 @@ export declare class TaskManager {
|
|
|
49
49
|
shutdown(): Promise<void>;
|
|
50
50
|
/** Check if the backend is healthy */
|
|
51
51
|
isHealthy(): Promise<boolean>;
|
|
52
|
+
private restoreScheduledTask;
|
|
53
|
+
private rollbackTaskUpdate;
|
|
52
54
|
/**
|
|
53
55
|
* Called by the backend on each scheduled tick.
|
|
54
56
|
* Executes the task, updates state, fires callbacks/events.
|