@juspay/neurolink 13.4.10 → 13.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -3
- package/README.md +3 -2
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
- package/dist/adapters/video/videoAnalyzer.js +4 -0
- package/dist/autoresearch/index.d.ts +1 -0
- package/dist/autoresearch/index.js +4 -0
- package/dist/autoresearch/worker.js +4 -0
- package/dist/browser/neurolink.min.js +466 -466
- package/dist/cli/commands/decide.js +25 -5
- package/dist/cli/commands/observability.js +11 -4
- package/dist/cli/commands/telemetry.js +36 -6
- package/dist/cli/utils/formatters.d.ts +17 -0
- package/dist/cli/utils/formatters.js +39 -0
- package/dist/constants/langfuse.d.ts +17 -1
- package/dist/constants/langfuse.js +23 -1
- package/dist/constants/telemetry.d.ts +6 -0
- package/dist/constants/telemetry.js +6 -0
- package/dist/core/baseProvider.js +2 -0
- package/dist/core/geminiLoopAdapter.js +19 -0
- package/dist/core/loopEngine.js +10 -7
- package/dist/core/modules/StreamHandler.js +5 -4
- package/dist/core/modules/TelemetryHandler.d.ts +5 -1
- package/dist/core/modules/TelemetryHandler.js +16 -0
- package/dist/core/modules/ToolsManager.js +3 -0
- package/dist/core/nativeGenerateLoop.js +1 -1
- package/dist/evaluation/index.d.ts +1 -0
- package/dist/evaluation/index.js +4 -0
- package/dist/evaluation/ragasEvaluator.js +1 -0
- package/dist/factories/providerDescriptors.d.ts +4 -3
- package/dist/factories/providerDescriptors.js +6 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/externalServerManager.js +6 -2
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.d.ts +20 -10
- package/dist/neurolink.js +134 -65
- package/dist/observability/FEATURE-STATUS.md +0 -4
- package/dist/observability/exporterRegistry.d.ts +6 -1
- package/dist/observability/exporterRegistry.js +18 -0
- package/dist/observability/exporterSpanProcessor.d.ts +20 -6
- package/dist/observability/exporterSpanProcessor.js +166 -34
- package/dist/observability/exporters/baseExporter.d.ts +22 -3
- package/dist/observability/exporters/baseExporter.js +87 -5
- package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
- package/dist/observability/exporters/langfuseExporter.js +251 -41
- package/dist/observability/langfuseMasking.d.ts +1 -1
- package/dist/observability/langfuseMasking.js +1 -1
- package/dist/observability/otlpEncoding.d.ts +18 -5
- package/dist/observability/otlpEncoding.js +69 -12
- package/dist/observability/utils/spanSerializer.d.ts +3 -2
- package/dist/observability/utils/spanSerializer.js +5 -3
- package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
- package/dist/processors/base/BaseFileProcessor.js +4 -0
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +4 -0
- package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
- package/dist/processors/registry/ProcessorRegistry.js +5 -0
- package/dist/providers/amazonBedrock/client.js +7 -5
- package/dist/providers/amazonSagemaker.js +32 -12
- package/dist/providers/anthropic/client.js +1 -0
- package/dist/providers/anthropic/loopAdapter.js +7 -3
- package/dist/providers/catalog/provider-catalog.schema.json +64 -3
- package/dist/providers/catalog/schema.js +81 -1
- package/dist/providers/catalog/xor.json +30 -10
- package/dist/providers/configuredDecision.d.ts +8 -1
- package/dist/providers/configuredDecision.js +22 -0
- package/dist/providers/decide/dialects/openaiDecisions.js +8 -0
- package/dist/providers/decide/dialects/systemOne.js +28 -2
- package/dist/providers/decide/guards.d.ts +6 -0
- package/dist/providers/decide/guards.js +22 -0
- package/dist/providers/googleAiStudio/client.js +2 -0
- package/dist/providers/googleVertex/client.d.ts +6 -5
- package/dist/providers/googleVertex/client.js +46 -19
- package/dist/providers/openaiChatCompletionsBase.js +4 -4
- package/dist/providers/systemOneDecision.d.ts +30 -7
- package/dist/providers/systemOneDecision.js +71 -14
- package/dist/proxy/codexUsage.js +59 -2
- package/dist/proxy/proxyTracer.d.ts +25 -1
- package/dist/proxy/proxyTracer.js +146 -22
- package/dist/proxy/proxyTranslationEngine.js +4 -2
- package/dist/proxy/sseInterceptor.js +6 -4
- package/dist/rag/chunkers/BaseChunker.js +1 -0
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +4 -0
- package/dist/rag/ragIntegration.js +1 -0
- package/dist/rag/reranker/reranker.js +2 -0
- package/dist/rag/retrieval/vectorQueryTool.js +1 -0
- package/dist/server/routes/codexProxyRoutes.js +3 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
- package/dist/services/server/ai/observability/instrumentation.js +217 -47
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
- package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
- package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
- package/dist/services/server/ai/observability/otlpSetup.js +55 -4
- package/dist/telemetry/aiMetrics.d.ts +56 -0
- package/dist/telemetry/aiMetrics.js +229 -0
- package/dist/telemetry/attributes.d.ts +24 -1
- package/dist/telemetry/attributes.js +94 -9
- package/dist/telemetry/chatMessages.d.ts +6 -0
- package/dist/telemetry/chatMessages.js +37 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/telemetry/observation.d.ts +5 -5
- package/dist/telemetry/observation.js +128 -39
- package/dist/telemetry/spanSanitizer.d.ts +2 -2
- package/dist/telemetry/spanSanitizer.js +76 -8
- package/dist/telemetry/telemetryStarter.d.ts +16 -0
- package/dist/telemetry/telemetryStarter.js +28 -0
- package/dist/telemetry/traceContext.d.ts +3 -3
- package/dist/telemetry/traceContext.js +3 -3
- package/dist/telemetry/tracers.d.ts +13 -1
- package/dist/telemetry/tracers.js +49 -10
- package/dist/telemetry/withSpan.d.ts +19 -3
- package/dist/telemetry/withSpan.js +149 -20
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/decision.d.ts +117 -10
- package/dist/types/exporter.d.ts +98 -3
- package/dist/types/livekit.d.ts +16 -1
- package/dist/types/loopEngine.d.ts +16 -1
- package/dist/types/observability.d.ts +38 -5
- package/dist/types/providerCatalog.d.ts +12 -1
- package/dist/types/providers.d.ts +2 -0
- package/dist/types/proxy.d.ts +2 -0
- package/dist/types/span.d.ts +26 -0
- package/dist/utils/decisionAnswers.d.ts +7 -1
- package/dist/utils/decisionAnswers.js +43 -2
- package/dist/utils/decisionLimits.d.ts +13 -1
- package/dist/utils/decisionLimits.js +52 -1
- package/dist/utils/decisionMedia.d.ts +4 -2
- package/dist/utils/decisionMedia.js +216 -14
- package/dist/utils/timeout.js +4 -2
- package/dist/voice/livekit/realtimeMcpTools.js +11 -2
- package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
- package/dist/voice/livekit/realtimeMemory.js +139 -46
- package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
- package/dist/voice/livekit/turnRecorder.js +22 -9
- package/dist/workflow/core/ensembleExecutor.js +2 -0
- package/dist/workflow/core/judgeScorer.js +1 -0
- package/dist/workflow/core/responseConditioner.js +1 -0
- package/dist/workflow/core/workflowRunner.js +6 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +4 -0
- package/docs-site/static/search-index.json +56 -48
- package/package.json +21 -2
|
@@ -909,14 +909,16 @@ export function resolveDefaultDecisionProvider(credentials) {
|
|
|
909
909
|
return DECISION_PROVIDERS.find((descriptor) => isDecisionProviderConfigured(descriptor, credentials))?.name;
|
|
910
910
|
}
|
|
911
911
|
/**
|
|
912
|
-
* Names of the decision providers that accept images, or, with `video
|
|
913
|
-
* ones that accept
|
|
914
|
-
* the same request is worse than none.
|
|
912
|
+
* Names of the decision providers that accept images, or, with `video` or
|
|
913
|
+
* `audio`, the ones that accept that too: a hint that names a provider which
|
|
914
|
+
* would refuse the same request is worse than none.
|
|
915
915
|
*/
|
|
916
916
|
export function listMediaDecisionProviders(needs) {
|
|
917
917
|
return DECISION_PROVIDERS.filter((descriptor) => {
|
|
918
918
|
const media = descriptor.decisionLimits?.media;
|
|
919
|
-
return media !== undefined &&
|
|
919
|
+
return (media !== undefined &&
|
|
920
|
+
(!needs?.video || media.video) &&
|
|
921
|
+
(!needs?.audio || media.audio !== undefined));
|
|
920
922
|
}).map((descriptor) => descriptor.name);
|
|
921
923
|
}
|
|
922
924
|
/** "a", "a or b", "a, b or c". */
|
package/dist/index.d.ts
CHANGED
|
@@ -416,7 +416,7 @@ export { classifyProviderError, ModelPool, createDefaultRequestRouter, Classifie
|
|
|
416
416
|
* if (team && team.confidence > 0.7) { route(team.choice); }
|
|
417
417
|
* ```
|
|
418
418
|
*/
|
|
419
|
-
export { decisionBooleanConfidence, decisionKey, gateDecisionBoolean, HOST_DECISION_NAMESPACE, isDecisionQuestion, readDecisionBoolean, readDecisionChoice, readDecisionScore, servesInferenceKind, } from "./utils/decisionAnswers.js";
|
|
419
|
+
export { decisionBooleanConfidence, decisionKey, gateDecisionBoolean, HOST_DECISION_NAMESPACE, isDecisionQuestion, readDecisionBoolean, readDecisionChoice, readDecisionScore, readDecisionValue, servesInferenceKind, } from "./utils/decisionAnswers.js";
|
|
420
420
|
export { estimateDecisionStateTokens, resolveDecisionLimitsReading, } from "./utils/decisionLimits.js";
|
|
421
421
|
export { DECISION_PROVIDERS, resolveDefaultDecisionProvider, } from "./factories/providerDescriptors.js";
|
|
422
422
|
export { TYPESAFE_MAX_REQUEST_TOKENS, TYPESAFE_MAX_STATE_TOKENS, } from "./providers/typesafe.js";
|
package/dist/index.js
CHANGED
|
@@ -633,7 +633,7 @@ export { classifyProviderError, ModelPool, createDefaultRequestRouter, Classifie
|
|
|
633
633
|
* if (team && team.confidence > 0.7) { route(team.choice); }
|
|
634
634
|
* ```
|
|
635
635
|
*/
|
|
636
|
-
export { decisionBooleanConfidence, decisionKey, gateDecisionBoolean, HOST_DECISION_NAMESPACE, isDecisionQuestion, readDecisionBoolean, readDecisionChoice, readDecisionScore, servesInferenceKind, } from "./utils/decisionAnswers.js";
|
|
636
|
+
export { decisionBooleanConfidence, decisionKey, gateDecisionBoolean, HOST_DECISION_NAMESPACE, isDecisionQuestion, readDecisionBoolean, readDecisionChoice, readDecisionScore, readDecisionValue, servesInferenceKind, } from "./utils/decisionAnswers.js";
|
|
637
637
|
// The estimator the pre-flight refusal uses, and the flattening behind
|
|
638
638
|
// `neurolink.decisionLimits()` — exported so a host measures a state with the
|
|
639
639
|
// very function that will judge it.
|
|
@@ -14,6 +14,7 @@ import { toolRegistry } from "./toolRegistry.js";
|
|
|
14
14
|
import { HITLUserRejectedError, HITLTimeoutError } from "../hitl/hitlErrors.js";
|
|
15
15
|
import { detectCategory } from "../utils/mcpDefaults.js";
|
|
16
16
|
import { isObject, isNonNullObject } from "../utils/typeUtils.js";
|
|
17
|
+
import { recordMCPToolCall } from "../telemetry/aiMetrics.js";
|
|
17
18
|
import { tracers } from "../telemetry/tracers.js";
|
|
18
19
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
19
20
|
// `@modelcontextprotocol/sdk`'s stdio transport (imported above) does
|
|
@@ -2045,8 +2046,9 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
2045
2046
|
if (result.success) {
|
|
2046
2047
|
// A resolved `{ isError: true }` still comes back with success:true so
|
|
2047
2048
|
// the caller receives the MCP error payload as-is, but it is a failed
|
|
2048
|
-
// call for telemetry:
|
|
2049
|
-
//
|
|
2049
|
+
// call for telemetry: count it in the server's totalErrors and as
|
|
2050
|
+
// success=false in mcp_tool_calls_total instead of counting every
|
|
2051
|
+
// resolved error as a success.
|
|
2050
2052
|
const resolvedError = result.isErrorResult === true;
|
|
2051
2053
|
if (resolvedError) {
|
|
2052
2054
|
instance.metrics.totalErrors++;
|
|
@@ -2056,6 +2058,7 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
2056
2058
|
: `[ExternalServerManager] Tool executed successfully: ${toolName} on ${serverId}`, {
|
|
2057
2059
|
duration,
|
|
2058
2060
|
});
|
|
2061
|
+
recordMCPToolCall(toolName, duration, !resolvedError);
|
|
2059
2062
|
return result.data;
|
|
2060
2063
|
}
|
|
2061
2064
|
else {
|
|
@@ -2064,6 +2067,7 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
2064
2067
|
}
|
|
2065
2068
|
catch (error) {
|
|
2066
2069
|
instance.metrics.totalErrors++;
|
|
2070
|
+
recordMCPToolCall(toolName, Date.now() - startTime, false);
|
|
2067
2071
|
mcpLogger.error(`[ExternalServerManager] Tool execution failed: ${toolName} on ${serverId}`, error);
|
|
2068
2072
|
// The SDK rejects with its connection-closed error when the server is
|
|
2069
2073
|
// gone. Without this transition the instance stayed "connected" and
|
package/dist/mcp/toolRegistry.js
CHANGED
|
@@ -293,6 +293,9 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
293
293
|
attributes: {
|
|
294
294
|
"tool.name": toolName,
|
|
295
295
|
[ATTR.MCP_SERVER_ID]: preResolvedServerId || "builtin",
|
|
296
|
+
// Registry-level wrapper around the `tool` observation of the same
|
|
297
|
+
// call: it stays an OpenTelemetry span but is not exported.
|
|
298
|
+
[ATTR.NL_INTERNAL]: true,
|
|
296
299
|
},
|
|
297
300
|
}, async (span) => {
|
|
298
301
|
try {
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DecisionCallerOptions, DecisionLimitsQuery, DecisionLimitsReading, DecisionOptions, DecisionResult, AgentDefinition, AgentNetworkConfig, AgentRunOptions, AgentRunOutcome, AgentToolRegistrationOptions, IsolatedAgentDefinition, NetworkExecutionInput, NetworkExecutionOptions, NetworkExecutionResult, NetworkStreamChunk, WorkerInstanceOptions } from "./types/index.js";
|
|
2
|
-
import type { CompactionConfig, CompactionResult, ArtifactStore, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ConversationSeedMessage, ConversationSeedOptions, RecordConversationTurnOptions, RecordConversationTurnResult, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPServerHealth, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig, ToolConfig, KnowledgeEngineStatus, ChecklistState, ArtifactPageRequest, BankArtifactOptions, BankedArtifactRef, DelegateCollectRequest, DelegateCollectResult, DelegateHandle, DelegateRegistrationOptions, DelegateSpawnOptions, BackgroundCommandHandle, BackgroundCommandOptions, BackgroundCommandOutputPage, BackgroundCommandPageRequest, BackgroundCommandPolicy, BackgroundCommandStatus, GitToolResult, GitToolsetOptions } from "./types/index.js";
|
|
2
|
+
import type { CompactionConfig, CompactionResult, ArtifactStore, SpanData, ExporterStatus, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ConversationSeedMessage, ConversationSeedOptions, RecordConversationTurnOptions, RecordConversationTurnResult, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPServerHealth, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig, ToolConfig, KnowledgeEngineStatus, ChecklistState, ArtifactPageRequest, BankArtifactOptions, BankedArtifactRef, DelegateCollectRequest, DelegateCollectResult, DelegateHandle, DelegateRegistrationOptions, DelegateSpawnOptions, BackgroundCommandHandle, BackgroundCommandOptions, BackgroundCommandOutputPage, BackgroundCommandPageRequest, BackgroundCommandPolicy, BackgroundCommandStatus, GitToolResult, GitToolsetOptions } from "./types/index.js";
|
|
3
3
|
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
|
|
4
4
|
import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
|
|
5
5
|
import { ExternalServerManager } from "./mcp/externalServerManager.js";
|
|
@@ -687,6 +687,10 @@ export declare class NeuroLink {
|
|
|
687
687
|
isTelemetryEnabled(): boolean;
|
|
688
688
|
/**
|
|
689
689
|
* Get comprehensive telemetry status including Langfuse and OpenTelemetry state
|
|
690
|
+
* and every registered exporter with its queued, delivered and dropped spans.
|
|
691
|
+
* `openTelemetry.enabled` is whether tracing is initialized, as it always
|
|
692
|
+
* was. Beside a host TracerProvider, `openTelemetry.usingExternalProvider`
|
|
693
|
+
* is `true`: NeuroLink exports nothing to the endpoint.
|
|
690
694
|
*/
|
|
691
695
|
getTelemetryStatus(): {
|
|
692
696
|
enabled: boolean;
|
|
@@ -699,7 +703,9 @@ export declare class NeuroLink {
|
|
|
699
703
|
enabled: boolean;
|
|
700
704
|
endpoint?: string;
|
|
701
705
|
serviceName?: string;
|
|
706
|
+
usingExternalProvider?: boolean;
|
|
702
707
|
};
|
|
708
|
+
exporters: ExporterStatus[];
|
|
703
709
|
};
|
|
704
710
|
/**
|
|
705
711
|
* Get aggregated observability metrics (latency, tokens, cost, success rate)
|
|
@@ -2694,12 +2700,14 @@ export declare class NeuroLink {
|
|
|
2694
2700
|
* many would have the WHOLE request refused, and NeuroLink's own routing
|
|
2695
2701
|
* would silently degrade to the heuristic — the host's problem would
|
|
2696
2702
|
* become NeuroLink's. A provider that declares no `maxQuestions`
|
|
2697
|
-
* (TypeSafe
|
|
2703
|
+
* (TypeSafe) takes every well-formed addition. For the same reason a
|
|
2704
|
+
* question the provider cannot answer (a type it does not list, a choice
|
|
2705
|
+
* or score past its cap) is dropped on its own.
|
|
2698
2706
|
* 4. Answers are split: the consumer sees only its own ids, exactly as it
|
|
2699
2707
|
* would without hooks; `onAnswers` gets the host's answers under the
|
|
2700
2708
|
* host's original ids plus the full result.
|
|
2701
2709
|
*
|
|
2702
|
-
* The request's `images` and `
|
|
2710
|
+
* The request's `images`, `video` and `audio` ride along untouched — `merged` and
|
|
2703
2711
|
* the wire call spread the request, and only `state`, the questions and
|
|
2704
2712
|
* the result are cloned — so an XOR request keeps its Buffers and its
|
|
2705
2713
|
* result keeps `mediaBytes`.
|
|
@@ -2712,13 +2720,15 @@ export declare class NeuroLink {
|
|
|
2712
2720
|
* on the wire can differ from the instance's.
|
|
2713
2721
|
*
|
|
2714
2722
|
* Fail-open by construction, in three layers. Without a decision provider
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2717
|
-
*
|
|
2718
|
-
*
|
|
2719
|
-
*
|
|
2720
|
-
*
|
|
2721
|
-
*
|
|
2723
|
+
* no hook runs and no event is emitted: the call goes to `tryRunDecide`
|
|
2724
|
+
* alone, which records the WARNING `decide` observation it always did, and
|
|
2725
|
+
* the consumer gets the null it always got. Everything a host can reach
|
|
2726
|
+
* (its hook's return value, the event payloads, the answer split,
|
|
2727
|
+
* `onAnswers`) runs inside one `try`; whatever a host throws, the consumer
|
|
2728
|
+
* gets what `tryRunDecide` alone would have given it. And every
|
|
2729
|
+
* host-facing value is a `structuredClone`, so a hook or listener that
|
|
2730
|
+
* edits what it is handed edits neither the wire request nor the
|
|
2731
|
+
* consumer's own question objects.
|
|
2722
2732
|
*
|
|
2723
2733
|
* `outer` is what the surrounding `generate()` / `stream()` supplies —
|
|
2724
2734
|
* per-call credentials, abort signal, ids — and wins over the same fields
|
package/dist/neurolink.js
CHANGED
|
@@ -37,7 +37,7 @@ import { describeDecisionProviderKeys, PROVIDER_ALIAS_INDEX, PROVIDER_DESCRIPTOR
|
|
|
37
37
|
import { describeSTTProviderKeys, getSTTProviderDescriptor, listConfiguredSTTProviders, resolveCallSTTEndpoints, resolveDefaultSTTProvider, resolveSTTProviderName, } from "./factories/sttDescriptors.js";
|
|
38
38
|
import { MAX_PARALLEL_DECISION_BATCHES, mergeDecisionResults, splitDecisionRequest, } from "./utils/decisionBatches.js";
|
|
39
39
|
import { decisionKey, HOST_DECISION_NAMESPACE, isHostDecisionKey, snapshotDecisionQuestion, stripHostAnswers, } from "./utils/decisionAnswers.js";
|
|
40
|
-
import { resolveDecisionLimitsReading } from "./utils/decisionLimits.js";
|
|
40
|
+
import { resolveDecisionLimitsReading, unsupportedDecisionQuestion, } from "./utils/decisionLimits.js";
|
|
41
41
|
import { isDirectTTSRequest, resolveRequestKind, } from "./core/resolveRequestKind.js";
|
|
42
42
|
import { createToolEventPayload } from "./core/toolEvents.js";
|
|
43
43
|
import { ProviderFactory } from "./factories/providerFactory.js";
|
|
@@ -71,8 +71,9 @@ import { getMetricsAggregator, MetricsAggregator, } from "./observability/metric
|
|
|
71
71
|
import { AnalyticsService, calculateAdvancedCost, parseAnalyticsQualityScore, } from "./analytics/index.js";
|
|
72
72
|
import { SpanStatus, SpanType, CircuitBreakerOpenError, ConversationMemoryError, DEFAULT_DECISION_HOOK_TIMEOUT_MS, MAX_DECISION_HOOK_TIMEOUT_MS, ModelAccessDeniedError, } from "./types/index.js";
|
|
73
73
|
import { resolveTerminalAgentMode } from "./agent/prompts/terminalAgentPrompt.js";
|
|
74
|
+
import { getExporterRegistry } from "./observability/exporterRegistry.js";
|
|
74
75
|
import { SpanSerializer } from "./observability/utils/spanSerializer.js";
|
|
75
|
-
import { withSpan } from "./telemetry/withSpan.js";
|
|
76
|
+
import { endSpanWhenDropped, withSpan } from "./telemetry/withSpan.js";
|
|
76
77
|
import { getActiveTraceContext } from "./telemetry/traceContext.js";
|
|
77
78
|
import { calculateCost } from "./utils/pricing.js";
|
|
78
79
|
import { acquireTelemetry, ensureTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, isOpenTelemetryInitialized, releaseTelemetry, setLangfuseContext, traceMetadataFromContext, } from "./services/server/ai/observability/instrumentation.js";
|
|
@@ -410,6 +411,9 @@ function observeTurn(span, attrs) {
|
|
|
410
411
|
}
|
|
411
412
|
}
|
|
412
413
|
function startTurn(span, optionsOrPrompt) {
|
|
414
|
+
if (!span.isRecording()) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
413
417
|
observeTurn(span, { input: turnInput(optionsOrPrompt) });
|
|
414
418
|
if (typeof optionsOrPrompt !== "string") {
|
|
415
419
|
const name = optionsOrPrompt.context?.observationName;
|
|
@@ -474,65 +478,87 @@ function traceTurnStream(result, span, turnContext) {
|
|
|
474
478
|
: "stream closed before first chunk");
|
|
475
479
|
}
|
|
476
480
|
};
|
|
481
|
+
const advance = async (pull, afterThrow = false) => {
|
|
482
|
+
started = true;
|
|
483
|
+
try {
|
|
484
|
+
const step = await context.with(turnContext, pull);
|
|
485
|
+
if (step.done) {
|
|
486
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
487
|
+
end(observationLevel(result.finishReason));
|
|
488
|
+
return { done: true, value: undefined };
|
|
489
|
+
}
|
|
490
|
+
const content = typeof step.value === "object" && step.value !== null
|
|
491
|
+
? step.value.content
|
|
492
|
+
: undefined;
|
|
493
|
+
if (typeof content === "string") {
|
|
494
|
+
output += content;
|
|
495
|
+
}
|
|
496
|
+
return step;
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
failTurnSpan(span, error instanceof Error ? error : new Error(String(error)));
|
|
500
|
+
// A consumer that throws into the stream ends it early, as return()
|
|
501
|
+
// does, and the root has always kept the model in that case.
|
|
502
|
+
if (afterThrow) {
|
|
503
|
+
observeTurn(span, { metadata: { model: result.model } });
|
|
504
|
+
}
|
|
505
|
+
finish();
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
477
509
|
const traced = {
|
|
478
510
|
async next() {
|
|
479
511
|
if (ended) {
|
|
480
512
|
return { done: true, value: undefined };
|
|
481
513
|
}
|
|
482
|
-
|
|
483
|
-
try {
|
|
484
|
-
const step = await context.with(turnContext, () => iterator.next());
|
|
485
|
-
if (step.done) {
|
|
486
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
487
|
-
end(observationLevel(result.finishReason));
|
|
488
|
-
return { done: true, value: undefined };
|
|
489
|
-
}
|
|
490
|
-
const content = typeof step.value === "object" && step.value !== null
|
|
491
|
-
? step.value.content
|
|
492
|
-
: undefined;
|
|
493
|
-
if (typeof content === "string") {
|
|
494
|
-
output += content;
|
|
495
|
-
}
|
|
496
|
-
return step;
|
|
497
|
-
}
|
|
498
|
-
catch (error) {
|
|
499
|
-
failTurnSpan(span, error instanceof Error ? error : new Error(String(error)));
|
|
500
|
-
finish();
|
|
501
|
-
throw error;
|
|
502
|
-
}
|
|
514
|
+
return advance(() => iterator.next());
|
|
503
515
|
},
|
|
504
516
|
async return() {
|
|
505
517
|
await close();
|
|
506
518
|
return { done: true, value: undefined };
|
|
507
519
|
},
|
|
508
520
|
async throw(error) {
|
|
509
|
-
|
|
510
|
-
|
|
521
|
+
const forward = iterator.throw;
|
|
522
|
+
if (ended || forward === undefined) {
|
|
523
|
+
await close();
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
return advance(() => forward.call(iterator, error), true);
|
|
511
527
|
},
|
|
512
528
|
[Symbol.asyncIterator]() {
|
|
513
529
|
return traced;
|
|
514
530
|
},
|
|
515
531
|
};
|
|
532
|
+
endSpanWhenDropped(traced, span);
|
|
516
533
|
return traced;
|
|
517
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Runs every pull of `stream` inside `drainContext`, and forwards `return()`
|
|
537
|
+
* and `throw()` to it even before the first `next()`. A generator that has not
|
|
538
|
+
* started never enters its body, so wrapping the drain in one would swallow a
|
|
539
|
+
* pre-start close and leave the drain's own settle step unreached.
|
|
540
|
+
*/
|
|
518
541
|
function drainInContext(stream, drainContext) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
542
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
543
|
+
const drained = {
|
|
544
|
+
next: () => context.with(drainContext, () => iterator.next()),
|
|
545
|
+
async return() {
|
|
546
|
+
await context.with(drainContext, async () => iterator.return?.());
|
|
547
|
+
return { done: true, value: undefined };
|
|
548
|
+
},
|
|
549
|
+
async throw(error) {
|
|
550
|
+
const forward = iterator.throw;
|
|
551
|
+
if (forward === undefined) {
|
|
552
|
+
await context.with(drainContext, async () => iterator.return?.());
|
|
553
|
+
throw error;
|
|
528
554
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
555
|
+
return context.with(drainContext, () => forward.call(iterator, error));
|
|
556
|
+
},
|
|
557
|
+
[Symbol.asyncIterator]() {
|
|
558
|
+
return drained;
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
return drained;
|
|
536
562
|
}
|
|
537
563
|
/** Monotonic source for {@link NeuroLink.logInstanceId}. Process-local by design. */
|
|
538
564
|
let neuroLinkInstanceCounter = 0;
|
|
@@ -834,8 +860,8 @@ export class NeuroLink {
|
|
|
834
860
|
}, callback);
|
|
835
861
|
}
|
|
836
862
|
createMetricsTraceContext() {
|
|
837
|
-
//
|
|
838
|
-
//
|
|
863
|
+
// Reuse the active OTel trace context so the in-process span records
|
|
864
|
+
// carry the traceId of the exported OTEL trace of the same request.
|
|
839
865
|
const activeSpan = trace.getSpan(context.active());
|
|
840
866
|
if (activeSpan) {
|
|
841
867
|
const spanCtx = activeSpan.spanContext();
|
|
@@ -3168,11 +3194,15 @@ Current user's request: ${currentInput}`;
|
|
|
3168
3194
|
}
|
|
3169
3195
|
/**
|
|
3170
3196
|
* Get comprehensive telemetry status including Langfuse and OpenTelemetry state
|
|
3197
|
+
* and every registered exporter with its queued, delivered and dropped spans.
|
|
3198
|
+
* `openTelemetry.enabled` is whether tracing is initialized, as it always
|
|
3199
|
+
* was. Beside a host TracerProvider, `openTelemetry.usingExternalProvider`
|
|
3200
|
+
* is `true`: NeuroLink exports nothing to the endpoint.
|
|
3171
3201
|
*/
|
|
3172
3202
|
getTelemetryStatus() {
|
|
3173
3203
|
const langfuseConfig = this.observabilityConfig?.langfuse;
|
|
3174
3204
|
const langfuseOptions = typeof langfuseConfig === "object" ? langfuseConfig : undefined;
|
|
3175
|
-
const { hasProcessor } = getLangfuseHealthStatus();
|
|
3205
|
+
const { hasProcessor, usingExternalProvider } = getLangfuseHealthStatus();
|
|
3176
3206
|
return {
|
|
3177
3207
|
enabled: isOpenTelemetryInitialized(),
|
|
3178
3208
|
langfuse: langfuseConfig !== undefined || hasProcessor
|
|
@@ -3188,8 +3218,10 @@ Current user's request: ${currentInput}`;
|
|
|
3188
3218
|
enabled: isOpenTelemetryInitialized(),
|
|
3189
3219
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
3190
3220
|
serviceName: process.env.OTEL_SERVICE_NAME,
|
|
3221
|
+
...(usingExternalProvider && { usingExternalProvider }),
|
|
3191
3222
|
}
|
|
3192
3223
|
: undefined,
|
|
3224
|
+
exporters: getExporterRegistry().getStatuses(),
|
|
3193
3225
|
};
|
|
3194
3226
|
}
|
|
3195
3227
|
/**
|
|
@@ -7435,8 +7467,12 @@ Current user's request: ${currentInput}`;
|
|
|
7435
7467
|
return { done: true, value: undefined };
|
|
7436
7468
|
},
|
|
7437
7469
|
async throw(error) {
|
|
7438
|
-
|
|
7439
|
-
|
|
7470
|
+
const forward = iterator.throw;
|
|
7471
|
+
if (forward === undefined) {
|
|
7472
|
+
await scope.run(eventScope, async () => iterator.return?.());
|
|
7473
|
+
throw error;
|
|
7474
|
+
}
|
|
7475
|
+
return scope.run(eventScope, () => forward.call(iterator, error));
|
|
7440
7476
|
},
|
|
7441
7477
|
[Symbol.asyncIterator]() {
|
|
7442
7478
|
return bound;
|
|
@@ -7805,6 +7841,7 @@ Current user's request: ${currentInput}`;
|
|
|
7805
7841
|
},
|
|
7806
7842
|
});
|
|
7807
7843
|
attempt.stream = drainInContext(attempt.stream, context.active());
|
|
7844
|
+
endSpanWhenDropped(attempt.stream, streamSpan);
|
|
7808
7845
|
return attempt;
|
|
7809
7846
|
}));
|
|
7810
7847
|
if (streamSttTranscription) {
|
|
@@ -8615,7 +8652,18 @@ Current user's request: ${currentInput}`;
|
|
|
8615
8652
|
}
|
|
8616
8653
|
return stream.return(value);
|
|
8617
8654
|
},
|
|
8618
|
-
throw: (error) =>
|
|
8655
|
+
throw: async (error) => {
|
|
8656
|
+
if (!started) {
|
|
8657
|
+
started = true;
|
|
8658
|
+
await this.settleStandardStreamDrain(drain, {
|
|
8659
|
+
accumulatedContent: "",
|
|
8660
|
+
chunkCount: 0,
|
|
8661
|
+
streamError: error instanceof Error ? error : new Error(String(error)),
|
|
8662
|
+
resolvedUsage: undefined,
|
|
8663
|
+
});
|
|
8664
|
+
}
|
|
8665
|
+
return stream.throw(error);
|
|
8666
|
+
},
|
|
8619
8667
|
[Symbol.asyncIterator]: () => guarded,
|
|
8620
8668
|
};
|
|
8621
8669
|
return guarded;
|
|
@@ -14046,12 +14094,14 @@ Current user's request: ${currentInput}`;
|
|
|
14046
14094
|
* many would have the WHOLE request refused, and NeuroLink's own routing
|
|
14047
14095
|
* would silently degrade to the heuristic — the host's problem would
|
|
14048
14096
|
* become NeuroLink's. A provider that declares no `maxQuestions`
|
|
14049
|
-
* (TypeSafe
|
|
14097
|
+
* (TypeSafe) takes every well-formed addition. For the same reason a
|
|
14098
|
+
* question the provider cannot answer (a type it does not list, a choice
|
|
14099
|
+
* or score past its cap) is dropped on its own.
|
|
14050
14100
|
* 4. Answers are split: the consumer sees only its own ids, exactly as it
|
|
14051
14101
|
* would without hooks; `onAnswers` gets the host's answers under the
|
|
14052
14102
|
* host's original ids plus the full result.
|
|
14053
14103
|
*
|
|
14054
|
-
* The request's `images` and `
|
|
14104
|
+
* The request's `images`, `video` and `audio` ride along untouched — `merged` and
|
|
14055
14105
|
* the wire call spread the request, and only `state`, the questions and
|
|
14056
14106
|
* the result are cloned — so an XOR request keeps its Buffers and its
|
|
14057
14107
|
* result keeps `mediaBytes`.
|
|
@@ -14064,13 +14114,15 @@ Current user's request: ${currentInput}`;
|
|
|
14064
14114
|
* on the wire can differ from the instance's.
|
|
14065
14115
|
*
|
|
14066
14116
|
* Fail-open by construction, in three layers. Without a decision provider
|
|
14067
|
-
*
|
|
14068
|
-
*
|
|
14069
|
-
*
|
|
14070
|
-
*
|
|
14071
|
-
*
|
|
14072
|
-
*
|
|
14073
|
-
*
|
|
14117
|
+
* no hook runs and no event is emitted: the call goes to `tryRunDecide`
|
|
14118
|
+
* alone, which records the WARNING `decide` observation it always did, and
|
|
14119
|
+
* the consumer gets the null it always got. Everything a host can reach
|
|
14120
|
+
* (its hook's return value, the event payloads, the answer split,
|
|
14121
|
+
* `onAnswers`) runs inside one `try`; whatever a host throws, the consumer
|
|
14122
|
+
* gets what `tryRunDecide` alone would have given it. And every
|
|
14123
|
+
* host-facing value is a `structuredClone`, so a hook or listener that
|
|
14124
|
+
* edits what it is handed edits neither the wire request nor the
|
|
14125
|
+
* consumer's own question objects.
|
|
14074
14126
|
*
|
|
14075
14127
|
* `outer` is what the surrounding `generate()` / `stream()` supplies —
|
|
14076
14128
|
* per-call credentials, abort signal, ids — and wins over the same fields
|
|
@@ -14092,9 +14144,10 @@ Current user's request: ${currentInput}`;
|
|
|
14092
14144
|
if (!site) {
|
|
14093
14145
|
return this.tryRunDecide(merged);
|
|
14094
14146
|
}
|
|
14095
|
-
//
|
|
14096
|
-
// request path of a tenant that has no decision model to reach, and
|
|
14097
|
-
// event for a call that could never be made would only mislead.
|
|
14147
|
+
// No host hook or event without a decision provider: a hook must not sit
|
|
14148
|
+
// on the request path of a tenant that has no decision model to reach, and
|
|
14149
|
+
// an event for a call that could never be made would only mislead. The
|
|
14150
|
+
// plain call still runs, so the WARNING observation it records is kept.
|
|
14098
14151
|
if (!this.resolveDecisionProviderName(merged.provider, merged.credentials)) {
|
|
14099
14152
|
return this.tryRunDecide(merged);
|
|
14100
14153
|
}
|
|
@@ -14145,7 +14198,7 @@ Current user's request: ${currentInput}`;
|
|
|
14145
14198
|
model: merged.model,
|
|
14146
14199
|
credentials: merged.credentials,
|
|
14147
14200
|
});
|
|
14148
|
-
// A provider without a question-count cap (TypeSafe
|
|
14201
|
+
// A provider without a question-count cap (TypeSafe) takes
|
|
14149
14202
|
// every well-formed addition; its token/byte limits still apply
|
|
14150
14203
|
// and are what `decisionLimits()` reports for sizing.
|
|
14151
14204
|
const room = limits?.maxQuestions !== undefined
|
|
@@ -14155,6 +14208,7 @@ Current user's request: ${currentInput}`;
|
|
|
14155
14208
|
...ownQuestions,
|
|
14156
14209
|
};
|
|
14157
14210
|
let invalidCount = 0;
|
|
14211
|
+
let unsupportedCount = 0;
|
|
14158
14212
|
let overflowCount = 0;
|
|
14159
14213
|
for (const [hostId, proposal] of proposedEntries) {
|
|
14160
14214
|
// The wire carries a snapshot of the host's question, never the
|
|
@@ -14164,6 +14218,12 @@ Current user's request: ${currentInput}`;
|
|
|
14164
14218
|
invalidCount += 1;
|
|
14165
14219
|
continue;
|
|
14166
14220
|
}
|
|
14221
|
+
// A question the provider cannot read would have the whole
|
|
14222
|
+
// request refused, NeuroLink's own questions with it.
|
|
14223
|
+
if (unsupportedDecisionQuestion(question, limits) !== undefined) {
|
|
14224
|
+
unsupportedCount += 1;
|
|
14225
|
+
continue;
|
|
14226
|
+
}
|
|
14167
14227
|
if (hostIdByWireId.size >= room) {
|
|
14168
14228
|
overflowCount += 1;
|
|
14169
14229
|
continue;
|
|
@@ -14175,6 +14235,9 @@ Current user's request: ${currentInput}`;
|
|
|
14175
14235
|
if (invalidCount > 0) {
|
|
14176
14236
|
logger.warn(`[Decision] dropped ${invalidCount} malformed host question(s) for site "${site}"`);
|
|
14177
14237
|
}
|
|
14238
|
+
if (unsupportedCount > 0) {
|
|
14239
|
+
logger.warn(`[Decision] dropped ${unsupportedCount} host question(s) for site "${site}" that ${limits?.provider ?? "the decision provider"} cannot answer`);
|
|
14240
|
+
}
|
|
14178
14241
|
if (overflowCount > 0 && limits) {
|
|
14179
14242
|
logger.warn(`[Decision] dropped ${overflowCount} host question(s) for site "${site}": ${limits.provider} accepts ${limits.maxQuestions} per request and NeuroLink asks ${ownCount}`);
|
|
14180
14243
|
}
|
|
@@ -14306,6 +14369,7 @@ Current user's request: ${currentInput}`;
|
|
|
14306
14369
|
"ai.provider": providerName,
|
|
14307
14370
|
"decision.question_count": questionCount,
|
|
14308
14371
|
"decision.images.count": options.images?.length ?? 0,
|
|
14372
|
+
"decision.audio.count": options.audio === undefined ? 0 : 1,
|
|
14309
14373
|
...siteSpanAttributes,
|
|
14310
14374
|
},
|
|
14311
14375
|
}, async (otelSpan) => {
|
|
@@ -14335,6 +14399,7 @@ Current user's request: ${currentInput}`;
|
|
|
14335
14399
|
"ai.model": options.model ?? "",
|
|
14336
14400
|
"decision.question_count": questionCount,
|
|
14337
14401
|
"decision.images.count": options.images?.length ?? 0,
|
|
14402
|
+
"decision.audio.count": options.audio === undefined ? 0 : 1,
|
|
14338
14403
|
...siteSpanAttributes,
|
|
14339
14404
|
}, parentSpanId, traceId);
|
|
14340
14405
|
try {
|
|
@@ -14348,6 +14413,7 @@ Current user's request: ${currentInput}`;
|
|
|
14348
14413
|
questions: options.questions,
|
|
14349
14414
|
images: options.images,
|
|
14350
14415
|
video: options.video,
|
|
14416
|
+
audio: options.audio,
|
|
14351
14417
|
model: options.model,
|
|
14352
14418
|
signal: options.signal,
|
|
14353
14419
|
timeoutMs: options.timeoutMs,
|
|
@@ -14404,11 +14470,14 @@ Current user's request: ${currentInput}`;
|
|
|
14404
14470
|
? { "decision.upstream_ms": result.upstreamMs }
|
|
14405
14471
|
: {}),
|
|
14406
14472
|
});
|
|
14407
|
-
// BOTH aggregators, exactly as the generate path does.
|
|
14408
|
-
//
|
|
14409
|
-
//
|
|
14410
|
-
//
|
|
14411
|
-
//
|
|
14473
|
+
// BOTH aggregators, exactly as the generate path does. They are
|
|
14474
|
+
// in-process stores that are never exported: what Langfuse and the
|
|
14475
|
+
// OTLP exporters receive for this decision is the `decide`
|
|
14476
|
+
// generation span. The instance one is what
|
|
14477
|
+
// `neurolink.getMetrics()` / `getSpans()` read; the global one is
|
|
14478
|
+
// what `getMetricsAggregator()` readers see. Recording to only the
|
|
14479
|
+
// global one would make a decision invisible to the very API a
|
|
14480
|
+
// caller would use to check whether it ran.
|
|
14412
14481
|
const ended = SpanSerializer.endSpan(span, SpanStatus.OK);
|
|
14413
14482
|
this.metricsAggregator.recordSpan(ended);
|
|
14414
14483
|
getMetricsAggregator().recordSpan(ended);
|
|
@@ -160,10 +160,6 @@ neurolink observability status
|
|
|
160
160
|
neurolink observability metrics
|
|
161
161
|
neurolink obs metrics --detailed
|
|
162
162
|
|
|
163
|
-
# List exporters
|
|
164
|
-
neurolink observability exporters
|
|
165
|
-
neurolink obs exp --format json
|
|
166
|
-
|
|
167
163
|
# Cost breakdown
|
|
168
164
|
neurolink observability costs
|
|
169
165
|
neurolink obs costs --by-model --by-provider
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages multiple observability exporters with circuit breaker protection
|
|
4
4
|
*/
|
|
5
5
|
import type { BaseExporter } from "./exporters/baseExporter.js";
|
|
6
|
-
import type { ExportResult, ExporterHealthStatus, ObservabilityCircuitBreakerConfig, ObservabilityCircuitBreakerState, Sampler, SpanData } from "../types/index.js";
|
|
6
|
+
import type { ExportResult, ExporterHealthStatus, ExporterStatus, ObservabilityCircuitBreakerConfig, ObservabilityCircuitBreakerState, Sampler, SpanData } from "../types/index.js";
|
|
7
7
|
/**
|
|
8
8
|
* Registry for managing multiple observability exporters
|
|
9
9
|
* Includes circuit breaker protection to prevent cascading failures
|
|
@@ -117,6 +117,11 @@ export declare class ExporterRegistry {
|
|
|
117
117
|
* Get total pending spans across all exporters
|
|
118
118
|
*/
|
|
119
119
|
getTotalPendingSpans(): number;
|
|
120
|
+
/**
|
|
121
|
+
* Snapshot of every registered exporter without a network call: whether it
|
|
122
|
+
* runs, what is queued, delivered and dropped, and an open circuit breaker
|
|
123
|
+
*/
|
|
124
|
+
getStatuses(): ExporterStatus[];
|
|
120
125
|
/**
|
|
121
126
|
* Clear all registered exporters and reset state
|
|
122
127
|
* (For testing and cleanup)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Manages multiple observability exporters with circuit breaker protection
|
|
4
4
|
*/
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
|
+
import { exporterStatusOf } from "./exporters/baseExporter.js";
|
|
6
7
|
import { AlwaysSampler } from "./sampling/samplers.js";
|
|
7
8
|
/** Default timeout for exporter API calls (30 seconds) */
|
|
8
9
|
const DEFAULT_EXPORT_TIMEOUT_MS = 30_000;
|
|
@@ -378,6 +379,23 @@ export class ExporterRegistry {
|
|
|
378
379
|
}
|
|
379
380
|
return total;
|
|
380
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Snapshot of every registered exporter without a network call: whether it
|
|
384
|
+
* runs, what is queued, delivered and dropped, and an open circuit breaker
|
|
385
|
+
*/
|
|
386
|
+
getStatuses() {
|
|
387
|
+
return Array.from(this.exporters.entries()).map(([name, exporter]) => {
|
|
388
|
+
const status = exporterStatusOf(exporter);
|
|
389
|
+
if (this.circuitBreakers.get(name)?.state !== "open") {
|
|
390
|
+
return status;
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
...status,
|
|
394
|
+
healthy: false,
|
|
395
|
+
errors: [...(status.errors ?? []), "Circuit breaker open"],
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
}
|
|
381
399
|
/**
|
|
382
400
|
* Clear all registered exporters and reset state
|
|
383
401
|
* (For testing and cleanup)
|