@juspay/neurolink 9.41.0 → 9.42.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.
Files changed (189) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +7 -1
  3. package/dist/auth/anthropicOAuth.d.ts +18 -3
  4. package/dist/auth/anthropicOAuth.js +137 -4
  5. package/dist/auth/providers/firebase.js +5 -1
  6. package/dist/auth/providers/jwt.js +5 -1
  7. package/dist/auth/providers/workos.js +5 -1
  8. package/dist/auth/sessionManager.d.ts +1 -1
  9. package/dist/auth/sessionManager.js +58 -27
  10. package/dist/browser/neurolink.min.js +337 -318
  11. package/dist/cli/commands/mcp.js +3 -0
  12. package/dist/cli/commands/proxy.d.ts +2 -1
  13. package/dist/cli/commands/proxy.js +279 -16
  14. package/dist/cli/commands/task.js +3 -0
  15. package/dist/cli/factories/commandFactory.d.ts +2 -0
  16. package/dist/cli/factories/commandFactory.js +38 -0
  17. package/dist/cli/parser.js +4 -3
  18. package/dist/client/aiSdkAdapter.js +3 -0
  19. package/dist/client/streamingClient.js +30 -10
  20. package/dist/core/modules/GenerationHandler.js +3 -2
  21. package/dist/core/redisConversationMemoryManager.js +7 -3
  22. package/dist/evaluation/BatchEvaluator.js +4 -1
  23. package/dist/evaluation/hooks/observabilityHooks.js +5 -3
  24. package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  25. package/dist/evaluation/pipeline/evaluationPipeline.js +20 -8
  26. package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  27. package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  28. package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
  29. package/dist/lib/auth/anthropicOAuth.js +137 -4
  30. package/dist/lib/auth/providers/firebase.js +5 -1
  31. package/dist/lib/auth/providers/jwt.js +5 -1
  32. package/dist/lib/auth/providers/workos.js +5 -1
  33. package/dist/lib/auth/sessionManager.d.ts +1 -1
  34. package/dist/lib/auth/sessionManager.js +58 -27
  35. package/dist/lib/client/aiSdkAdapter.js +3 -0
  36. package/dist/lib/client/streamingClient.js +30 -10
  37. package/dist/lib/core/modules/GenerationHandler.js +3 -2
  38. package/dist/lib/core/redisConversationMemoryManager.js +7 -3
  39. package/dist/lib/evaluation/BatchEvaluator.js +4 -1
  40. package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
  41. package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  42. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +20 -8
  43. package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  44. package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  45. package/dist/lib/neurolink.d.ts +3 -2
  46. package/dist/lib/neurolink.js +260 -494
  47. package/dist/lib/observability/otelBridge.d.ts +2 -2
  48. package/dist/lib/observability/otelBridge.js +12 -3
  49. package/dist/lib/providers/amazonBedrock.js +2 -4
  50. package/dist/lib/providers/anthropic.d.ts +9 -5
  51. package/dist/lib/providers/anthropic.js +19 -14
  52. package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
  53. package/dist/lib/providers/anthropicBaseProvider.js +5 -4
  54. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  55. package/dist/lib/providers/azureOpenai.js +5 -4
  56. package/dist/lib/providers/googleAiStudio.js +30 -1
  57. package/dist/lib/providers/googleVertex.js +28 -6
  58. package/dist/lib/providers/huggingFace.d.ts +3 -3
  59. package/dist/lib/providers/huggingFace.js +6 -8
  60. package/dist/lib/providers/litellm.js +41 -29
  61. package/dist/lib/providers/mistral.js +2 -1
  62. package/dist/lib/providers/ollama.js +80 -23
  63. package/dist/lib/providers/openAI.js +3 -2
  64. package/dist/lib/providers/openRouter.js +2 -1
  65. package/dist/lib/providers/openaiCompatible.d.ts +4 -4
  66. package/dist/lib/providers/openaiCompatible.js +4 -4
  67. package/dist/lib/proxy/claudeFormat.d.ts +3 -2
  68. package/dist/lib/proxy/claudeFormat.js +25 -20
  69. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  70. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  71. package/dist/lib/proxy/modelRouter.js +3 -0
  72. package/dist/lib/proxy/oauthFetch.d.ts +1 -1
  73. package/dist/lib/proxy/oauthFetch.js +65 -72
  74. package/dist/lib/proxy/proxyConfig.js +44 -24
  75. package/dist/lib/proxy/proxyEnv.d.ts +19 -0
  76. package/dist/lib/proxy/proxyEnv.js +73 -0
  77. package/dist/lib/proxy/proxyFetch.js +50 -4
  78. package/dist/lib/proxy/proxyTracer.d.ts +133 -0
  79. package/dist/lib/proxy/proxyTracer.js +645 -0
  80. package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
  81. package/dist/lib/proxy/rawStreamCapture.js +83 -0
  82. package/dist/lib/proxy/requestLogger.d.ts +32 -5
  83. package/dist/lib/proxy/requestLogger.js +406 -37
  84. package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
  85. package/dist/lib/proxy/sseInterceptor.js +402 -0
  86. package/dist/lib/proxy/usageStats.d.ts +4 -3
  87. package/dist/lib/proxy/usageStats.js +25 -12
  88. package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
  89. package/dist/lib/rag/chunking/markdownChunker.js +15 -6
  90. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +7 -2
  91. package/dist/lib/server/routes/claudeProxyRoutes.js +1737 -508
  92. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
  93. package/dist/lib/services/server/ai/observability/instrumentation.js +240 -40
  94. package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
  95. package/dist/lib/tasks/backends/bullmqBackend.js +14 -7
  96. package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
  97. package/dist/lib/tasks/store/redisTaskStore.js +34 -26
  98. package/dist/lib/tasks/taskManager.d.ts +3 -0
  99. package/dist/lib/tasks/taskManager.js +63 -30
  100. package/dist/lib/telemetry/index.d.ts +2 -1
  101. package/dist/lib/telemetry/index.js +2 -1
  102. package/dist/lib/telemetry/telemetryService.d.ts +3 -0
  103. package/dist/lib/telemetry/telemetryService.js +65 -5
  104. package/dist/lib/types/cli.d.ts +10 -0
  105. package/dist/lib/types/proxyTypes.d.ts +37 -5
  106. package/dist/lib/types/streamTypes.d.ts +25 -3
  107. package/dist/lib/utils/messageBuilder.js +3 -2
  108. package/dist/lib/utils/providerHealth.d.ts +18 -0
  109. package/dist/lib/utils/providerHealth.js +240 -9
  110. package/dist/lib/utils/providerUtils.js +14 -8
  111. package/dist/lib/utils/toolChoice.d.ts +4 -0
  112. package/dist/lib/utils/toolChoice.js +7 -0
  113. package/dist/neurolink.d.ts +3 -2
  114. package/dist/neurolink.js +260 -494
  115. package/dist/observability/otelBridge.d.ts +2 -2
  116. package/dist/observability/otelBridge.js +12 -3
  117. package/dist/providers/amazonBedrock.js +2 -4
  118. package/dist/providers/anthropic.d.ts +9 -5
  119. package/dist/providers/anthropic.js +19 -14
  120. package/dist/providers/anthropicBaseProvider.d.ts +3 -3
  121. package/dist/providers/anthropicBaseProvider.js +5 -4
  122. package/dist/providers/azureOpenai.d.ts +1 -1
  123. package/dist/providers/azureOpenai.js +5 -4
  124. package/dist/providers/googleAiStudio.js +30 -1
  125. package/dist/providers/googleVertex.js +28 -6
  126. package/dist/providers/huggingFace.d.ts +3 -3
  127. package/dist/providers/huggingFace.js +6 -7
  128. package/dist/providers/litellm.js +41 -29
  129. package/dist/providers/mistral.js +2 -1
  130. package/dist/providers/ollama.js +80 -23
  131. package/dist/providers/openAI.js +3 -2
  132. package/dist/providers/openRouter.js +2 -1
  133. package/dist/providers/openaiCompatible.d.ts +4 -4
  134. package/dist/providers/openaiCompatible.js +4 -3
  135. package/dist/proxy/claudeFormat.d.ts +3 -2
  136. package/dist/proxy/claudeFormat.js +25 -20
  137. package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  138. package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  139. package/dist/proxy/modelRouter.js +3 -0
  140. package/dist/proxy/oauthFetch.d.ts +1 -1
  141. package/dist/proxy/oauthFetch.js +65 -72
  142. package/dist/proxy/proxyConfig.js +44 -24
  143. package/dist/proxy/proxyEnv.d.ts +19 -0
  144. package/dist/proxy/proxyEnv.js +72 -0
  145. package/dist/proxy/proxyFetch.js +50 -4
  146. package/dist/proxy/proxyTracer.d.ts +133 -0
  147. package/dist/proxy/proxyTracer.js +644 -0
  148. package/dist/proxy/rawStreamCapture.d.ts +10 -0
  149. package/dist/proxy/rawStreamCapture.js +82 -0
  150. package/dist/proxy/requestLogger.d.ts +32 -5
  151. package/dist/proxy/requestLogger.js +406 -37
  152. package/dist/proxy/sseInterceptor.d.ts +97 -0
  153. package/dist/proxy/sseInterceptor.js +401 -0
  154. package/dist/proxy/usageStats.d.ts +4 -3
  155. package/dist/proxy/usageStats.js +25 -12
  156. package/dist/rag/chunkers/MarkdownChunker.js +13 -5
  157. package/dist/rag/chunking/markdownChunker.js +15 -6
  158. package/dist/server/routes/claudeProxyRoutes.d.ts +7 -2
  159. package/dist/server/routes/claudeProxyRoutes.js +1737 -508
  160. package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
  161. package/dist/services/server/ai/observability/instrumentation.js +240 -40
  162. package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
  163. package/dist/tasks/backends/bullmqBackend.js +14 -7
  164. package/dist/tasks/store/redisTaskStore.d.ts +1 -0
  165. package/dist/tasks/store/redisTaskStore.js +34 -26
  166. package/dist/tasks/taskManager.d.ts +3 -0
  167. package/dist/tasks/taskManager.js +63 -30
  168. package/dist/telemetry/index.d.ts +2 -1
  169. package/dist/telemetry/index.js +2 -1
  170. package/dist/telemetry/telemetryService.d.ts +3 -0
  171. package/dist/telemetry/telemetryService.js +65 -5
  172. package/dist/types/cli.d.ts +10 -0
  173. package/dist/types/proxyTypes.d.ts +37 -5
  174. package/dist/types/streamTypes.d.ts +25 -3
  175. package/dist/utils/messageBuilder.js +3 -2
  176. package/dist/utils/providerHealth.d.ts +18 -0
  177. package/dist/utils/providerHealth.js +240 -9
  178. package/dist/utils/providerUtils.js +14 -8
  179. package/dist/utils/toolChoice.d.ts +4 -0
  180. package/dist/utils/toolChoice.js +6 -0
  181. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
  182. package/docs/changelog.md +252 -0
  183. package/package.json +17 -1
  184. package/scripts/observability/check-proxy-telemetry.mjs +235 -0
  185. package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
  186. package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
  187. package/scripts/observability/manage-local-openobserve.sh +184 -0
  188. package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
  189. package/scripts/observability/proxy-observability.env.example +23 -0
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { LangfuseSpanProcessor } from "@langfuse/otel";
10
10
  import { trace } from "@opentelemetry/api";
