confused-ai-core 0.1.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/FEATURES.md +169 -0
- package/package.json +119 -0
- package/src/agent.ts +187 -0
- package/src/agentic/index.ts +87 -0
- package/src/agentic/runner.ts +386 -0
- package/src/agentic/types.ts +91 -0
- package/src/artifacts/artifact.ts +417 -0
- package/src/artifacts/index.ts +42 -0
- package/src/artifacts/media.ts +304 -0
- package/src/cli/index.ts +122 -0
- package/src/core/base-agent.ts +151 -0
- package/src/core/context-builder.ts +106 -0
- package/src/core/index.ts +8 -0
- package/src/core/schemas.ts +17 -0
- package/src/core/types.ts +158 -0
- package/src/create-agent.ts +309 -0
- package/src/debug-logger.ts +188 -0
- package/src/dx/agent.ts +88 -0
- package/src/dx/define-agent.ts +183 -0
- package/src/dx/dev-logger.ts +57 -0
- package/src/dx/index.ts +11 -0
- package/src/errors.ts +175 -0
- package/src/execution/engine.ts +522 -0
- package/src/execution/graph-builder.ts +362 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/types.ts +257 -0
- package/src/execution/worker-pool.ts +308 -0
- package/src/extensions/index.ts +123 -0
- package/src/guardrails/allowlist.ts +155 -0
- package/src/guardrails/index.ts +17 -0
- package/src/guardrails/types.ts +159 -0
- package/src/guardrails/validator.ts +265 -0
- package/src/index.ts +74 -0
- package/src/knowledge/index.ts +5 -0
- package/src/knowledge/types.ts +52 -0
- package/src/learning/in-memory-store.ts +72 -0
- package/src/learning/index.ts +6 -0
- package/src/learning/types.ts +42 -0
- package/src/llm/cache.ts +300 -0
- package/src/llm/index.ts +22 -0
- package/src/llm/model-resolver.ts +81 -0
- package/src/llm/openai-provider.ts +313 -0
- package/src/llm/openrouter-provider.ts +29 -0
- package/src/llm/types.ts +131 -0
- package/src/memory/in-memory-store.ts +255 -0
- package/src/memory/index.ts +7 -0
- package/src/memory/types.ts +193 -0
- package/src/memory/vector-store.ts +251 -0
- package/src/observability/console-logger.ts +123 -0
- package/src/observability/index.ts +12 -0
- package/src/observability/metrics.ts +85 -0
- package/src/observability/otlp-exporter.ts +417 -0
- package/src/observability/tracer.ts +105 -0
- package/src/observability/types.ts +341 -0
- package/src/orchestration/agent-adapter.ts +33 -0
- package/src/orchestration/index.ts +34 -0
- package/src/orchestration/load-balancer.ts +151 -0
- package/src/orchestration/mcp-types.ts +59 -0
- package/src/orchestration/message-bus.ts +192 -0
- package/src/orchestration/orchestrator.ts +349 -0
- package/src/orchestration/pipeline.ts +66 -0
- package/src/orchestration/supervisor.ts +107 -0
- package/src/orchestration/swarm.ts +1099 -0
- package/src/orchestration/toolkit.ts +47 -0
- package/src/orchestration/types.ts +339 -0
- package/src/planner/classical-planner.ts +383 -0
- package/src/planner/index.ts +8 -0
- package/src/planner/llm-planner.ts +353 -0
- package/src/planner/types.ts +227 -0
- package/src/planner/validator.ts +297 -0
- package/src/production/circuit-breaker.ts +290 -0
- package/src/production/graceful-shutdown.ts +251 -0
- package/src/production/health.ts +333 -0
- package/src/production/index.ts +57 -0
- package/src/production/latency-eval.ts +62 -0
- package/src/production/rate-limiter.ts +287 -0
- package/src/production/resumable-stream.ts +289 -0
- package/src/production/types.ts +81 -0
- package/src/sdk/index.ts +374 -0
- package/src/session/db-driver.ts +50 -0
- package/src/session/in-memory-store.ts +235 -0
- package/src/session/index.ts +12 -0
- package/src/session/sql-store.ts +315 -0
- package/src/session/sqlite-store.ts +61 -0
- package/src/session/types.ts +153 -0
- package/src/tools/base-tool.ts +223 -0
- package/src/tools/browser-tool.ts +123 -0
- package/src/tools/calculator-tool.ts +265 -0
- package/src/tools/file-tools.ts +394 -0
- package/src/tools/github-tool.ts +432 -0
- package/src/tools/hackernews-tool.ts +187 -0
- package/src/tools/http-tool.ts +118 -0
- package/src/tools/index.ts +99 -0
- package/src/tools/jira-tool.ts +373 -0
- package/src/tools/notion-tool.ts +322 -0
- package/src/tools/openai-tool.ts +236 -0
- package/src/tools/registry.ts +131 -0
- package/src/tools/serpapi-tool.ts +234 -0
- package/src/tools/shell-tool.ts +118 -0
- package/src/tools/slack-tool.ts +327 -0
- package/src/tools/telegram-tool.ts +127 -0
- package/src/tools/types.ts +229 -0
- package/src/tools/websearch-tool.ts +335 -0
- package/src/tools/wikipedia-tool.ts +177 -0
- package/src/tools/yfinance-tool.ts +33 -0
- package/src/voice/index.ts +17 -0
- package/src/voice/voice-provider.ts +228 -0
- package/tests/artifact.test.ts +241 -0
- package/tests/circuit-breaker.test.ts +171 -0
- package/tests/health.test.ts +192 -0
- package/tests/llm-cache.test.ts +186 -0
- package/tests/rate-limiter.test.ts +161 -0
- package/tsconfig.json +29 -0
- package/vitest.config.ts +47 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console Logger Implementation
|
|
3
|
+
*
|
|
4
|
+
* Simple console-based logging for development and debugging
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Logger, LogLevel, LogEntry, LogContext } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Console logger configuration
|
|
11
|
+
*/
|
|
12
|
+
export interface ConsoleLoggerConfig {
|
|
13
|
+
readonly minLevel?: LogLevel;
|
|
14
|
+
readonly includeTimestamp?: boolean;
|
|
15
|
+
readonly prefix?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Console logger implementation
|
|
20
|
+
*/
|
|
21
|
+
export class ConsoleLogger implements Logger {
|
|
22
|
+
private minLevel: LogLevel;
|
|
23
|
+
private includeTimestamp: boolean;
|
|
24
|
+
private prefix: string;
|
|
25
|
+
private context: Partial<LogContext> = {};
|
|
26
|
+
|
|
27
|
+
private static readonly LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
28
|
+
[LogLevel.DEBUG]: 0,
|
|
29
|
+
[LogLevel.INFO]: 1,
|
|
30
|
+
[LogLevel.WARN]: 2,
|
|
31
|
+
[LogLevel.ERROR]: 3,
|
|
32
|
+
[LogLevel.FATAL]: 4,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
constructor(config: ConsoleLoggerConfig = {}) {
|
|
36
|
+
this.minLevel = config.minLevel ?? LogLevel.INFO;
|
|
37
|
+
this.includeTimestamp = config.includeTimestamp ?? true;
|
|
38
|
+
this.prefix = config.prefix ?? '[AgentFramework]';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
debug(message: string, context?: Partial<LogContext>, metadata?: Record<string, unknown>): void {
|
|
42
|
+
this.log(LogLevel.DEBUG, message, context, metadata);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
info(message: string, context?: Partial<LogContext>, metadata?: Record<string, unknown>): void {
|
|
46
|
+
this.log(LogLevel.INFO, message, context, metadata);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
warn(message: string, context?: Partial<LogContext>, metadata?: Record<string, unknown>): void {
|
|
50
|
+
this.log(LogLevel.WARN, message, context, metadata);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
error(message: string, context?: Partial<LogContext>, metadata?: Record<string, unknown>): void {
|
|
54
|
+
this.log(LogLevel.ERROR, message, context, metadata);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fatal(message: string, context?: Partial<LogContext>, metadata?: Record<string, unknown>): void {
|
|
58
|
+
this.log(LogLevel.FATAL, message, context, metadata);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
log(level: LogLevel, message: string, context?: Partial<LogContext>, metadata?: Record<string, unknown>): void {
|
|
62
|
+
if (!this.shouldLog(level)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const entry: LogEntry = {
|
|
67
|
+
id: this.generateId(),
|
|
68
|
+
timestamp: new Date(),
|
|
69
|
+
level,
|
|
70
|
+
message,
|
|
71
|
+
source: this.prefix,
|
|
72
|
+
context: { ...this.context, ...context },
|
|
73
|
+
metadata,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this.output(entry);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
child(additionalContext: Partial<LogContext>): Logger {
|
|
80
|
+
const childLogger = new ConsoleLogger({
|
|
81
|
+
minLevel: this.minLevel,
|
|
82
|
+
includeTimestamp: this.includeTimestamp,
|
|
83
|
+
prefix: this.prefix,
|
|
84
|
+
});
|
|
85
|
+
childLogger.context = { ...this.context, ...additionalContext };
|
|
86
|
+
return childLogger;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private shouldLog(level: LogLevel): boolean {
|
|
90
|
+
return ConsoleLogger.LEVEL_PRIORITY[level] >= ConsoleLogger.LEVEL_PRIORITY[this.minLevel];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private output(entry: LogEntry): void {
|
|
94
|
+
const timestamp = this.includeTimestamp
|
|
95
|
+
? `[${entry.timestamp.toISOString()}]`
|
|
96
|
+
: '';
|
|
97
|
+
const level = `[${entry.level.toUpperCase()}]`;
|
|
98
|
+
const prefix = entry.source;
|
|
99
|
+
|
|
100
|
+
const parts = [timestamp, prefix, level, entry.message].filter(Boolean);
|
|
101
|
+
const formattedMessage = parts.join(' ');
|
|
102
|
+
|
|
103
|
+
switch (entry.level) {
|
|
104
|
+
case LogLevel.DEBUG:
|
|
105
|
+
console.debug(formattedMessage, entry.metadata ?? '');
|
|
106
|
+
break;
|
|
107
|
+
case LogLevel.INFO:
|
|
108
|
+
console.info(formattedMessage, entry.metadata ?? '');
|
|
109
|
+
break;
|
|
110
|
+
case LogLevel.WARN:
|
|
111
|
+
console.warn(formattedMessage, entry.metadata ?? '');
|
|
112
|
+
break;
|
|
113
|
+
case LogLevel.ERROR:
|
|
114
|
+
case LogLevel.FATAL:
|
|
115
|
+
console.error(formattedMessage, entry.metadata ?? '');
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private generateId(): string {
|
|
121
|
+
return `log-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './types.js';
|
|
6
|
+
export { ConsoleLogger } from './console-logger.js';
|
|
7
|
+
export { InMemoryTracer } from './tracer.js';
|
|
8
|
+
export { MetricsCollectorImpl } from './metrics.js';
|
|
9
|
+
|
|
10
|
+
// OTLP Export
|
|
11
|
+
export { OTLPTraceExporter, OTLPMetricsExporter } from './otlp-exporter.js';
|
|
12
|
+
export type { OTLPExporterConfig } from './otlp-exporter.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collector Implementation
|
|
3
|
+
*
|
|
4
|
+
* In-memory metrics collection for monitoring and observability
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { MetricsCollector, MetricValue, MetricType } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Metrics collector implementation
|
|
11
|
+
*/
|
|
12
|
+
export class MetricsCollectorImpl implements MetricsCollector {
|
|
13
|
+
private metrics: MetricValue[] = [];
|
|
14
|
+
|
|
15
|
+
counter(name: string, value = 1, labels: Record<string, string> = {}): void {
|
|
16
|
+
this.metrics.push({
|
|
17
|
+
name,
|
|
18
|
+
type: MetricType.COUNTER,
|
|
19
|
+
value,
|
|
20
|
+
labels,
|
|
21
|
+
timestamp: new Date(),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
gauge(name: string, value: number, labels: Record<string, string> = {}): void {
|
|
26
|
+
// Remove previous gauge with same name and labels
|
|
27
|
+
this.metrics = this.metrics.filter(m =>
|
|
28
|
+
!(m.name === name &&
|
|
29
|
+
m.type === MetricType.GAUGE &&
|
|
30
|
+
JSON.stringify(m.labels) === JSON.stringify(labels))
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
this.metrics.push({
|
|
34
|
+
name,
|
|
35
|
+
type: MetricType.GAUGE,
|
|
36
|
+
value,
|
|
37
|
+
labels,
|
|
38
|
+
timestamp: new Date(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
histogram(name: string, value: number, labels: Record<string, string> = {}): void {
|
|
43
|
+
this.metrics.push({
|
|
44
|
+
name,
|
|
45
|
+
type: MetricType.HISTOGRAM,
|
|
46
|
+
value,
|
|
47
|
+
labels,
|
|
48
|
+
timestamp: new Date(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getMetrics(): MetricValue[] {
|
|
53
|
+
return [...this.metrics];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
clear(): void {
|
|
57
|
+
this.metrics = [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get metrics by name
|
|
62
|
+
*/
|
|
63
|
+
getMetricsByName(name: string): MetricValue[] {
|
|
64
|
+
return this.metrics.filter(m => m.name === name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get metrics by type
|
|
69
|
+
*/
|
|
70
|
+
getMetricsByType(type: MetricType): MetricValue[] {
|
|
71
|
+
return this.metrics.filter(m => m.type === type);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the latest value for a metric
|
|
76
|
+
*/
|
|
77
|
+
getLatestValue(name: string): number | undefined {
|
|
78
|
+
const metrics = this.getMetricsByName(name);
|
|
79
|
+
if (metrics.length === 0) return undefined;
|
|
80
|
+
|
|
81
|
+
return metrics.sort((a, b) =>
|
|
82
|
+
b.timestamp.getTime() - a.timestamp.getTime()
|
|
83
|
+
)[0].value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTLP Exporter - OpenTelemetry Protocol Export for Distributed Tracing
|
|
3
|
+
*
|
|
4
|
+
* Production-grade observability export supporting:
|
|
5
|
+
* - OTLP/HTTP trace export (Jaeger, Datadog, Honeycomb, etc.)
|
|
6
|
+
* - Batched export with configurable intervals
|
|
7
|
+
* - W3C trace context propagation
|
|
8
|
+
* - Automatic retry with backoff
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TraceSpan, SpanStatus, MetricValue } from './types.js';
|
|
12
|
+
|
|
13
|
+
/** OTLP export configuration */
|
|
14
|
+
export interface OTLPExporterConfig {
|
|
15
|
+
/** OTLP endpoint URL (e.g., https://api.honeycomb.io/v1/traces) */
|
|
16
|
+
readonly endpoint: string;
|
|
17
|
+
/** Service name for trace attribution */
|
|
18
|
+
readonly serviceName: string;
|
|
19
|
+
/** Optional headers (e.g., API keys) */
|
|
20
|
+
readonly headers?: Record<string, string>;
|
|
21
|
+
/** Batch size before export (default: 50) */
|
|
22
|
+
readonly batchSize?: number;
|
|
23
|
+
/** Export interval in ms (default: 5000) */
|
|
24
|
+
readonly exportIntervalMs?: number;
|
|
25
|
+
/** Max queue size before dropping (default: 2048) */
|
|
26
|
+
readonly maxQueueSize?: number;
|
|
27
|
+
/** Export timeout in ms (default: 30000) */
|
|
28
|
+
readonly timeoutMs?: number;
|
|
29
|
+
/** Enable console logging for debug (default: false) */
|
|
30
|
+
readonly debug?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** OTLP span format */
|
|
34
|
+
interface OTLPSpan {
|
|
35
|
+
traceId: string;
|
|
36
|
+
spanId: string;
|
|
37
|
+
parentSpanId?: string;
|
|
38
|
+
name: string;
|
|
39
|
+
kind: number;
|
|
40
|
+
startTimeUnixNano: string;
|
|
41
|
+
endTimeUnixNano?: string;
|
|
42
|
+
status: { code: number; message?: string };
|
|
43
|
+
attributes: Array<{ key: string; value: { stringValue?: string; intValue?: number; boolValue?: boolean } }>;
|
|
44
|
+
events: Array<{
|
|
45
|
+
name: string;
|
|
46
|
+
timeUnixNano: string;
|
|
47
|
+
attributes?: Array<{ key: string; value: { stringValue: string } }>;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Export batch request */
|
|
52
|
+
interface OTLPTraceRequest {
|
|
53
|
+
resourceSpans: Array<{
|
|
54
|
+
resource: {
|
|
55
|
+
attributes: Array<{ key: string; value: { stringValue: string } }>;
|
|
56
|
+
};
|
|
57
|
+
scopeSpans: Array<{
|
|
58
|
+
scope: { name: string; version: string };
|
|
59
|
+
spans: OTLPSpan[];
|
|
60
|
+
}>;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* OTLP Trace Exporter - exports spans to OTLP-compatible backends.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const exporter = new OTLPTraceExporter({
|
|
69
|
+
* endpoint: 'https://api.honeycomb.io/v1/traces',
|
|
70
|
+
* serviceName: 'my-agent',
|
|
71
|
+
* headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY },
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* // Export spans
|
|
75
|
+
* exporter.addSpan(span);
|
|
76
|
+
*
|
|
77
|
+
* // Start automatic export
|
|
78
|
+
* exporter.start();
|
|
79
|
+
*
|
|
80
|
+
* // Clean shutdown
|
|
81
|
+
* await exporter.shutdown();
|
|
82
|
+
*/
|
|
83
|
+
export class OTLPTraceExporter {
|
|
84
|
+
private readonly config: Required<Omit<OTLPExporterConfig, 'headers'>> &
|
|
85
|
+
Pick<OTLPExporterConfig, 'headers'>;
|
|
86
|
+
private spanQueue: TraceSpan[] = [];
|
|
87
|
+
private exportTimer: NodeJS.Timeout | null = null;
|
|
88
|
+
private isShuttingDown = false;
|
|
89
|
+
|
|
90
|
+
constructor(config: OTLPExporterConfig) {
|
|
91
|
+
this.config = {
|
|
92
|
+
endpoint: config.endpoint,
|
|
93
|
+
serviceName: config.serviceName,
|
|
94
|
+
headers: config.headers,
|
|
95
|
+
batchSize: config.batchSize ?? 50,
|
|
96
|
+
exportIntervalMs: config.exportIntervalMs ?? 5_000,
|
|
97
|
+
maxQueueSize: config.maxQueueSize ?? 2048,
|
|
98
|
+
timeoutMs: config.timeoutMs ?? 30_000,
|
|
99
|
+
debug: config.debug ?? false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Add a span to the export queue */
|
|
104
|
+
addSpan(span: TraceSpan): void {
|
|
105
|
+
if (this.isShuttingDown) return;
|
|
106
|
+
|
|
107
|
+
if (this.spanQueue.length >= this.config.maxQueueSize) {
|
|
108
|
+
this.log('warn', `Queue full (${this.config.maxQueueSize}), dropping oldest spans`);
|
|
109
|
+
this.spanQueue.shift();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.spanQueue.push(span);
|
|
113
|
+
|
|
114
|
+
// Export immediately if batch is full
|
|
115
|
+
if (this.spanQueue.length >= this.config.batchSize) {
|
|
116
|
+
this.export().catch(e => this.log('error', `Export failed: ${e}`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Add multiple spans */
|
|
121
|
+
addSpans(spans: TraceSpan[]): void {
|
|
122
|
+
for (const span of spans) {
|
|
123
|
+
this.addSpan(span);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Start automatic periodic export */
|
|
128
|
+
start(): void {
|
|
129
|
+
if (this.exportTimer) return;
|
|
130
|
+
|
|
131
|
+
this.exportTimer = setInterval(() => {
|
|
132
|
+
this.export().catch(e => this.log('error', `Periodic export failed: ${e}`));
|
|
133
|
+
}, this.config.exportIntervalMs);
|
|
134
|
+
|
|
135
|
+
this.log('debug', `Started with interval ${this.config.exportIntervalMs}ms`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Stop automatic export and flush remaining spans */
|
|
139
|
+
async shutdown(): Promise<void> {
|
|
140
|
+
this.isShuttingDown = true;
|
|
141
|
+
|
|
142
|
+
if (this.exportTimer) {
|
|
143
|
+
clearInterval(this.exportTimer);
|
|
144
|
+
this.exportTimer = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Flush remaining spans
|
|
148
|
+
if (this.spanQueue.length > 0) {
|
|
149
|
+
await this.export();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.log('debug', 'Shutdown complete');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Force export current batch */
|
|
156
|
+
async export(): Promise<{ success: boolean; exported: number; errors?: string[] }> {
|
|
157
|
+
if (this.spanQueue.length === 0) {
|
|
158
|
+
return { success: true, exported: 0 };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const batch = this.spanQueue.splice(0, this.config.batchSize);
|
|
162
|
+
const request = this.buildRequest(batch);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
167
|
+
|
|
168
|
+
const response = await fetch(this.config.endpoint, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
...this.config.headers,
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify(request),
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
clearTimeout(timeoutId);
|
|
179
|
+
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
const errorText = await response.text();
|
|
182
|
+
this.log('error', `Export failed: ${response.status} ${errorText}`);
|
|
183
|
+
// Re-queue failed spans
|
|
184
|
+
this.spanQueue.unshift(...batch);
|
|
185
|
+
return { success: false, exported: 0, errors: [errorText] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.log('debug', `Exported ${batch.length} spans`);
|
|
189
|
+
return { success: true, exported: batch.length };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
this.log('error', `Export error: ${message}`);
|
|
193
|
+
// Re-queue failed spans
|
|
194
|
+
this.spanQueue.unshift(...batch);
|
|
195
|
+
return { success: false, exported: 0, errors: [message] };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Get current queue size */
|
|
200
|
+
getQueueSize(): number {
|
|
201
|
+
return this.spanQueue.length;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- Private methods ---
|
|
205
|
+
|
|
206
|
+
private buildRequest(spans: TraceSpan[]): OTLPTraceRequest {
|
|
207
|
+
return {
|
|
208
|
+
resourceSpans: [
|
|
209
|
+
{
|
|
210
|
+
resource: {
|
|
211
|
+
attributes: [
|
|
212
|
+
{ key: 'service.name', value: { stringValue: this.config.serviceName } },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
scopeSpans: [
|
|
216
|
+
{
|
|
217
|
+
scope: { name: '@confused-ai/core', version: '0.1.0' },
|
|
218
|
+
spans: spans.map(span => this.convertSpan(span)),
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private convertSpan(span: TraceSpan): OTLPSpan {
|
|
227
|
+
return {
|
|
228
|
+
traceId: span.traceId,
|
|
229
|
+
spanId: String(span.id),
|
|
230
|
+
parentSpanId: span.parentId ? String(span.parentId) : undefined,
|
|
231
|
+
name: span.name,
|
|
232
|
+
kind: 1, // INTERNAL
|
|
233
|
+
startTimeUnixNano: String(span.startTime.getTime() * 1_000_000),
|
|
234
|
+
endTimeUnixNano: span.endTime ? String(span.endTime.getTime() * 1_000_000) : undefined,
|
|
235
|
+
status: {
|
|
236
|
+
code: this.statusToCode(span.status),
|
|
237
|
+
},
|
|
238
|
+
attributes: Object.entries(span.attributes).map(([key, value]) => ({
|
|
239
|
+
key,
|
|
240
|
+
value: this.convertAttributeValue(value),
|
|
241
|
+
})),
|
|
242
|
+
events: span.events.map(event => ({
|
|
243
|
+
name: event.name,
|
|
244
|
+
timeUnixNano: String(event.timestamp.getTime() * 1_000_000),
|
|
245
|
+
attributes: event.attributes
|
|
246
|
+
? Object.entries(event.attributes).map(([key, value]) => ({
|
|
247
|
+
key,
|
|
248
|
+
value: { stringValue: String(value) },
|
|
249
|
+
}))
|
|
250
|
+
: undefined,
|
|
251
|
+
})),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private statusToCode(status: SpanStatus): number {
|
|
256
|
+
switch (status) {
|
|
257
|
+
case 'ok':
|
|
258
|
+
return 1;
|
|
259
|
+
case 'error':
|
|
260
|
+
return 2;
|
|
261
|
+
default:
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private convertAttributeValue(value: unknown): { stringValue?: string; intValue?: number; boolValue?: boolean } {
|
|
267
|
+
if (typeof value === 'string') {
|
|
268
|
+
return { stringValue: value };
|
|
269
|
+
}
|
|
270
|
+
if (typeof value === 'number') {
|
|
271
|
+
return { intValue: value };
|
|
272
|
+
}
|
|
273
|
+
if (typeof value === 'boolean') {
|
|
274
|
+
return { boolValue: value };
|
|
275
|
+
}
|
|
276
|
+
return { stringValue: String(value) };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private log(level: 'debug' | 'warn' | 'error', message: string): void {
|
|
280
|
+
if (!this.config.debug && level === 'debug') return;
|
|
281
|
+
|
|
282
|
+
const prefix = `[OTLPExporter] [${level.toUpperCase()}]`;
|
|
283
|
+
if (level === 'error') {
|
|
284
|
+
console.error(prefix, message);
|
|
285
|
+
} else if (level === 'warn') {
|
|
286
|
+
console.warn(prefix, message);
|
|
287
|
+
} else {
|
|
288
|
+
console.log(prefix, message);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* OTLP Metrics Exporter - exports metrics to OTLP-compatible backends.
|
|
295
|
+
*/
|
|
296
|
+
export class OTLPMetricsExporter {
|
|
297
|
+
private readonly config: Required<Omit<OTLPExporterConfig, 'headers'>> &
|
|
298
|
+
Pick<OTLPExporterConfig, 'headers'>;
|
|
299
|
+
private metricsQueue: MetricValue[] = [];
|
|
300
|
+
private exportTimer: NodeJS.Timeout | null = null;
|
|
301
|
+
private isShuttingDown = false;
|
|
302
|
+
|
|
303
|
+
constructor(config: OTLPExporterConfig) {
|
|
304
|
+
this.config = {
|
|
305
|
+
endpoint: config.endpoint,
|
|
306
|
+
serviceName: config.serviceName,
|
|
307
|
+
headers: config.headers,
|
|
308
|
+
batchSize: config.batchSize ?? 100,
|
|
309
|
+
exportIntervalMs: config.exportIntervalMs ?? 10_000,
|
|
310
|
+
maxQueueSize: config.maxQueueSize ?? 4096,
|
|
311
|
+
timeoutMs: config.timeoutMs ?? 30_000,
|
|
312
|
+
debug: config.debug ?? false,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Add a metric to the export queue */
|
|
317
|
+
addMetric(metric: MetricValue): void {
|
|
318
|
+
if (this.isShuttingDown) return;
|
|
319
|
+
|
|
320
|
+
if (this.metricsQueue.length >= this.config.maxQueueSize) {
|
|
321
|
+
this.metricsQueue.shift();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.metricsQueue.push(metric);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Start automatic periodic export */
|
|
328
|
+
start(): void {
|
|
329
|
+
if (this.exportTimer) return;
|
|
330
|
+
|
|
331
|
+
this.exportTimer = setInterval(() => {
|
|
332
|
+
this.export().catch(e => console.error('[OTLPMetrics] Export failed:', e));
|
|
333
|
+
}, this.config.exportIntervalMs);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Stop and flush */
|
|
337
|
+
async shutdown(): Promise<void> {
|
|
338
|
+
this.isShuttingDown = true;
|
|
339
|
+
|
|
340
|
+
if (this.exportTimer) {
|
|
341
|
+
clearInterval(this.exportTimer);
|
|
342
|
+
this.exportTimer = null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (this.metricsQueue.length > 0) {
|
|
346
|
+
await this.export();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Export current batch */
|
|
351
|
+
async export(): Promise<{ success: boolean; exported: number }> {
|
|
352
|
+
if (this.metricsQueue.length === 0) {
|
|
353
|
+
return { success: true, exported: 0 };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const batch = this.metricsQueue.splice(0, this.config.batchSize);
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const response = await fetch(this.config.endpoint, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: {
|
|
362
|
+
'Content-Type': 'application/json',
|
|
363
|
+
...this.config.headers,
|
|
364
|
+
},
|
|
365
|
+
body: JSON.stringify(this.buildMetricsRequest(batch)),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (!response.ok) {
|
|
369
|
+
this.metricsQueue.unshift(...batch);
|
|
370
|
+
return { success: false, exported: 0 };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { success: true, exported: batch.length };
|
|
374
|
+
} catch {
|
|
375
|
+
this.metricsQueue.unshift(...batch);
|
|
376
|
+
return { success: false, exported: 0 };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private buildMetricsRequest(metrics: MetricValue[]) {
|
|
381
|
+
return {
|
|
382
|
+
resourceMetrics: [
|
|
383
|
+
{
|
|
384
|
+
resource: {
|
|
385
|
+
attributes: [
|
|
386
|
+
{ key: 'service.name', value: { stringValue: this.config.serviceName } },
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
scopeMetrics: [
|
|
390
|
+
{
|
|
391
|
+
scope: { name: '@confused-ai/core', version: '0.1.0' },
|
|
392
|
+
metrics: metrics.map(m => ({
|
|
393
|
+
name: m.name,
|
|
394
|
+
description: '',
|
|
395
|
+
unit: '',
|
|
396
|
+
sum: {
|
|
397
|
+
dataPoints: [
|
|
398
|
+
{
|
|
399
|
+
asDouble: m.value,
|
|
400
|
+
timeUnixNano: String(m.timestamp.getTime() * 1_000_000),
|
|
401
|
+
attributes: Object.entries(m.labels).map(([k, v]) => ({
|
|
402
|
+
key: k,
|
|
403
|
+
value: { stringValue: v },
|
|
404
|
+
})),
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
aggregationTemporality: 2, // CUMULATIVE
|
|
408
|
+
isMonotonic: m.type === 'counter',
|
|
409
|
+
},
|
|
410
|
+
})),
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|