@cat-factory/observability-otel 0.2.10

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.
@@ -0,0 +1,304 @@
1
+ // The SINGLE source of truth for how a cat-factory observability event becomes
2
+ // OpenTelemetry telemetry. BOTH transports import from here — the workerd-safe fetch
3
+ // exporter (`./index`) and the official-SDK exporter (`./node`) — so the attribute keys,
4
+ // metric names/units, span shape, trace-id grouping and timestamps they emit are
5
+ // identical by construction. `src/conformity.test.ts` feeds the same events through both
6
+ // and asserts the emitted telemetry matches; this module is why that holds.
7
+ //
8
+ // Nothing here depends on `@opentelemetry/*` or on any runtime API beyond the `crypto`
9
+ // global (present on both workerd and Node), so it stays importable from the fetch entry
10
+ // that must not pull the Node-only SDK into the Worker bundle.
11
+ /** Default OTLP resource `service.name`; overridable via `OTEL_SERVICE_NAME`. */
12
+ export const DEFAULT_SERVICE_NAME = 'cat-factory';
13
+ /** The instrumentation scope name stamped on every emitted span/metric. */
14
+ export const SCOPE_NAME = '@cat-factory/observability-otel';
15
+ /**
16
+ * Attribute keys, following the OpenTelemetry GenAI semantic conventions where they exist
17
+ * (`gen_ai.*`) plus a small `cat_factory.*` namespace for our own dimensions. Centralised
18
+ * so the two transports can never disagree on a key.
19
+ */
20
+ export const ATTR = {
21
+ system: 'gen_ai.system',
22
+ requestModel: 'gen_ai.request.model',
23
+ inputTokens: 'gen_ai.usage.input_tokens',
24
+ outputTokens: 'gen_ai.usage.output_tokens',
25
+ finishReasons: 'gen_ai.response.finish_reasons',
26
+ tokenType: 'gen_ai.token.type',
27
+ workspaceId: 'cat_factory.workspace_id',
28
+ agentKind: 'cat_factory.agent_kind',
29
+ serviceName: 'service.name',
30
+ };
31
+ /** Metric names + units (OTel GenAI client metrics). */
32
+ export const METRIC = {
33
+ tokenUsage: 'gen_ai.client.token.usage',
34
+ duration: 'gen_ai.client.operation.duration',
35
+ };
36
+ export const TOKEN_UNIT = '{token}';
37
+ export const DURATION_UNIT = 's';
38
+ /** Span event names carrying the prompt / completion bodies (only when recorded). */
39
+ const EVENT = {
40
+ prompt: 'gen_ai.content.prompt',
41
+ completion: 'gen_ai.content.completion',
42
+ };
43
+ const EVENT_ATTR = {
44
+ prompt: 'gen_ai.prompt',
45
+ completion: 'gen_ai.completion',
46
+ };
47
+ /** Hex-encode `bytes` random bytes via the `crypto` global (both workerd and Node). */
48
+ function randomHex(bytes) {
49
+ const arr = new Uint8Array(bytes);
50
+ crypto.getRandomValues(arr);
51
+ let out = '';
52
+ for (const b of arr)
53
+ out += b.toString(16).padStart(2, '0');
54
+ return out;
55
+ }
56
+ /**
57
+ * Deterministic hex of `bytes` bytes from a string, so every call of a run derives the
58
+ * SAME trace id and the backend groups them into one trace — the fetch and SDK transports
59
+ * therefore agree without sharing state. FNV-1a, re-hashed with a counter salt to fill the
60
+ * width. Guaranteed non-zero (OTLP rejects an all-zero id).
61
+ */
62
+ function hashHex(input, bytes) {
63
+ let out = '';
64
+ for (let i = 0; out.length < bytes * 2; i++) {
65
+ let h = 0x811c9dc5;
66
+ const salted = `${i}:${input}`;
67
+ for (let j = 0; j < salted.length; j++) {
68
+ h ^= salted.charCodeAt(j);
69
+ h = Math.imul(h, 0x01000193);
70
+ }
71
+ out += (h >>> 0).toString(16).padStart(8, '0');
72
+ }
73
+ out = out.slice(0, bytes * 2);
74
+ return /^0+$/.test(out) ? `${out.slice(0, -1)}1` : out;
75
+ }
76
+ export function randomTraceId() {
77
+ return randomHex(16);
78
+ }
79
+ export function randomSpanId() {
80
+ return randomHex(8);
81
+ }
82
+ /** The trace id a call belongs to: derived from its run, else a fresh standalone id. */
83
+ function deriveTraceId(executionId) {
84
+ return executionId ? hashHex(executionId, 16) : randomTraceId();
85
+ }
86
+ /** Epoch ms → OTLP unix-nano string (string arithmetic avoids float precision loss). */
87
+ export function toUnixNano(ms) {
88
+ return `${Math.round(ms)}000000`;
89
+ }
90
+ /**
91
+ * The LOW-cardinality dimensions safe to carry on a metric time series: provider, model,
92
+ * and agent kind are all bounded sets. Deliberately EXCLUDES the workspace id — it is
93
+ * unbounded (one value per tenant), so putting it on a metric would explode the backend's
94
+ * time-series cardinality (and cost). The workspace id belongs on spans, where high
95
+ * cardinality is expected; see {@link generationDimensions}.
96
+ */
97
+ function metricDimensions(event) {
98
+ return {
99
+ [ATTR.system]: event.provider,
100
+ [ATTR.requestModel]: event.model,
101
+ [ATTR.agentKind]: event.agentKind,
102
+ };
103
+ }
104
+ /** The dimensions on a generation's SPAN: the metric dimensions plus the workspace id. */
105
+ function generationDimensions(event) {
106
+ const attrs = metricDimensions(event);
107
+ if (event.workspaceId)
108
+ attrs[ATTR.workspaceId] = event.workspaceId;
109
+ return attrs;
110
+ }
111
+ /** Map one completed LLM call to a neutral span. */
112
+ export function mapGeneration(event) {
113
+ const attributes = {
114
+ ...generationDimensions(event),
115
+ [ATTR.inputTokens]: event.promptTokens,
116
+ [ATTR.outputTokens]: event.completionTokens,
117
+ };
118
+ if (event.finishReason)
119
+ attributes[ATTR.finishReasons] = [event.finishReason];
120
+ const events = [];
121
+ // Bodies are present only when prompt recording is on (upstream blanks them otherwise),
122
+ // so an empty string means "not recorded" and is omitted — matching the Langfuse sink.
123
+ if (event.input) {
124
+ events.push({
125
+ name: EVENT.prompt,
126
+ timeMs: event.startedAt,
127
+ attributes: { [EVENT_ATTR.prompt]: event.input },
128
+ });
129
+ }
130
+ if (event.output) {
131
+ events.push({
132
+ name: EVENT.completion,
133
+ timeMs: event.endedAt,
134
+ attributes: { [EVENT_ATTR.completion]: event.output },
135
+ });
136
+ }
137
+ return {
138
+ traceId: deriveTraceId(event.executionId),
139
+ name: event.agentKind,
140
+ startTimeMs: event.startedAt,
141
+ endTimeMs: event.endedAt,
142
+ ok: event.ok,
143
+ ...(event.ok ? {} : { statusMessage: event.errorMessage ?? undefined }),
144
+ attributes,
145
+ events,
146
+ };
147
+ }
148
+ /** Map one completed LLM call to its token-usage + duration metrics. */
149
+ export function mapGenerationMetrics(event) {
150
+ // Metrics use the low-cardinality dimensions only (no workspace id) — see metricDimensions.
151
+ const dims = metricDimensions(event);
152
+ return {
153
+ tokenUsage: [
154
+ { value: event.promptTokens, attributes: { ...dims, [ATTR.tokenType]: 'input' } },
155
+ { value: event.completionTokens, attributes: { ...dims, [ATTR.tokenType]: 'output' } },
156
+ ],
157
+ durationSeconds: Math.max(0, event.endedAt - event.startedAt) / 1000,
158
+ durationAttributes: dims,
159
+ startTimeMs: event.startedAt,
160
+ endTimeMs: event.endedAt,
161
+ };
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Platform-operator observability → OpenTelemetry metrics. The dual of the per-run
165
+ // generation metrics above: where those describe ONE LLM call, these describe the
166
+ // deployment's aggregate run health (outcomes, failure taxonomy, live/parked depth,
167
+ // durations) over a trailing window, scoped to an account. A periodic sweep computes the
168
+ // `PlatformObservability` projection and feeds it here; the fetch exporter (`./platform`)
169
+ // encodes the result as OTLP GAUGE data points (point-in-time aggregates the OTel backend
170
+ // itself trends over the series). Emitted here (not `./index`) so the mapping stays the
171
+ // single source of truth for both signals.
172
+ // ---------------------------------------------------------------------------
173
+ /** Metric names for the deployment-level platform observability gauges. */
174
+ export const PLATFORM_METRIC = {
175
+ /** Windowed run count, split by run status (done/failed/running/…). */
176
+ runs: 'cat_factory.platform.runs',
177
+ /** Windowed `done / (done + failed)` success ratio (0..1). */
178
+ runSuccessRate: 'cat_factory.platform.run_success_rate',
179
+ /** Windowed failed-run count, split by failure kind. */
180
+ runFailures: 'cat_factory.platform.run_failures',
181
+ /** Current live/parked run count (a snapshot, not windowed), split by lifecycle state. */
182
+ liveRuns: 'cat_factory.platform.live_runs',
183
+ /** Windowed wall-clock run duration (seconds), split by statistic (avg/min/max/pNN). */
184
+ runDuration: 'cat_factory.platform.run_duration',
185
+ };
186
+ /**
187
+ * Attribute keys for the platform metrics. `account_id` is the tenant scope (bounded — the
188
+ * billing entity, far fewer than workspaces, so safe on a metric time series, unlike the
189
+ * workspace id excluded from the per-call metrics); `window` labels the trailing aggregation
190
+ * window. The remaining keys are the bounded split dimensions of each gauge.
191
+ */
192
+ export const PLATFORM_ATTR = {
193
+ accountId: 'cat_factory.account_id',
194
+ window: 'cat_factory.window',
195
+ runStatus: 'cat_factory.run_status',
196
+ runState: 'cat_factory.run_state',
197
+ failureKind: 'cat_factory.failure_kind',
198
+ durationStat: 'cat_factory.duration_stat',
199
+ };
200
+ /** Metric units: a dimensionless run count, a dimensionless ratio, and seconds. */
201
+ export const RUN_UNIT = '{run}';
202
+ export const RATIO_UNIT = '1';
203
+ /**
204
+ * Map a {@link PlatformObservability} projection to the OpenTelemetry gauge metrics. All are
205
+ * point-in-time gauges (the OTel backend builds trends from the series over time), stamped
206
+ * with the projection's `generatedAt`. Every point carries the `account_id`; the windowed
207
+ * gauges additionally carry the `window` label. Null/absent aggregates (e.g. a success rate
208
+ * or percentiles with no terminal runs) are omitted rather than emitted as a misleading zero.
209
+ */
210
+ export function mapPlatformMetrics(snapshot, dims) {
211
+ const base = { [PLATFORM_ATTR.accountId]: dims.accountId };
212
+ const windowed = { ...base, [PLATFORM_ATTR.window]: snapshot.window };
213
+ const gauges = [];
214
+ const o = snapshot.outcomes;
215
+ const runStatuses = [
216
+ ['done', o.done],
217
+ ['failed', o.failed],
218
+ ['running', o.running],
219
+ ['blocked', o.blocked],
220
+ ['paused', o.paused],
221
+ ['other', o.other],
222
+ ];
223
+ gauges.push({
224
+ name: PLATFORM_METRIC.runs,
225
+ unit: RUN_UNIT,
226
+ points: runStatuses.map(([status, value]) => ({
227
+ attributes: { ...windowed, [PLATFORM_ATTR.runStatus]: status },
228
+ value,
229
+ isInt: true,
230
+ })),
231
+ });
232
+ if (o.successRate !== null) {
233
+ gauges.push({
234
+ name: PLATFORM_METRIC.runSuccessRate,
235
+ unit: RATIO_UNIT,
236
+ points: [{ attributes: windowed, value: o.successRate, isInt: false }],
237
+ });
238
+ }
239
+ if (snapshot.failures.length > 0) {
240
+ gauges.push({
241
+ name: PLATFORM_METRIC.runFailures,
242
+ unit: RUN_UNIT,
243
+ points: snapshot.failures.map((f) => ({
244
+ attributes: { ...windowed, [PLATFORM_ATTR.failureKind]: f.kind },
245
+ value: f.count,
246
+ isInt: true,
247
+ })),
248
+ });
249
+ }
250
+ const live = snapshot.live;
251
+ gauges.push({
252
+ name: PLATFORM_METRIC.liveRuns,
253
+ unit: RUN_UNIT,
254
+ // Live/parked depth is a snapshot, NOT windowed — so these points carry no window label.
255
+ points: [
256
+ ['running', live.running],
257
+ ['blocked', live.blocked],
258
+ ['paused', live.paused],
259
+ ['pending', live.pending],
260
+ ].map(([state, value]) => ({
261
+ attributes: { ...base, [PLATFORM_ATTR.runState]: state },
262
+ value,
263
+ isInt: true,
264
+ })),
265
+ });
266
+ const d = snapshot.durations;
267
+ const durationStats = [
268
+ ['avg', d.avgMs],
269
+ ['min', d.minMs],
270
+ ['max', d.maxMs],
271
+ ['p50', d.p50Ms],
272
+ ['p90', d.p90Ms],
273
+ ['p99', d.p99Ms],
274
+ ];
275
+ const durationPoints = durationStats
276
+ .filter(([, ms]) => ms !== null)
277
+ .map(([stat, ms]) => ({
278
+ attributes: { ...windowed, [PLATFORM_ATTR.durationStat]: stat },
279
+ // OTel duration convention is seconds; the projection carries ms.
280
+ value: ms / 1000,
281
+ isInt: false,
282
+ }));
283
+ if (durationPoints.length > 0) {
284
+ gauges.push({ name: PLATFORM_METRIC.runDuration, unit: DURATION_UNIT, points: durationPoints });
285
+ }
286
+ return gauges;
287
+ }
288
+ /** Map one container tool call to a neutral span under its run's trace. */
289
+ export function mapToolSpan(context, span) {
290
+ const attributes = { [ATTR.agentKind]: context.agentKind };
291
+ if (context.workspaceId)
292
+ attributes[ATTR.workspaceId] = context.workspaceId;
293
+ return {
294
+ // Tool spans only reach here with a non-null executionId (the sinks guard on it).
295
+ traceId: deriveTraceId(context.executionId),
296
+ name: span.tool,
297
+ startTimeMs: span.startedAt,
298
+ endTimeMs: span.endedAt,
299
+ ok: span.ok,
300
+ attributes,
301
+ events: [],
302
+ };
303
+ }
304
+ //# sourceMappingURL=mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.js","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":"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"}
package/dist/node.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { type SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
+ import { type MetricReader } from '@opentelemetry/sdk-metrics';
3
+ import type { LlmGenerationEvent, LlmToolSpan, LlmToolSpanContext, LlmTraceSink } from '@cat-factory/kernel';
4
+ import type { OtelLogger } from './index.js';
5
+ export interface NodeOtelSinkConfig {
6
+ /** OTLP/HTTP base URL, e.g. `http://collector:4318` (the `/v1/*` paths are appended). */
7
+ endpoint: string;
8
+ /** Extra headers merged onto every OTLP request (auth tokens, tenant ids, …). */
9
+ headers?: Record<string, string>;
10
+ /** OTLP resource `service.name`; defaults to `cat-factory`. */
11
+ serviceName?: string;
12
+ /** Optional logger for swallowed errors. */
13
+ logger?: OtelLogger;
14
+ /** Test seam: override the span processor (e.g. a SimpleSpanProcessor over an in-memory exporter). */
15
+ spanProcessor?: SpanProcessor;
16
+ /** Test seam: override the metric reader (e.g. one over an InMemoryMetricExporter). */
17
+ metricReader?: MetricReader;
18
+ }
19
+ export declare class NodeOtelTraceSink implements LlmTraceSink {
20
+ private readonly tracerProvider;
21
+ private readonly meterProvider;
22
+ private readonly idGenerator;
23
+ private readonly logger?;
24
+ private readonly startSpanForRun;
25
+ private readonly tokenCounter;
26
+ private readonly durationHistogram;
27
+ constructor(config: NodeOtelSinkConfig);
28
+ recordGeneration(event: LlmGenerationEvent): void;
29
+ recordToolSpans(context: LlmToolSpanContext, spans: LlmToolSpan[]): void;
30
+ private emitSpan;
31
+ /** Flush any buffered spans/metrics (e.g. before shutdown). Best-effort. */
32
+ forceFlush(): Promise<void>;
33
+ /** Shut the providers down, flushing first. Wire into the facade's stop path. */
34
+ shutdown(): Promise<void>;
35
+ private warn;
36
+ }
37
+ /** Build an SDK-backed {@link NodeOtelTraceSink}. The Node/local facades' OTLP exporter. */
38
+ export declare function createNodeOtelSink(config: NodeOtelSinkConfig): NodeOtelTraceSink;
39
+ //# sourceMappingURL=node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,+BAA+B,CAAA;AAEtC,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,4BAA4B,CAAA;AAInC,OAAO,KAAK,EACV,kBAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,YAAY,EACb,MAAM,qBAAqB,CAAA;AAe5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAe5C,MAAM,WAAW,kBAAkB;IACjC,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAA;IAChB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,sGAAsG;IACtG,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,uFAAuF;IACvF,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B;AAmBD,qBAAa,iBAAkB,YAAW,YAAY;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAoB;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAY;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAMvB;IACT,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAW;IAE7C,YAAY,MAAM,EAAE,kBAAkB,EAyCrC;IAED,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAehD;IAED,eAAe,CAAC,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,IAAI,CASvE;IAED,OAAO,CAAC,QAAQ;IAiBhB,4EAA4E;IACtE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAMhC;IAED,iFAAiF;IAC3E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAM9B;IAED,OAAO,CAAC,IAAI;CAMb;AAED,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,iBAAiB,CAEhF"}
package/dist/node.js ADDED
@@ -0,0 +1,131 @@
1
+ import { SpanKind, SpanStatusCode, } from '@opentelemetry/api';
2
+ import { BatchSpanProcessor, } from '@opentelemetry/sdk-trace-base';
3
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
4
+ import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader, } from '@opentelemetry/sdk-metrics';
5
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
6
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
7
+ import { resourceFromAttributes } from '@opentelemetry/resources';
8
+ import { ATTR, DEFAULT_SERVICE_NAME, DURATION_UNIT, METRIC, SCOPE_NAME, TOKEN_UNIT, mapGeneration, mapGenerationMetrics, mapToolSpan, randomSpanId, randomTraceId, } from './mapping.js';
9
+ /**
10
+ * Forces the trace id of the next span so a run's calls share one trace. The SDK calls
11
+ * `generateTraceId()` synchronously while starting a ROOT span (our spans have no active
12
+ * parent), so we set {@link nextTraceId} immediately before `startSpan` and clear it after.
13
+ */
14
+ class RunIdGenerator {
15
+ nextTraceId = null;
16
+ generateTraceId() {
17
+ return this.nextTraceId ?? randomTraceId();
18
+ }
19
+ generateSpanId() {
20
+ return randomSpanId();
21
+ }
22
+ }
23
+ export class NodeOtelTraceSink {
24
+ tracerProvider;
25
+ meterProvider;
26
+ idGenerator;
27
+ logger;
28
+ startSpanForRun;
29
+ tokenCounter;
30
+ durationHistogram;
31
+ constructor(config) {
32
+ const base = config.endpoint.replace(/\/+$/, '');
33
+ const headers = config.headers;
34
+ const resource = resourceFromAttributes({
35
+ [ATTR.serviceName]: config.serviceName || DEFAULT_SERVICE_NAME,
36
+ });
37
+ this.idGenerator = new RunIdGenerator();
38
+ this.logger = config.logger;
39
+ const spanProcessor = config.spanProcessor ??
40
+ new BatchSpanProcessor(new OTLPTraceExporter({ url: `${base}/v1/traces`, headers }));
41
+ this.tracerProvider = new NodeTracerProvider({
42
+ resource,
43
+ idGenerator: this.idGenerator,
44
+ spanProcessors: [spanProcessor],
45
+ });
46
+ const tracer = this.tracerProvider.getTracer(SCOPE_NAME);
47
+ this.startSpanForRun = (name, traceId, startTimeMs, kind, attributes) => {
48
+ this.idGenerator.nextTraceId = traceId;
49
+ try {
50
+ return tracer.startSpan(name, { kind, startTime: startTimeMs, attributes, root: true });
51
+ }
52
+ finally {
53
+ this.idGenerator.nextTraceId = null;
54
+ }
55
+ };
56
+ const metricReader = config.metricReader ??
57
+ new PeriodicExportingMetricReader({
58
+ exporter: new OTLPMetricExporter({
59
+ url: `${base}/v1/metrics`,
60
+ headers,
61
+ temporalityPreference: AggregationTemporality.DELTA,
62
+ }),
63
+ });
64
+ this.meterProvider = new MeterProvider({ resource, readers: [metricReader] });
65
+ const meter = this.meterProvider.getMeter(SCOPE_NAME);
66
+ this.tokenCounter = meter.createCounter(METRIC.tokenUsage, { unit: TOKEN_UNIT });
67
+ this.durationHistogram = meter.createHistogram(METRIC.duration, { unit: DURATION_UNIT });
68
+ }
69
+ recordGeneration(event) {
70
+ try {
71
+ const mapped = mapGeneration(event);
72
+ this.emitSpan(mapped, SpanKind.CLIENT);
73
+ const metrics = mapGenerationMetrics(event);
74
+ for (const point of metrics.tokenUsage) {
75
+ this.tokenCounter.add(point.value, point.attributes);
76
+ }
77
+ this.durationHistogram.record(metrics.durationSeconds, metrics.durationAttributes);
78
+ }
79
+ catch (err) {
80
+ this.warn(err);
81
+ }
82
+ }
83
+ recordToolSpans(context, spans) {
84
+ if (!context.executionId || spans.length === 0)
85
+ return;
86
+ try {
87
+ for (const span of spans) {
88
+ this.emitSpan(mapToolSpan(context, span), SpanKind.INTERNAL);
89
+ }
90
+ }
91
+ catch (err) {
92
+ this.warn(err);
93
+ }
94
+ }
95
+ emitSpan(mapped, kind) {
96
+ const span = this.startSpanForRun(mapped.name, mapped.traceId, mapped.startTimeMs, kind, mapped.attributes);
97
+ for (const event of mapped.events) {
98
+ span.addEvent(event.name, event.attributes, event.timeMs);
99
+ }
100
+ if (!mapped.ok) {
101
+ span.setStatus({ code: SpanStatusCode.ERROR, message: mapped.statusMessage });
102
+ }
103
+ span.end(mapped.endTimeMs);
104
+ }
105
+ /** Flush any buffered spans/metrics (e.g. before shutdown). Best-effort. */
106
+ async forceFlush() {
107
+ try {
108
+ await Promise.all([this.tracerProvider.forceFlush(), this.meterProvider.forceFlush()]);
109
+ }
110
+ catch (err) {
111
+ this.warn(err);
112
+ }
113
+ }
114
+ /** Shut the providers down, flushing first. Wire into the facade's stop path. */
115
+ async shutdown() {
116
+ try {
117
+ await Promise.all([this.tracerProvider.shutdown(), this.meterProvider.shutdown()]);
118
+ }
119
+ catch (err) {
120
+ this.warn(err);
121
+ }
122
+ }
123
+ warn(err) {
124
+ this.logger?.warn({ scope: 'otel', err: err instanceof Error ? err.message : String(err) }, 'otel: failed to record telemetry');
125
+ }
126
+ }
127
+ /** Build an SDK-backed {@link NodeOtelTraceSink}. The Node/local facades' OTLP exporter. */
128
+ export function createNodeOtelSink(config) {
129
+ return new NodeOtelTraceSink(config);
130
+ }
131
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
package/dist/otlp.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { AttributeMap } from './mapping.js';
2
+ /** Minimal structured logger (pino-compatible); optional. */
3
+ export interface OtlpLogger {
4
+ warn(obj: Record<string, unknown>, msg?: string): void;
5
+ }
6
+ /** An OTLP `AnyValue` in the JSON encoding (string / int / double / string list). */
7
+ export type AnyValue = {
8
+ stringValue: string;
9
+ } | {
10
+ intValue: string;
11
+ } | {
12
+ doubleValue: number;
13
+ } | {
14
+ arrayValue: {
15
+ values: AnyValue[];
16
+ };
17
+ };
18
+ /** An OTLP `KeyValue` pair (an attribute). */
19
+ export interface KeyValue {
20
+ key: string;
21
+ value: AnyValue;
22
+ }
23
+ /** Encode a neutral attribute map as an OTLP `KeyValue[]`. */
24
+ export declare function keyValues(attrs: AttributeMap): KeyValue[];
25
+ /**
26
+ * POST an OTLP/JSON payload to `endpoint`, best-effort. Observability must never break the
27
+ * caller, so a non-2xx response or a transport error is only logged (never thrown) and the
28
+ * batch is dropped — the documented worst case. Bounded by {@link SEND_TIMEOUT_MS} so a
29
+ * hung collector can't dangle the caller's budget.
30
+ */
31
+ export declare function postOtlp(opts: {
32
+ fetchImpl: typeof fetch;
33
+ endpoint: string;
34
+ headers: Record<string, string>;
35
+ payload: unknown;
36
+ logger?: OtlpLogger;
37
+ timeoutMs?: number;
38
+ }): Promise<void>;
39
+ //# sourceMappingURL=otlp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otlp.d.ts","sourceRoot":"","sources":["../src/otlp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,cAAc,CAAA;AAWhE,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvD;AAED,qFAAqF;AACrF,MAAM,MAAM,QAAQ,GAChB;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GACvB;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpB;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GACvB;IAAE,UAAU,EAAE;QAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;KAAE,CAAA;CAAE,CAAA;AAE1C,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,QAAQ,CAAA;CAChB;AAaD,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,KAAK,EAAE,YAAY,GAAG,QAAQ,EAAE,CAEzD;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IACnC,SAAS,EAAE,OAAO,KAAK,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBhB"}
package/dist/otlp.js ADDED
@@ -0,0 +1,46 @@
1
+ // Shared OTLP/HTTP JSON encoding + transport helpers used by BOTH fetch-based exporters
2
+ // in this package — the per-call LLM trace/metric exporter (`./index`) and the periodic
3
+ // platform-metrics exporter (`./platform`). Kept here so the two never drift on how an
4
+ // attribute value is encoded or how a batch is POSTed. Nothing here depends on
5
+ // `@opentelemetry/*` (workerd-safe) or on any global beyond `fetch`/`AbortSignal`.
6
+ /** Hard ceiling on a single OTLP POST, so a hung collector can't tie up the caller. */
7
+ const SEND_TIMEOUT_MS = 10_000;
8
+ /** Encode one neutral attribute value as an OTLP `AnyValue`. */
9
+ function anyValue(value) {
10
+ if (Array.isArray(value)) {
11
+ return { arrayValue: { values: value.map((v) => ({ stringValue: String(v) })) } };
12
+ }
13
+ if (typeof value === 'number') {
14
+ return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
15
+ }
16
+ return { stringValue: value };
17
+ }
18
+ /** Encode a neutral attribute map as an OTLP `KeyValue[]`. */
19
+ export function keyValues(attrs) {
20
+ return Object.entries(attrs).map(([key, value]) => ({ key, value: anyValue(value) }));
21
+ }
22
+ /**
23
+ * POST an OTLP/JSON payload to `endpoint`, best-effort. Observability must never break the
24
+ * caller, so a non-2xx response or a transport error is only logged (never thrown) and the
25
+ * batch is dropped — the documented worst case. Bounded by {@link SEND_TIMEOUT_MS} so a
26
+ * hung collector can't dangle the caller's budget.
27
+ */
28
+ export async function postOtlp(opts) {
29
+ try {
30
+ const res = await opts.fetchImpl(opts.endpoint, {
31
+ method: 'POST',
32
+ headers: opts.headers,
33
+ body: JSON.stringify(opts.payload),
34
+ signal: AbortSignal.timeout(opts.timeoutMs ?? SEND_TIMEOUT_MS),
35
+ });
36
+ // OTLP/HTTP returns 200 on full success and may return 200 with a partial-success body;
37
+ // any non-2xx is a failure we only log — observability never breaks the caller.
38
+ if (!res.ok) {
39
+ opts.logger?.warn({ scope: 'otel', status: res.status }, 'otel: OTLP endpoint rejected batch');
40
+ }
41
+ }
42
+ catch (err) {
43
+ opts.logger?.warn({ scope: 'otel', err: err instanceof Error ? err.message : String(err) }, 'otel: failed to POST OTLP batch');
44
+ }
45
+ }
46
+ //# sourceMappingURL=otlp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otlp.js","sourceRoot":"","sources":["../src/otlp.ts"],"names":[],"mappings":"AAEA,wFAAwF;AACxF,wFAAwF;AACxF,uFAAuF;AACvF,+EAA+E;AAC/E,mFAAmF;AAEnF,uFAAuF;AACvF,MAAM,eAAe,GAAG,MAAM,CAAA;AAoB9B,gEAAgE;AAChE,SAAS,QAAQ,CAAC,KAAqB;IACrC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAA;IACnF,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;IACvF,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;AAC/B,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,SAAS,CAAC,KAAmB;IAC3C,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;AACvF,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAO9B;IACC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE;YAC9C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;YAClC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,IAAI,eAAe,CAAC;SAC/D,CAAC,CAAA;QACF,wFAAwF;QACxF,gFAAgF;QAChF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,oCAAoC,CAAC,CAAA;QAChG,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,EAAE,IAAI,CACf,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EACxE,iCAAiC,CAClC,CAAA;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { PlatformObservability } from '@cat-factory/contracts';
2
+ import { type OtlpLogger } from './otlp.js';
3
+ export interface PlatformMetricsOtelExporterConfig {
4
+ /** OTLP/HTTP base URL, e.g. `http://collector:4318` (`/v1/metrics` is appended). */
5
+ endpoint: string;
6
+ /** Extra headers merged onto every request (auth tokens, tenant ids, …). */
7
+ headers?: Record<string, string>;
8
+ /** OTLP resource `service.name`; defaults to `cat-factory`. */
9
+ serviceName?: string;
10
+ /** Optional logger for swallowed errors. */
11
+ logger?: OtlpLogger;
12
+ /** Injectable fetch (tests); defaults to the global `fetch`. */
13
+ fetchImpl?: typeof fetch;
14
+ }
15
+ export declare class PlatformMetricsOtelExporter {
16
+ private readonly metricsEndpoint;
17
+ private readonly headers;
18
+ private readonly serviceName;
19
+ private readonly logger?;
20
+ private readonly fetchImpl;
21
+ constructor(config: PlatformMetricsOtelExporterConfig);
22
+ private resourceAttributes;
23
+ /**
24
+ * Export one account's platform-observability snapshot as OTLP gauge metrics. Gauges are
25
+ * stamped with the snapshot's `generatedAt` (the clock the projection was computed at), so
26
+ * no wall-clock read is needed here. In practice a snapshot always yields the run-count and
27
+ * live-depth gauges (even all-zero — a zero-valued gauge is meaningful, not misleading), so
28
+ * a POST is always sent; the empty-batch guard below is defensive only, in case the mapping
29
+ * ever becomes fully conditional. Best-effort — see the file header.
30
+ */
31
+ export(snapshot: PlatformObservability, dims: {
32
+ accountId: string;
33
+ }): Promise<void>;
34
+ }
35
+ /** Build a fetch-based {@link PlatformMetricsOtelExporter}. The workerd-safe opt-in exporter. */
36
+ export declare function createPlatformMetricsOtelExporter(config: PlatformMetricsOtelExporterConfig): PlatformMetricsOtelExporter;
37
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AASnE,OAAO,EAAiB,KAAK,UAAU,EAAuB,MAAM,WAAW,CAAA;AAoB/E,MAAM,WAAW,iCAAiC;IAChD,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAiBD,qBAAa,2BAA2B;IACtC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAY;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IAExC,YAAY,MAAM,EAAE,iCAAiC,EAOpD;IAED,OAAO,CAAC,kBAAkB;IAI1B;;;;;;;OAOG;IACG,MAAM,CAAC,QAAQ,EAAE,qBAAqB,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBxF;CACF;AAED,iGAAiG;AACjG,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,iCAAiC,GACxC,2BAA2B,CAE7B"}