@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.
- package/CHANGELOG.md +6 -0
- package/README.md +7 -1
- package/dist/auth/anthropicOAuth.d.ts +18 -3
- package/dist/auth/anthropicOAuth.js +137 -4
- package/dist/auth/providers/firebase.js +5 -1
- package/dist/auth/providers/jwt.js +5 -1
- package/dist/auth/providers/workos.js +5 -1
- package/dist/auth/sessionManager.d.ts +1 -1
- package/dist/auth/sessionManager.js +58 -27
- package/dist/browser/neurolink.min.js +337 -318
- package/dist/cli/commands/mcp.js +3 -0
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +279 -16
- package/dist/cli/commands/task.js +3 -0
- package/dist/cli/factories/commandFactory.d.ts +2 -0
- package/dist/cli/factories/commandFactory.js +38 -0
- package/dist/cli/parser.js +4 -3
- package/dist/client/aiSdkAdapter.js +3 -0
- package/dist/client/streamingClient.js +30 -10
- package/dist/core/modules/GenerationHandler.js +3 -2
- package/dist/core/redisConversationMemoryManager.js +7 -3
- package/dist/evaluation/BatchEvaluator.js +4 -1
- package/dist/evaluation/hooks/observabilityHooks.js +5 -3
- package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
- package/dist/evaluation/pipeline/evaluationPipeline.js +20 -8
- package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
- package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
- package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
- package/dist/lib/auth/anthropicOAuth.js +137 -4
- package/dist/lib/auth/providers/firebase.js +5 -1
- package/dist/lib/auth/providers/jwt.js +5 -1
- package/dist/lib/auth/providers/workos.js +5 -1
- package/dist/lib/auth/sessionManager.d.ts +1 -1
- package/dist/lib/auth/sessionManager.js +58 -27
- package/dist/lib/client/aiSdkAdapter.js +3 -0
- package/dist/lib/client/streamingClient.js +30 -10
- package/dist/lib/core/modules/GenerationHandler.js +3 -2
- package/dist/lib/core/redisConversationMemoryManager.js +7 -3
- package/dist/lib/evaluation/BatchEvaluator.js +4 -1
- package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
- package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +20 -8
- package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
- package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
- package/dist/lib/neurolink.d.ts +3 -2
- package/dist/lib/neurolink.js +260 -494
- package/dist/lib/observability/otelBridge.d.ts +2 -2
- package/dist/lib/observability/otelBridge.js +12 -3
- package/dist/lib/providers/amazonBedrock.js +2 -4
- package/dist/lib/providers/anthropic.d.ts +9 -5
- package/dist/lib/providers/anthropic.js +19 -14
- package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
- package/dist/lib/providers/anthropicBaseProvider.js +5 -4
- package/dist/lib/providers/azureOpenai.d.ts +1 -1
- package/dist/lib/providers/azureOpenai.js +5 -4
- package/dist/lib/providers/googleAiStudio.js +30 -1
- package/dist/lib/providers/googleVertex.js +28 -6
- package/dist/lib/providers/huggingFace.d.ts +3 -3
- package/dist/lib/providers/huggingFace.js +6 -8
- package/dist/lib/providers/litellm.js +41 -29
- package/dist/lib/providers/mistral.js +2 -1
- package/dist/lib/providers/ollama.js +80 -23
- package/dist/lib/providers/openAI.js +3 -2
- package/dist/lib/providers/openRouter.js +2 -1
- package/dist/lib/providers/openaiCompatible.d.ts +4 -4
- package/dist/lib/providers/openaiCompatible.js +4 -4
- package/dist/lib/proxy/claudeFormat.d.ts +3 -2
- package/dist/lib/proxy/claudeFormat.js +25 -20
- package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
- package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
- package/dist/lib/proxy/modelRouter.js +3 -0
- package/dist/lib/proxy/oauthFetch.d.ts +1 -1
- package/dist/lib/proxy/oauthFetch.js +65 -72
- package/dist/lib/proxy/proxyConfig.js +44 -24
- package/dist/lib/proxy/proxyEnv.d.ts +19 -0
- package/dist/lib/proxy/proxyEnv.js +73 -0
- package/dist/lib/proxy/proxyFetch.js +50 -4
- package/dist/lib/proxy/proxyTracer.d.ts +133 -0
- package/dist/lib/proxy/proxyTracer.js +645 -0
- package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
- package/dist/lib/proxy/rawStreamCapture.js +83 -0
- package/dist/lib/proxy/requestLogger.d.ts +32 -5
- package/dist/lib/proxy/requestLogger.js +406 -37
- package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
- package/dist/lib/proxy/sseInterceptor.js +402 -0
- package/dist/lib/proxy/usageStats.d.ts +4 -3
- package/dist/lib/proxy/usageStats.js +25 -12
- package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
- package/dist/lib/rag/chunking/markdownChunker.js +15 -6
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +7 -2
- package/dist/lib/server/routes/claudeProxyRoutes.js +1737 -508
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
- package/dist/lib/services/server/ai/observability/instrumentation.js +240 -40
- package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
- package/dist/lib/tasks/backends/bullmqBackend.js +14 -7
- package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
- package/dist/lib/tasks/store/redisTaskStore.js +34 -26
- package/dist/lib/tasks/taskManager.d.ts +3 -0
- package/dist/lib/tasks/taskManager.js +63 -30
- package/dist/lib/telemetry/index.d.ts +2 -1
- package/dist/lib/telemetry/index.js +2 -1
- package/dist/lib/telemetry/telemetryService.d.ts +3 -0
- package/dist/lib/telemetry/telemetryService.js +65 -5
- package/dist/lib/types/cli.d.ts +10 -0
- package/dist/lib/types/proxyTypes.d.ts +37 -5
- package/dist/lib/types/streamTypes.d.ts +25 -3
- package/dist/lib/utils/messageBuilder.js +3 -2
- package/dist/lib/utils/providerHealth.d.ts +18 -0
- package/dist/lib/utils/providerHealth.js +240 -9
- package/dist/lib/utils/providerUtils.js +14 -8
- package/dist/lib/utils/toolChoice.d.ts +4 -0
- package/dist/lib/utils/toolChoice.js +7 -0
- package/dist/neurolink.d.ts +3 -2
- package/dist/neurolink.js +260 -494
- package/dist/observability/otelBridge.d.ts +2 -2
- package/dist/observability/otelBridge.js +12 -3
- package/dist/providers/amazonBedrock.js +2 -4
- package/dist/providers/anthropic.d.ts +9 -5
- package/dist/providers/anthropic.js +19 -14
- package/dist/providers/anthropicBaseProvider.d.ts +3 -3
- package/dist/providers/anthropicBaseProvider.js +5 -4
- package/dist/providers/azureOpenai.d.ts +1 -1
- package/dist/providers/azureOpenai.js +5 -4
- package/dist/providers/googleAiStudio.js +30 -1
- package/dist/providers/googleVertex.js +28 -6
- package/dist/providers/huggingFace.d.ts +3 -3
- package/dist/providers/huggingFace.js +6 -7
- package/dist/providers/litellm.js +41 -29
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/ollama.js +80 -23
- package/dist/providers/openAI.js +3 -2
- package/dist/providers/openRouter.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +4 -4
- package/dist/providers/openaiCompatible.js +4 -3
- package/dist/proxy/claudeFormat.d.ts +3 -2
- package/dist/proxy/claudeFormat.js +25 -20
- package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
- package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
- package/dist/proxy/modelRouter.js +3 -0
- package/dist/proxy/oauthFetch.d.ts +1 -1
- package/dist/proxy/oauthFetch.js +65 -72
- package/dist/proxy/proxyConfig.js +44 -24
- package/dist/proxy/proxyEnv.d.ts +19 -0
- package/dist/proxy/proxyEnv.js +72 -0
- package/dist/proxy/proxyFetch.js +50 -4
- package/dist/proxy/proxyTracer.d.ts +133 -0
- package/dist/proxy/proxyTracer.js +644 -0
- package/dist/proxy/rawStreamCapture.d.ts +10 -0
- package/dist/proxy/rawStreamCapture.js +82 -0
- package/dist/proxy/requestLogger.d.ts +32 -5
- package/dist/proxy/requestLogger.js +406 -37
- package/dist/proxy/sseInterceptor.d.ts +97 -0
- package/dist/proxy/sseInterceptor.js +401 -0
- package/dist/proxy/usageStats.d.ts +4 -3
- package/dist/proxy/usageStats.js +25 -12
- package/dist/rag/chunkers/MarkdownChunker.js +13 -5
- package/dist/rag/chunking/markdownChunker.js +15 -6
- package/dist/server/routes/claudeProxyRoutes.d.ts +7 -2
- package/dist/server/routes/claudeProxyRoutes.js +1737 -508
- package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
- package/dist/services/server/ai/observability/instrumentation.js +240 -40
- package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
- package/dist/tasks/backends/bullmqBackend.js +14 -7
- package/dist/tasks/store/redisTaskStore.d.ts +1 -0
- package/dist/tasks/store/redisTaskStore.js +34 -26
- package/dist/tasks/taskManager.d.ts +3 -0
- package/dist/tasks/taskManager.js +63 -30
- package/dist/telemetry/index.d.ts +2 -1
- package/dist/telemetry/index.js +2 -1
- package/dist/telemetry/telemetryService.d.ts +3 -0
- package/dist/telemetry/telemetryService.js +65 -5
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/proxyTypes.d.ts +37 -5
- package/dist/types/streamTypes.d.ts +25 -3
- package/dist/utils/messageBuilder.js +3 -2
- package/dist/utils/providerHealth.d.ts +18 -0
- package/dist/utils/providerHealth.js +240 -9
- package/dist/utils/providerUtils.js +14 -8
- package/dist/utils/toolChoice.d.ts +4 -0
- package/dist/utils/toolChoice.js +6 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
- package/docs/changelog.md +252 -0
- package/package.json +17 -1
- package/scripts/observability/check-proxy-telemetry.mjs +235 -0
- package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
- package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
- package/scripts/observability/manage-local-openobserve.sh +184 -0
- package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
- 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
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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 =
|
|
475
|
-
// Step 1: Create LangfuseSpanProcessor
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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]:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
548
|
-
logger.
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
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
|
*/
|
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
92
|
+
const queue = this.getQueue();
|
|
93
93
|
this.executors.delete(taskId);
|
|
94
94
|
// Remove repeatable job scheduler
|
|
95
95
|
try {
|
|
96
|
-
await
|
|
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
|
|
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.
|
|
65
|
-
await
|
|
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.
|
|
70
|
-
const data = await
|
|
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.
|
|
78
|
-
const all = await
|
|
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.
|
|
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
|
|
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.
|
|
102
|
+
const client = this.getClient();
|
|
103
103
|
await Promise.all([
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
111
|
+
const client = this.getClient();
|
|
112
112
|
const key = taskRunsKey(taskId);
|
|
113
|
-
await
|
|
113
|
+
await client.lPush(key, JSON.stringify(run));
|
|
114
114
|
// Trim to keep only the latest maxRunLogs entries
|
|
115
|
-
await
|
|
115
|
+
await client.lTrim(key, 0, this.maxRunLogs - 1);
|
|
116
116
|
}
|
|
117
117
|
async getRuns(taskId, options) {
|
|
118
|
-
this.
|
|
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
|
|
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.
|
|
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
|
|
138
|
+
await client.rPush(key, serialized);
|
|
139
139
|
// Trim to keep only the most recent entries, preventing unbounded growth
|
|
140
|
-
await
|
|
140
|
+
await client.lTrim(key, -this.maxHistoryEntries, -1);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
async getHistory(taskId) {
|
|
144
|
-
this.
|
|
144
|
+
const client = this.getClient();
|
|
145
145
|
const key = taskHistoryKey(taskId);
|
|
146
|
-
const items = await
|
|
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.
|
|
151
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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?: {
|