11
- import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
11
+ import { LoggerProvider } from "@opentelemetry/sdk-logs";
12
+ import { type SpanProcessor } from "@opentelemetry/sdk-trace-base";
12
13
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
13
14
  import type { LangfuseConfig } from "../../../../types/observability.js";
14
15
  /**
@@ -104,6 +105,11 @@ export declare function getLangfuseSpanProcessor(): LangfuseSpanProcessor | null
104
105
  * Get the tracer provider
105
106
  */
106
107
  export declare function getTracerProvider(): NodeTracerProvider | null;
108
+ /**
109
+ * Get the logger provider for emitting OTLP log records.
110
+ * Returns null if OTLP is not configured or LoggerProvider was not created.
111
+ */
112
+ export declare function getLoggerProvider(): LoggerProvider | null;
107
113
  /**
108
114
  * Check if OpenTelemetry is initialized
109
115
  */
@@ -7,8 +7,15 @@
7
7
  * Flow: Vercel AI SDK → OpenTelemetry Spans → LangfuseSpanProcessor → Langfuse Platform
8
8
  */
9
9
  import { LangfuseSpanProcessor } from "@langfuse/otel";
10
- import { trace } from "@opentelemetry/api";
10
+ import { metrics, trace } from "@opentelemetry/api";
11
+ import { W3CTraceContextPropagator } from "@opentelemetry/core";
12
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
13
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
14
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
11
15
  import { resourceFromAttributes } from "@opentelemetry/resources";
