agentfootprint 2.8.2 → 2.9.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.
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ /**
3
+ * otelObservability — OpenTelemetry distributed-tracing adapter.
4
+ *
5
+ * Ships every agentfootprint event as OpenTelemetry spans + log
6
+ * records via a consumer-supplied OTel API. Same hierarchical
7
+ * mapping as the X-Ray adapter, but the destination is whichever
8
+ * OTel-compat backend the consumer's SDK exports to:
9
+ *
10
+ * - **Honeycomb** (OTLP/HTTP)
11
+ * - **Grafana Cloud / Tempo / Mimir** (OTLP)
12
+ * - **AWS Distro for OTel** → AWS X-Ray (alternative to xrayObservability)
13
+ * - **Datadog APM** (OTLP endpoint)
14
+ * - **Splunk Observability Cloud** (OTLP)
15
+ * - **New Relic** (OTLP endpoint)
16
+ * - **Lightstep / ServiceNow Cloud Observability** (OTLP)
17
+ * - any custom OTel collector / processor pipeline
18
+ *
19
+ * Subpath: `agentfootprint/observability-providers`
20
+ * Peer dep: `@opentelemetry/api` (OPTIONAL — installed only when
21
+ * this adapter is used. The consumer ALSO installs the
22
+ * OTel SDK + exporter of their choice — that's the BYO
23
+ * contract that makes this adapter backend-agnostic.).
24
+ *
25
+ * **Why BYO SDK:** OTel's SDK is heavyweight and exporter-specific
26
+ * (each backend has its own exporter package). Forcing a particular
27
+ * exporter would defeat the "OTel is portable" guarantee. Consumers
28
+ * configure the SDK + exporter once at app startup; we just speak
29
+ * the typed OTel API.
30
+ *
31
+ * Mapping:
32
+ *
33
+ * agent.turn_start ↦ start root span (one trace per turn)
34
+ * agent.turn_end ↦ end root span
35
+ * agent.iteration_start ↦ start child span under root
36
+ * agent.iteration_end ↦ end iteration span
37
+ * stream.llm_start ↦ start child span (model call)
38
+ * stream.llm_end ↦ end llm span
39
+ * stream.tool_start ↦ start child span (tool call)
40
+ * stream.tool_end ↦ end tool span (with `error: true` if errored)
41
+ * cost.tick ↦ setAttribute on topmost active span
42
+ *
43
+ * @example Basic — Honeycomb via OTLP
44
+ * ```ts
45
+ * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
46
+ * import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
47
+ * import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
48
+ * import { trace } from '@opentelemetry/api';
49
+ * import { otelObservability } from 'agentfootprint/observability-providers';
50
+ *
51
+ * // Set up OTel ONCE at app startup.
52
+ * const provider = new NodeTracerProvider();
53
+ * provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({
54
+ * url: 'https://api.honeycomb.io/v1/traces',
55
+ * headers: { 'x-honeycomb-team': process.env.HONEYCOMB_KEY },
56
+ * })));
57
+ * provider.register();
58
+ *
59
+ * agent.enable.observability({
60
+ * strategy: otelObservability({
61
+ * serviceName: 'my-agent',
62
+ * // tracer optional — defaults to trace.getTracer('agentfootprint').
63
+ * }),
64
+ * });
65
+ * ```
66
+ *
67
+ * @example Test injection
68
+ * ```ts
69
+ * otelObservability({
70
+ * serviceName: 'test',
71
+ * tracer: mockTracer, // anything matching the OTel Tracer interface
72
+ * });
73
+ * ```
74
+ */
75
+ Object.defineProperty(exports, "__esModule", { value: true });
76
+ exports.otelObservability = void 0;
77
+ const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
78
+ // ─── Strategy factory ────────────────────────────────────────────────
79
+ function otelObservability(opts) {
80
+ if (!opts.serviceName) {
81
+ throw new TypeError(`[otelObservability] \`serviceName\` is required. ` +
82
+ `Pass an identifier visible in your OTel backend's service map, e.g. 'my-agent-prod'.`);
83
+ }
84
+ const sampleRate = opts.sampleRate ?? 1;
85
+ // Lazy-resolve tracer if not injected. Defer the API import until
86
+ // first event so consumers who don't actually fire events (no agent
87
+ // run yet) don't even hit the OTel API surface.
88
+ let tracer = opts.tracer;
89
+ let otelApi;
90
+ function ensureTracer() {
91
+ if (tracer)
92
+ return tracer;
93
+ if (!otelApi) {
94
+ try {
95
+ otelApi = (0, lazyRequire_js_1.lazyRequire)('@opentelemetry/api');
96
+ }
97
+ catch {
98
+ throw new Error('otelObservability requires the `@opentelemetry/api` peer dependency.\n' +
99
+ ' Install: npm install @opentelemetry/api\n' +
100
+ ' Plus an OTel SDK + exporter for your backend (e.g.,\n' +
101
+ ' `@opentelemetry/sdk-trace-node` + `@opentelemetry/exporter-trace-otlp-http`).\n' +
102
+ ' Or pass `tracer` for test injection.');
103
+ }
104
+ }
105
+ if (!otelApi.trace?.getTracer) {
106
+ throw new Error('otelObservability: `@opentelemetry/api` is installed but `trace.getTracer` not found. Update the package.');
107
+ }
108
+ tracer = otelApi.trace.getTracer('agentfootprint');
109
+ return tracer;
110
+ }
111
+ // Per-turn state — same pattern as xrayObservability. Events for
112
+ // multiple in-flight turns interleave correctly because we key by
113
+ // `runId` from the event payload.
114
+ const activeTurns = new Map();
115
+ let stopped = false;
116
+ let onErrorHook;
117
+ function pushSpan(turnState, name, attrs) {
118
+ // OTel parent-context wiring: we capture the parent in a context
119
+ // and start the new span under it. (For BYO SDK setups, the
120
+ // `trace.setSpan` + `context.with` pattern is canonical. For
121
+ // the test-injected tracer path, we just pass the parent as
122
+ // implicit context.)
123
+ const parent = turnState.stack[turnState.stack.length - 1]?.span;
124
+ let ctx;
125
+ if (parent && otelApi?.trace?.setSpan && otelApi?.context?.active) {
126
+ ctx = otelApi.trace.setSpan(otelApi.context.active(), parent);
127
+ }
128
+ const span = ensureTracer().startSpan(name, attrs ? { attributes: attrs } : undefined, ctx);
129
+ turnState.stack.push({ name, span });
130
+ return span;
131
+ }
132
+ function popSpan(turnState, expectedName) {
133
+ let idx = turnState.stack.length - 1;
134
+ if (expectedName) {
135
+ while (idx >= 0 && turnState.stack[idx].name !== expectedName)
136
+ idx--;
137
+ }
138
+ if (idx < 0)
139
+ return undefined;
140
+ return turnState.stack.splice(idx, 1)[0].span;
141
+ }
142
+ function endSpan(span, opts) {
143
+ if (opts?.error) {
144
+ const code = otelApi?.SpanStatusCode?.ERROR ?? 2;
145
+ try {
146
+ span.setStatus({ code });
147
+ }
148
+ catch {
149
+ /* mock tracers may not implement setStatus — ignore */
150
+ }
151
+ }
152
+ span.end();
153
+ }
154
+ // ─── Event-to-span dispatch ────────────────────────────────────────
155
+ function handleEvent(event) {
156
+ if (stopped)
157
+ return;
158
+ const runId = event.payload?.runId;
159
+ if (!runId)
160
+ return; // Events without a turn anchor — skip.
161
+ switch (event.type) {
162
+ case 'agentfootprint.agent.turn_start': {
163
+ const sampled = sampleRate >= 1 || Math.random() < sampleRate;
164
+ const turnState = { stack: [], sampled };
165
+ activeTurns.set(runId, turnState);
166
+ if (sampled)
167
+ pushSpan(turnState, opts.serviceName, { 'service.name': opts.serviceName });
168
+ break;
169
+ }
170
+ case 'agentfootprint.agent.turn_end': {
171
+ const t = activeTurns.get(runId);
172
+ if (!t)
173
+ break;
174
+ // Defensive: end everything still on the stack.
175
+ while (t.stack.length > 0) {
176
+ const span = popSpan(t);
177
+ if (span)
178
+ endSpan(span);
179
+ }
180
+ activeTurns.delete(runId);
181
+ break;
182
+ }
183
+ case 'agentfootprint.agent.iteration_start': {
184
+ const t = activeTurns.get(runId);
185
+ if (t?.sampled) {
186
+ const iteration = event.payload.iteration;
187
+ pushSpan(t, `iteration:${iteration ?? '?'}`, {
188
+ ...(typeof iteration === 'number' && { 'iteration.number': iteration }),
189
+ });
190
+ }
191
+ break;
192
+ }
193
+ case 'agentfootprint.agent.iteration_end': {
194
+ const t = activeTurns.get(runId);
195
+ if (t?.sampled) {
196
+ const span = popSpan(t);
197
+ if (span)
198
+ endSpan(span);
199
+ }
200
+ break;
201
+ }
202
+ case 'agentfootprint.stream.llm_start': {
203
+ const t = activeTurns.get(runId);
204
+ if (!t?.sampled)
205
+ break;
206
+ const model = event.payload.model;
207
+ pushSpan(t, 'llm', model ? { 'gen_ai.request.model': model } : undefined);
208
+ break;
209
+ }
210
+ case 'agentfootprint.stream.llm_end': {
211
+ const t = activeTurns.get(runId);
212
+ if (!t?.sampled)
213
+ break;
214
+ const span = popSpan(t, 'llm');
215
+ if (span)
216
+ endSpan(span);
217
+ break;
218
+ }
219
+ case 'agentfootprint.stream.tool_start': {
220
+ const t = activeTurns.get(runId);
221
+ if (!t?.sampled)
222
+ break;
223
+ const toolName = event.payload.toolName ?? 'tool';
224
+ pushSpan(t, `tool:${toolName}`, { 'tool.name': toolName });
225
+ break;
226
+ }
227
+ case 'agentfootprint.stream.tool_end': {
228
+ const t = activeTurns.get(runId);
229
+ if (!t?.sampled)
230
+ break;
231
+ const toolName = event.payload.toolName;
232
+ const errored = event.payload.error !== undefined;
233
+ const span = popSpan(t, toolName ? `tool:${toolName}` : undefined);
234
+ if (span)
235
+ endSpan(span, { error: errored });
236
+ break;
237
+ }
238
+ // Other events — annotate the topmost active span.
239
+ default: {
240
+ const t = activeTurns.get(runId);
241
+ const top = t?.stack[t.stack.length - 1]?.span;
242
+ if (!t?.sampled || !top)
243
+ break;
244
+ // Cost ticks are particularly valuable as attributes.
245
+ if (event.type === 'agentfootprint.cost.tick') {
246
+ const p = event.payload;
247
+ if (typeof p.cumulativeCostUsd === 'number') {
248
+ try {
249
+ top.setAttribute('cost.cumulative_usd', p.cumulativeCostUsd);
250
+ }
251
+ catch {
252
+ /* ignore */
253
+ }
254
+ }
255
+ }
256
+ break;
257
+ }
258
+ }
259
+ }
260
+ return {
261
+ name: 'otel',
262
+ capabilities: { events: true, traces: true },
263
+ exportEvent: handleEvent,
264
+ flush() {
265
+ // OTel SDKs handle their own flushing (the consumer-configured
266
+ // SpanProcessor's `forceFlush()`). We don't cross that boundary
267
+ // here — calling `provider.forceFlush()` is the consumer's
268
+ // responsibility on shutdown. Documented in the README.
269
+ },
270
+ stop() {
271
+ stopped = true;
272
+ // Defensive: end any spans the agent loop didn't close.
273
+ for (const [, t] of activeTurns) {
274
+ while (t.stack.length > 0) {
275
+ const span = popSpan(t);
276
+ if (span)
277
+ endSpan(span);
278
+ }
279
+ }
280
+ activeTurns.clear();
281
+ },
282
+ _onError(err, event) {
283
+ onErrorHook =
284
+ onErrorHook ??
285
+ ((e) => {
286
+ // eslint-disable-next-line no-console
287
+ console.error(`[otelObservability] error:`, e.message);
288
+ });
289
+ onErrorHook(err, event);
290
+ },
291
+ };
292
+ }
293
+ exports.otelObservability = otelObservability;
294
+ //# sourceMappingURL=otel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otel.js","sourceRoot":"","sources":["../../../src/adapters/observability/otel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,370 @@
1
+ "use strict";
2
+ /**
3
+ * xrayObservability — AWS X-Ray distributed-tracing adapter.
4
+ *
5
+ * Maps agentfootprint's event taxonomy onto AWS X-Ray segment trees:
6
+ *
7
+ * agent.turn_start ↦ root segment (one trace per turn)
8
+ * agent.turn_end ↦ close root segment + flush
9
+ * agent.iteration_start ↦ push subsegment under root
10
+ * agent.iteration_end ↦ close iteration subsegment
11
+ * stream.llm_start ↦ push leaf subsegment (model call)
12
+ * stream.llm_end ↦ close llm subsegment
13
+ * stream.tool_start ↦ push leaf subsegment (tool call)
14
+ * stream.tool_end ↦ close tool subsegment
15
+ *
16
+ * The result in the X-Ray Trace Map: a hierarchical timeline of every
17
+ * agent run — turn → iteration → llm-call/tool-call — queryable in
18
+ * X-Ray Insights, joinable with the rest of your AWS distributed
19
+ * trace via `AWSTraceHeader` propagation (consumer's responsibility
20
+ * to wire upstream/downstream IDs).
21
+ *
22
+ * Subpath: `agentfootprint/observability-providers`
23
+ * Peer dep: `@aws-sdk/client-xray` (OPTIONAL — installed only when
24
+ * this adapter is used).
25
+ *
26
+ * Sampling:
27
+ * By default every turn produces one trace. Pass `sampleRate: 0.1`
28
+ * to sample 10% of turns — sampling decisions are made at
29
+ * `turn_start` and persist for the whole turn (so partial traces
30
+ * never reach X-Ray).
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { xrayObservability } from 'agentfootprint/observability-providers';
35
+ * import { microtaskBatchDriver } from 'footprintjs/detach';
36
+ *
37
+ * agent.enable.observability({
38
+ * strategy: xrayObservability({
39
+ * region: 'us-east-1',
40
+ * serviceName: 'my-agent',
41
+ * sampleRate: 0.1, // 10% sampling
42
+ * }),
43
+ * detach: { driver: microtaskBatchDriver, mode: 'forget' },
44
+ * });
45
+ * ```
46
+ *
47
+ * @example Test injection
48
+ * ```ts
49
+ * xrayObservability({
50
+ * serviceName: 'test',
51
+ * _client: {
52
+ * putTraceSegments: async (input) => { capturedDocs.push(input); },
53
+ * },
54
+ * });
55
+ * ```
56
+ */
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.xrayObservability = void 0;
59
+ const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
60
+ // ─── Strategy factory ────────────────────────────────────────────────
61
+ function xrayObservability(opts) {
62
+ if (!opts.serviceName) {
63
+ throw new TypeError(`[xrayObservability] \`serviceName\` is required. ` +
64
+ `Pass an identifier visible in your X-Ray service map, e.g. 'my-agent-prod'.`);
65
+ }
66
+ const sampleRate = opts.sampleRate ?? 1;
67
+ const maxBatchSegments = opts.maxBatchSegments ?? 25;
68
+ const flushIntervalMs = opts.flushIntervalMs ?? 1000;
69
+ // Per-turn state. agentfootprint events arrive interleaved across
70
+ // multiple in-flight turns; we key the active stack by `runId`
71
+ // (every event payload carries it after enrichment).
72
+ const activeTurns = new Map();
73
+ // Outbound segment buffer (flat list of closed segments ready for
74
+ // PutTraceSegments). Drained by flush() / size-trigger / time-trigger.
75
+ const outbox = [];
76
+ let lastFlushPromise = Promise.resolve();
77
+ let timer;
78
+ let stopped = false;
79
+ let onErrorHook;
80
+ // Lazy SDK client.
81
+ let client = opts._client;
82
+ function ensureClient() {
83
+ if (client)
84
+ return client;
85
+ client = createXRayClient(opts.region);
86
+ return client;
87
+ }
88
+ function scheduleTimedFlush() {
89
+ if (timer || flushIntervalMs <= 0 || stopped)
90
+ return;
91
+ timer = setTimeout(() => {
92
+ timer = undefined;
93
+ void doFlush();
94
+ }, flushIntervalMs);
95
+ }
96
+ async function doFlush() {
97
+ if (outbox.length === 0 || stopped)
98
+ return;
99
+ const batch = outbox.splice(0, maxBatchSegments);
100
+ try {
101
+ await ensureClient().putTraceSegments({
102
+ TraceSegmentDocuments: batch.map((s) => JSON.stringify(s)),
103
+ });
104
+ }
105
+ catch (err) {
106
+ onErrorHook?.(err instanceof Error ? err : new Error(String(err)));
107
+ }
108
+ // If outbox grew during the put (size > maxBatchSegments emits
109
+ // arrived), chain another flush.
110
+ if (outbox.length > 0 && !stopped) {
111
+ lastFlushPromise = lastFlushPromise.then(doFlush, doFlush);
112
+ }
113
+ }
114
+ function pushSegment(turnState, name) {
115
+ const parent = turnState.stack[turnState.stack.length - 1];
116
+ const seg = {
117
+ name,
118
+ id: hexId(16),
119
+ trace_id: turnState.traceId,
120
+ ...(parent && { parent_id: parent.id }),
121
+ start_time: nowSeconds(),
122
+ in_progress: true,
123
+ };
124
+ turnState.stack.push(seg);
125
+ return seg;
126
+ }
127
+ function popSegment(turnState, expectedName) {
128
+ // Defensive: pop the topmost segment whose name matches (if
129
+ // provided). Out-of-order events would otherwise leave dangling
130
+ // segments. If no match, pop the topmost.
131
+ let idx = turnState.stack.length - 1;
132
+ if (expectedName) {
133
+ while (idx >= 0 && turnState.stack[idx].name !== expectedName)
134
+ idx--;
135
+ }
136
+ if (idx < 0)
137
+ return undefined;
138
+ const seg = turnState.stack.splice(idx, 1)[0];
139
+ seg.end_time = nowSeconds();
140
+ delete seg.in_progress;
141
+ return seg;
142
+ }
143
+ function closeSegment(turnState, expectedName, extra) {
144
+ const seg = popSegment(turnState, expectedName);
145
+ if (!seg)
146
+ return;
147
+ if (extra?.error)
148
+ seg.error = true;
149
+ if (extra?.annotations)
150
+ seg.annotations = { ...seg.annotations, ...extra.annotations };
151
+ if (extra?.metadata)
152
+ seg.metadata = { default: { ...(seg.metadata?.default ?? {}), ...extra.metadata } };
153
+ if (turnState.sampled) {
154
+ turnState.closed.push(seg);
155
+ // Once the root closes, the whole turn graduates to outbox.
156
+ if (turnState.stack.length === 0) {
157
+ outbox.push(...turnState.closed);
158
+ if (outbox.length >= maxBatchSegments) {
159
+ lastFlushPromise = lastFlushPromise.then(doFlush, doFlush);
160
+ }
161
+ else {
162
+ scheduleTimedFlush();
163
+ }
164
+ }
165
+ }
166
+ }
167
+ // ─── Event-to-segment dispatch ─────────────────────────────────────
168
+ function handleEvent(event) {
169
+ if (stopped)
170
+ return;
171
+ const runId = event.payload?.runId;
172
+ if (!runId)
173
+ return; // Events without a turn anchor — skip.
174
+ switch (event.type) {
175
+ case 'agentfootprint.agent.turn_start': {
176
+ const sampled = sampleRate >= 1 || Math.random() < sampleRate;
177
+ const turnState = {
178
+ traceId: makeTraceId(),
179
+ stack: [],
180
+ closed: [],
181
+ sampled,
182
+ };
183
+ activeTurns.set(runId, turnState);
184
+ if (sampled)
185
+ pushSegment(turnState, opts.serviceName);
186
+ break;
187
+ }
188
+ case 'agentfootprint.agent.turn_end': {
189
+ const t = activeTurns.get(runId);
190
+ if (!t)
191
+ break;
192
+ // Close everything still on the stack — defensive against
193
+ // missing `_end` events (e.g., pause/resume mid-turn).
194
+ while (t.stack.length > 0)
195
+ closeSegment(t, undefined);
196
+ activeTurns.delete(runId);
197
+ break;
198
+ }
199
+ case 'agentfootprint.agent.iteration_start': {
200
+ const t = activeTurns.get(runId);
201
+ if (t?.sampled)
202
+ pushSegment(t, `iteration:${event.payload.iteration ?? '?'}`);
203
+ break;
204
+ }
205
+ case 'agentfootprint.agent.iteration_end': {
206
+ const t = activeTurns.get(runId);
207
+ if (t?.sampled)
208
+ closeSegment(t, undefined);
209
+ break;
210
+ }
211
+ case 'agentfootprint.stream.llm_start': {
212
+ const t = activeTurns.get(runId);
213
+ if (!t?.sampled)
214
+ break;
215
+ const seg = pushSegment(t, 'llm');
216
+ const model = event.payload.model;
217
+ if (model)
218
+ seg.annotations = { model };
219
+ break;
220
+ }
221
+ case 'agentfootprint.stream.llm_end': {
222
+ const t = activeTurns.get(runId);
223
+ if (!t?.sampled)
224
+ break;
225
+ closeSegment(t, 'llm', {
226
+ metadata: { event: event.payload },
227
+ });
228
+ break;
229
+ }
230
+ case 'agentfootprint.stream.tool_start': {
231
+ const t = activeTurns.get(runId);
232
+ if (!t?.sampled)
233
+ break;
234
+ const toolName = event.payload.toolName ?? 'tool';
235
+ const seg = pushSegment(t, `tool:${toolName}`);
236
+ seg.annotations = { toolName };
237
+ break;
238
+ }
239
+ case 'agentfootprint.stream.tool_end': {
240
+ const t = activeTurns.get(runId);
241
+ if (!t?.sampled)
242
+ break;
243
+ const toolName = event.payload.toolName;
244
+ closeSegment(t, toolName ? `tool:${toolName}` : undefined, {
245
+ error: event.payload.error !== undefined,
246
+ });
247
+ break;
248
+ }
249
+ // Other events become annotations on the topmost active segment
250
+ // (cheaper than spawning a subsegment per event).
251
+ default: {
252
+ const t = activeTurns.get(runId);
253
+ const top = t?.stack[t.stack.length - 1];
254
+ if (!t?.sampled || !top)
255
+ break;
256
+ // Annotate cost ticks specially so they're queryable in
257
+ // X-Ray Insights.
258
+ if (event.type === 'agentfootprint.cost.tick') {
259
+ const p = event.payload;
260
+ if (typeof p.cumulativeCostUsd === 'number') {
261
+ top.annotations = { ...top.annotations, cumulativeCostUsd: p.cumulativeCostUsd };
262
+ }
263
+ }
264
+ break;
265
+ }
266
+ }
267
+ }
268
+ return {
269
+ name: 'xray',
270
+ capabilities: { events: true, traces: true },
271
+ exportEvent: handleEvent,
272
+ async flush() {
273
+ // Force-close any in-flight turn segments so partial traces
274
+ // make it into X-Ray on shutdown.
275
+ for (const [, t] of activeTurns) {
276
+ if (!t.sampled)
277
+ continue;
278
+ while (t.stack.length > 0)
279
+ closeSegment(t, undefined);
280
+ }
281
+ while (outbox.length > 0) {
282
+ const before = lastFlushPromise;
283
+ await before;
284
+ if (outbox.length > 0) {
285
+ lastFlushPromise = doFlush();
286
+ }
287
+ if (lastFlushPromise === before && outbox.length === 0)
288
+ break;
289
+ }
290
+ },
291
+ stop() {
292
+ stopped = true;
293
+ if (timer) {
294
+ clearTimeout(timer);
295
+ timer = undefined;
296
+ }
297
+ },
298
+ _onError(err, event) {
299
+ onErrorHook =
300
+ onErrorHook ??
301
+ ((e) => {
302
+ // eslint-disable-next-line no-console
303
+ console.error(`[xrayObservability] flush failed:`, e.message);
304
+ });
305
+ onErrorHook(err, event);
306
+ },
307
+ };
308
+ }
309
+ exports.xrayObservability = xrayObservability;
310
+ // ─── ID + time helpers ───────────────────────────────────────────────
311
+ /**
312
+ * Generate an X-Ray trace ID. Format:
313
+ * `1-{8-hex-of-unix-timestamp}-{24-hex-random}`
314
+ * (Note X-Ray's docs say "12 hex" for the random part; the actual
315
+ * spec is 24 hex / 96-bit. AWS examples use 24.)
316
+ */
317
+ function makeTraceId() {
318
+ const seconds = Math.floor(Date.now() / 1000);
319
+ return `1-${seconds.toString(16).padStart(8, '0')}-${hexId(24)}`;
320
+ }
321
+ /** Generate a hex string of `len` chars, cryptographically-strong
322
+ * where available, falling back to Math.random for environments
323
+ * without `crypto.getRandomValues` (older runtimes). */
324
+ function hexId(len) {
325
+ const bytes = Math.ceil(len / 2);
326
+ // Try the Web Crypto / Node Crypto API first.
327
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
328
+ const cryptoApi = globalThis.crypto;
329
+ if (cryptoApi?.getRandomValues) {
330
+ const buf = new Uint8Array(bytes);
331
+ cryptoApi.getRandomValues(buf);
332
+ return Array.from(buf, (b) => b.toString(16).padStart(2, '0'))
333
+ .join('')
334
+ .slice(0, len);
335
+ }
336
+ // Fallback (deterministic-quality, NOT for security-critical IDs —
337
+ // X-Ray IDs aren't security boundaries, just trace correlation).
338
+ let s = '';
339
+ while (s.length < len)
340
+ s += Math.random().toString(16).slice(2);
341
+ return s.slice(0, len);
342
+ }
343
+ /** X-Ray timestamps are unix seconds with fractional precision. */
344
+ function nowSeconds() {
345
+ return Date.now() / 1000;
346
+ }
347
+ // ─── SDK construction (lazy) ─────────────────────────────────────────
348
+ function createXRayClient(region) {
349
+ let mod;
350
+ try {
351
+ mod = (0, lazyRequire_js_1.lazyRequire)('@aws-sdk/client-xray');
352
+ }
353
+ catch {
354
+ throw new Error('xrayObservability requires the `@aws-sdk/client-xray` peer dependency.\n' +
355
+ ' Install: npm install @aws-sdk/client-xray\n' +
356
+ ' Or pass `_client` for test injection.');
357
+ }
358
+ if (!mod.XRayClient || !mod.PutTraceSegmentsCommand) {
359
+ throw new Error('xrayObservability: `@aws-sdk/client-xray` is installed but `XRayClient` / ' +
360
+ '`PutTraceSegmentsCommand` was not found. Update the SDK.');
361
+ }
362
+ const sdkClient = new mod.XRayClient({ ...(region && { region }) });
363
+ return {
364
+ async putTraceSegments(input) {
365
+ const cmd = new mod.PutTraceSegmentsCommand(input);
366
+ await sdkClient.send(cmd);
367
+ },
368
+ };
369
+ }
370
+ //# sourceMappingURL=xray.js.map