16
+ import { MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics";
17
+ import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
18
+ import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
12
19
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
13
20
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
14
21
  import { AsyncLocalStorage } from "async_hooks";
@@ -16,6 +23,8 @@ import { logger } from "../../../../utils/logger.js";
16
23
  const LOG_PREFIX = "[OpenTelemetry]";
17
24
  const contextStorage = new AsyncLocalStorage();
18
25
  let tracerProvider = null;
26
+ let meterProvider = null;
27
+ let loggerProvider = null;
19
28
  let langfuseProcessor = null;
20
29
  let isInitialized = false;
21
30
  let isCredentialsValid = false;
@@ -453,58 +462,179 @@ export function initializeOpenTelemetry(config) {
453
462
  return;
454
463
  }
455
464
  }
456
- // THEN: Check enabled for standalone mode
457
- if (!config?.enabled) {
458
- logger.debug(`${LOG_PREFIX} Langfuse disabled and no external provider, skipping initialization`);
465
+ const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
466
+ const langfuseRequested = config?.enabled === true;
467
+ const hasLangfuseCreds = !!config.publicKey && !!config.secretKey;
468
+ // THEN: Check whether we have any standalone observability backend at all.
469
+ if ((!langfuseRequested || !hasLangfuseCreds) && !otlpEndpoint) {
470
+ if (langfuseRequested && !hasLangfuseCreds) {
471
+ logger.warn(`${LOG_PREFIX} Langfuse requested but credentials are missing, and no OTLP endpoint is configured; skipping initialization`, {
472
+ hasPublicKey: !!config.publicKey,
473
+ hasSecretKey: !!config.secretKey,
474
+ });
475
+ }
476
+ else {
477
+ logger.debug(`${LOG_PREFIX} Langfuse disabled and OTLP endpoint missing, skipping initialization`);
478
+ }
459
479
  isInitialized = true;
460
480
  return;
461
481
  }
462
- // Validate credentials for standalone mode
463
- if (!config.publicKey || !config.secretKey) {
464
- logger.warn(`${LOG_PREFIX} Langfuse enabled but missing credentials, skipping initialization`, {
482
+ if (langfuseRequested && !hasLangfuseCreds) {
483
+ logger.warn(`${LOG_PREFIX} Langfuse requested but credentials are missing; continuing with OTLP-only telemetry`, {
465
484
  hasPublicKey: !!config.publicKey,
466
485
  hasSecretKey: !!config.secretKey,
486
+ otlpEnabled: !!otlpEndpoint,
467
487
  });
468
- isInitialized = true;
469
- isCredentialsValid = false;
470
- return;
471
488
  }
472
489
  try {
473
490
  currentConfig = config;
474
- isCredentialsValid = true;
475
- // Step 1: Create LangfuseSpanProcessor for standalone mode
476
- // shouldExportSpan: export all spans (v5 default filters to gen_ai spans only)
477
- langfuseProcessor = new LangfuseSpanProcessor({
478
- publicKey: config.publicKey,
479
- secretKey: config.secretKey,
480
- baseUrl: config.baseUrl || "https://cloud.langfuse.com",
481
- environment: config.environment || "dev",
482
- release: config.release || "v1.0.0",
483
- shouldExportSpan: () => true,
484
- });
485
- logger.debug(`${LOG_PREFIX} Created LangfuseSpanProcessor`, {
491
+ isCredentialsValid = hasLangfuseCreds;
492
+ // Step 1: Create LangfuseSpanProcessor only when Langfuse is explicitly enabled
493
+ // with real credentials. OTLP-only mode is valid and should not construct one.
494
+ if (langfuseRequested && hasLangfuseCreds) {
495
+ // shouldExportSpan: export all spans (v5 default filters to gen_ai spans only)
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
+ }
508
+ logger.debug(`${LOG_PREFIX} Standalone observability mode`, {
509
+ langfuseEnabled: !!langfuseProcessor,
510
+ otlpEnabled: !!otlpEndpoint,
486
511
  baseUrl: config.baseUrl || "https://cloud.langfuse.com",
487
512
  environment: config.environment || "dev",
488
513
  });
489
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";
490
517
  const resource = resourceFromAttributes({
491
- [ATTR_SERVICE_NAME]: "neurolink",
518
+ [ATTR_SERVICE_NAME]: serviceName,
492
519
  [ATTR_SERVICE_VERSION]: config.release || "v1.0.0",
493
520
  "deployment.environment": config.environment || "dev",
494
521
  });
522
+ // Build span processor list
523
+ const spanProcessors = [new ContextEnricher()];
524
+ if (langfuseProcessor) {
525
+ spanProcessors.push(langfuseProcessor);
526
+ }
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
+ if (otlpEndpoint) {
530
+ try {
531
+ const otlpExporter = new OTLPTraceExporter({
532
+ url: `${otlpEndpoint}/v1/traces`,
533
+ });
534
+ const otlpBatchProcessor = new BatchSpanProcessor(otlpExporter, {
535
+ maxQueueSize: 2048,
536
+ maxExportBatchSize: 512,
537
+ scheduledDelayMillis: 1000,
538
+ exportTimeoutMillis: 30000,
539
+ });
540
+ spanProcessors.push(otlpBatchProcessor);
541
+ logger.info(`${LOG_PREFIX} OTLP trace exporter added`, {
542
+ endpoint: `${otlpEndpoint}/v1/traces`,
543
+ serviceName,
544
+ });
545
+ }
546
+ catch (otlpError) {
547
+ logger.warn(`${LOG_PREFIX} Failed to create OTLP exporter (non-fatal)`, {
548
+ error: otlpError instanceof Error
549
+ ? otlpError.message
550
+ : String(otlpError),
551
+ endpoint: otlpEndpoint,
552
+ });
553
+ }
554
+ }
495
555
  tracerProvider = new NodeTracerProvider({
496
556
  resource,
497
- spanProcessors: [new ContextEnricher(), langfuseProcessor],
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.
562
+ tracerProvider.register({
563
+ propagator: new W3CTraceContextPropagator(),
498
564
  });
499
- // Step 4: Register globally
500
- tracerProvider.register();
501
565
  usingExternalProvider = false;
502
566
  isInitialized = true;
503
- logger.info(`${LOG_PREFIX} Initialized with Langfuse span processor`, {
567
+ // Step 5: If OTLP endpoint is set, also set up MeterProvider for metrics export
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
+ }
630
+ logger.info(`${LOG_PREFIX} Observability initialized`, {
504
631
  baseUrl: config.baseUrl || "https://cloud.langfuse.com",
505
632
  environment: config.environment || "dev",
506
633
  release: config.release || "v1.0.0",
507
634
  mode: "standalone",
635
+ langfuseEnabled: !!langfuseProcessor,
636
+ otlpEnabled: !!otlpEndpoint,
637
+ serviceName,
508
638
  });
509
639
  }
510
640
  catch (error) {
@@ -540,22 +670,75 @@ export async function flushOpenTelemetry() {
540
670
  logger.debug(`${LOG_PREFIX} Not initialized, skipping flush`);
541
671
  return;
542
672
  }
543
- if (!langfuseProcessor) {
544
- logger.debug(`${LOG_PREFIX} No processor to flush (Langfuse disabled)`);
545
- return;
673
+ const failures = [];
674
+ if (langfuseProcessor) {
675
+ try {
676
+ logger.info(`${LOG_PREFIX} Flushing Langfuse spans...`);
677
+ await langfuseProcessor.forceFlush();
678
+ }
679
+ catch (error) {
680
+ failures.push({ signal: "langfuse", error });
681
+ logger.error(`${LOG_PREFIX} Langfuse flush failed`, {
682
+ error: error instanceof Error ? error.message : String(error),
683
+ stack: error instanceof Error ? error.stack : undefined,
684
+ });
685
+ }
546
686
  }
547
- try {
548
- logger.info(`${LOG_PREFIX} Flushing pending spans to Langfuse...`);
549
- await langfuseProcessor.forceFlush();
550
- logger.info(`${LOG_PREFIX} Successfully flushed spans to Langfuse`);
687
+ else {
688
+ logger.debug(`${LOG_PREFIX} Langfuse disabled, skipping Langfuse flush`);
551
689
  }
552
- catch (error) {
553
- logger.error(`${LOG_PREFIX} Flush failed`, {
554
- error: error instanceof Error ? error.message : String(error),
555
- stack: error instanceof Error ? error.stack : undefined,
556
- });
557
- throw error;
690
+ if (tracerProvider && !usingExternalProvider) {
691
+ try {
692
+ logger.info(`${LOG_PREFIX} Flushing OTLP traces...`);
693
+ await tracerProvider.forceFlush();
694
+ }
695
+ catch (error) {
696
+ failures.push({ signal: "traces", error });
697
+ logger.error(`${LOG_PREFIX} Trace flush failed`, {
698
+ error: error instanceof Error ? error.message : String(error),
699
+ stack: error instanceof Error ? error.stack : undefined,
700
+ });
701
+ }
702
+ }
703
+ else {
704
+ logger.debug(`${LOG_PREFIX} No TracerProvider to flush`);
558
705
  }
706
+ if (meterProvider) {
707
+ try {
708
+ logger.info(`${LOG_PREFIX} Flushing OTLP metrics...`);
709
+ await meterProvider.forceFlush();
710
+ }
711
+ catch (error) {
712
+ failures.push({ signal: "metrics", error });
713
+ logger.error(`${LOG_PREFIX} Metric flush failed`, {
714
+ error: error instanceof Error ? error.message : String(error),
715
+ stack: error instanceof Error ? error.stack : undefined,
716
+ });
717
+ }
718
+ }
719
+ else {
720
+ logger.debug(`${LOG_PREFIX} No MeterProvider to flush`);
721
+ }
722
+ if (loggerProvider) {
723
+ try {
724
+ logger.info(`${LOG_PREFIX} Flushing OTLP logs...`);
725
+ await loggerProvider.forceFlush();
726
+ }
727
+ catch (error) {
728
+ failures.push({ signal: "logs", error });
729
+ logger.error(`${LOG_PREFIX} Log flush failed`, {
730
+ error: error instanceof Error ? error.message : String(error),
731
+ stack: error instanceof Error ? error.stack : undefined,
732
+ });
733
+ }
734
+ }
735
+ else {
736
+ logger.debug(`${LOG_PREFIX} No LoggerProvider to flush`);
737
+ }
738
+ if (failures.length > 0) {
739
+ throw new Error(`${LOG_PREFIX} Flush failed for: ${failures.map((f) => f.signal).join(", ")}`);
740
+ }
741
+ logger.info(`${LOG_PREFIX} Flush complete`);
559
742
  }
560
743
  /**
561
744
  * Shutdown OpenTelemetry and Langfuse span processor
@@ -577,7 +760,17 @@ export async function shutdownOpenTelemetry() {
577
760
  if (cachedContextEnricher) {
578
761
  await cachedContextEnricher.shutdown();
579
762
  }
763
+ // Shutdown MeterProvider if we created it
764
+ if (meterProvider) {
765
+ await meterProvider.shutdown();
766
+ }
767
+ // Shutdown LoggerProvider if we created it
768
+ if (loggerProvider) {
769
+ await loggerProvider.shutdown();
770
+ }
580
771
  tracerProvider = null;
772
+ meterProvider = null;
773
+ loggerProvider = null;
581
774
  langfuseProcessor = null;
582
775
  cachedContextEnricher = null;
583
776
  isInitialized = false;
@@ -603,6 +796,13 @@ export function getLangfuseSpanProcessor() {
603
796
  export function getTracerProvider() {
604
797
  return tracerProvider;
605
798
  }
799
+ /**
800
+ * Get the logger provider for emitting OTLP log records.
801
+ * Returns null if OTLP is not configured or LoggerProvider was not created.
802
+ */
803
+ export function getLoggerProvider() {
804
+ return loggerProvider;
805
+ }
606
806
  /**
607
807
  * Check if OpenTelemetry is initialized
608
808
  */
@@ -29,4 +29,5 @@ export declare class BullMQBackend implements TaskBackend {
29
29
  */
30
30
  private getConnectionConfig;
31
31
  private ensureInitialized;
32
+ private getQueue;
32
33
  }
@@ -62,23 +62,23 @@ export class BullMQBackend {
62
62
  logger.info("[BullMQ] Backend shut down");
63
63
  }
64
64
  async schedule(task, executor) {
65
- this.ensureInitialized();
65
+ const queue = this.getQueue();
66
66
  this.executors.set(task.id, executor);
67
67
  const jobData = { taskId: task.id, task };
68
68
  const schedule = task.schedule;
69
69
  if (schedule.type === "cron") {
70
- await this.queue.upsertJobScheduler(task.id, {
70
+ await queue.upsertJobScheduler(task.id, {
71
71
  pattern: schedule.expression,
72
72
  ...(schedule.timezone ? { tz: schedule.timezone } : {}),
73
73
  }, { name: task.name, data: jobData });
74
74
  }
75
75
  else if (schedule.type === "interval") {
76
- await this.queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
76
+ await queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
77
77
  }
78
78
  else if (schedule.type === "once") {
79
79
  const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
80
80
  const delay = Math.max(0, at.getTime() - Date.now());
81
- await this.queue.add(task.name, jobData, {
81
+ await queue.add(task.name, jobData, {
82
82
  jobId: task.id,
83
83
  delay,
84
84
  });
@@ -89,18 +89,18 @@ export class BullMQBackend {
89
89
  });
90
90
  }
91
91
  async cancel(taskId) {
92
- this.ensureInitialized();
92
+ const queue = this.getQueue();
93
93
  this.executors.delete(taskId);
94
94
  // Remove repeatable job scheduler
95
95
  try {
96
- await this.queue.removeJobScheduler(taskId);
96
+ await queue.removeJobScheduler(taskId);
97
97
  }
98
98
  catch {
99
99
  // May not be a repeatable job — try removing by job ID
100
100
  }
101
101
  // Remove delayed/waiting job
102
102
  try {
103
- const job = await this.queue.getJob(taskId);
103
+ const job = await queue.getJob(taskId);
104
104
  if (job) {
105
105
  await job.remove();
106
106
  }
@@ -185,5 +185,12 @@ export class BullMQBackend {
185
185
  throw TaskError.create("BACKEND_NOT_INITIALIZED", "[BullMQ] Backend not initialized. Call initialize() first.");
186
186
  }
187
187
  }
188
+ getQueue() {
189
+ this.ensureInitialized();
190
+ if (!this.queue) {
191
+ throw TaskError.create("BACKEND_NOT_INITIALIZED", "[BullMQ] Queue is unavailable after initialization.");
192
+ }
193
+ return this.queue;
194
+ }
188
195
  }
189
196
  //# sourceMappingURL=bullmqBackend.js.map
@@ -34,6 +34,7 @@ export declare class RedisTaskStore implements TaskStore {
34
34
  getHistory(taskId: string): Promise<ConversationEntry[]>;
35
35
  clearHistory(taskId: string): Promise<void>;
36
36
  private ensureConnected;
37
+ private getClient;
37
38
  /**
38
39
  * Set Redis TTL on terminal-state tasks so they auto-expire.
39
40
  * Active and paused tasks never expire.
@@ -61,21 +61,21 @@ export class RedisTaskStore {
61
61
  }
62
62
  // ── Task CRUD ───────────────────────────────────────────
63
63
  async save(task) {
64
- this.ensureConnected();
65
- await this.client.hSet(TASKS_HASH, task.id, JSON.stringify(task));
64
+ const client = this.getClient();
65
+ await client.hSet(TASKS_HASH, task.id, JSON.stringify(task));
66
66
  this.applyRetentionTTL(task);
67
67
  }
68
68
  async get(taskId) {
69
- this.ensureConnected();
70
- const data = await this.client.hGet(TASKS_HASH, taskId);
69
+ const client = this.getClient();
70
+ const data = await client.hGet(TASKS_HASH, taskId);
71
71
  if (!data) {
72
72
  return null;
73
73
  }
74
74
  return JSON.parse(String(data));
75
75
  }
76
76
  async list(filter) {
77
- this.ensureConnected();
78
- const all = await this.client.hGetAll(TASKS_HASH);
77
+ const client = this.getClient();
78
+ const all = await client.hGetAll(TASKS_HASH);
79
79
  let tasks = Object.values(all).map((v) => JSON.parse(String(v)));
80
80
  if (filter?.status) {
81
81
  tasks = tasks.filter((t) => t.status === filter.status);
@@ -83,7 +83,7 @@ export class RedisTaskStore {
83
83
  return tasks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
84
84
  }
85
85
  async update(taskId, updates) {
86
- this.ensureConnected();
86
+ const client = this.getClient();
87
87
  const existing = await this.get(taskId);
88
88
  if (!existing) {
89
89
  throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
@@ -94,35 +94,35 @@ export class RedisTaskStore {
94
94
  id: existing.id, // ID is immutable
95
95
  updatedAt: new Date().toISOString(),
96
96
  };
97
- await this.client.hSet(TASKS_HASH, taskId, JSON.stringify(updated));
97
+ await client.hSet(TASKS_HASH, taskId, JSON.stringify(updated));
98
98
  this.applyRetentionTTL(updated);
99
99
  return updated;
100
100
  }
101
101
  async delete(taskId) {
102
- this.ensureConnected();
102
+ const client = this.getClient();
103
103
  await Promise.all([
104
- this.client.hDel(TASKS_HASH, taskId),
105
- this.client.del(taskRunsKey(taskId)),
106
- this.client.del(taskHistoryKey(taskId)),
104
+ client.hDel(TASKS_HASH, taskId),
105
+ client.del(taskRunsKey(taskId)),
106
+ client.del(taskHistoryKey(taskId)),
107
107
  ]);
108
108
  }
109
109
  // ── Run Logs ──────────────────────────────────────────
110
110
  async appendRun(taskId, run) {
111
- this.ensureConnected();
111
+ const client = this.getClient();
112
112
  const key = taskRunsKey(taskId);
113
- await this.client.lPush(key, JSON.stringify(run));
113
+ await client.lPush(key, JSON.stringify(run));
114
114
  // Trim to keep only the latest maxRunLogs entries
115
- await this.client.lTrim(key, 0, this.maxRunLogs - 1);
115
+ await client.lTrim(key, 0, this.maxRunLogs - 1);
116
116
  }
117
117
  async getRuns(taskId, options) {
118
- this.ensureConnected();
118
+ const client = this.getClient();
119
119
  const limit = options?.limit ?? 20;
120
120
  const key = taskRunsKey(taskId);
121
121
  // When a status filter is applied, we need to fetch more items than `limit`
122
122
  // because post-filter may discard many entries. Fetch all (-1) when filtering,
123
123
  // otherwise fetch exactly `limit` items.
124
124
  const fetchEnd = options?.status ? -1 : limit - 1;
125
- const items = await this.client.lRange(key, 0, fetchEnd);
125
+ const items = await client.lRange(key, 0, fetchEnd);
126
126
  let runs = items.map((v) => JSON.parse(String(v)));
127
127
  if (options?.status) {
128
128
  runs = runs.filter((r) => r.status === options.status);
@@ -131,24 +131,24 @@ export class RedisTaskStore {
131
131
  }
132
132
  // ── Continuation History ──────────────────────────────
133
133
  async appendHistory(taskId, messages) {
134
- this.ensureConnected();
134
+ const client = this.getClient();
135
135
  const key = taskHistoryKey(taskId);
136
136
  const serialized = messages.map((m) => JSON.stringify(m));
137
137
  if (serialized.length > 0) {
138
- await this.client.rPush(key, serialized);
138
+ await client.rPush(key, serialized);
139
139
  // Trim to keep only the most recent entries, preventing unbounded growth
140
- await this.client.lTrim(key, -this.maxHistoryEntries, -1);
140
+ await client.lTrim(key, -this.maxHistoryEntries, -1);
141
141
  }
142
142
  }
143
143
  async getHistory(taskId) {
144
- this.ensureConnected();
144
+ const client = this.getClient();
145
145
  const key = taskHistoryKey(taskId);
146
- const items = await this.client.lRange(key, 0, -1);
146
+ const items = await client.lRange(key, 0, -1);
147
147
  return items.map((v) => JSON.parse(String(v)));
148
148
  }
149
149
  async clearHistory(taskId) {
150
- this.ensureConnected();
151
- await this.client.del(taskHistoryKey(taskId));
150
+ const client = this.getClient();
151
+ await client.del(taskHistoryKey(taskId));
152
152
  }
153
153
  // ── Internal ──────────────────────────────────────────
154
154
  ensureConnected() {
@@ -156,6 +156,13 @@ export class RedisTaskStore {
156
156
  throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskStore:Redis] Not connected. Call initialize() first.");
157
157
  }
158
158
  }
159
+ getClient() {
160
+ this.ensureConnected();
161
+ if (!this.client) {
162
+ throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskStore:Redis] Client is unavailable after initialization.");
163
+ }
164
+ return this.client;
165
+ }
159
166
  /**
160
167
  * Set Redis TTL on terminal-state tasks so they auto-expire.
161
168
  * Active and paused tasks never expire.
@@ -171,14 +178,15 @@ export class RedisTaskStore {
171
178
  };
172
179
  const ttlMs = ttlMap[task.status];
173
180
  if (ttlMs) {
181
+ const client = this.getClient();
174
182
  const ttlSeconds = Math.ceil(ttlMs / 1000);
175
183
  // Set TTL on associated keys
176
- this.client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => {
184
+ client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => {
177
185
  logger.debug("[TaskStore:Redis] Failed to set TTL", {
178
186
  error: String(err),
179
187
  });
180
188
  });
181
- this.client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => {
189
+ client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => {
182
190
  logger.debug("[TaskStore:Redis] Failed to set TTL", {
183
191
  error: String(err),
184
192
  });
@@ -28,6 +28,9 @@ export declare class TaskManager {
28
28
  }): void;
29
29
  private ensureInitialized;
30
30
  private doInitialize;
31
+ private getStore;
32
+ private getBackend;
33
+ private getExecutor;
31
34
  create(definition: TaskDefinition): Promise<Task>;
32
35
  get(taskId: string): Promise<Task | null>;
33
36
  list(filter?: {