autotel 2.26.3 → 3.0.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/README.md +29 -19
- package/dist/attributes.d.cts +3 -3
- package/dist/attributes.d.ts +3 -3
- package/dist/business-baggage.d.cts +1 -1
- package/dist/business-baggage.d.ts +1 -1
- package/dist/{chunk-YN7USLHW.js → chunk-3QMFLJHJ.js} +11 -10
- package/dist/chunk-3QMFLJHJ.js.map +1 -0
- package/dist/{chunk-BJ2XPN77.js → chunk-4DAG3RFS.js} +4 -4
- package/dist/{chunk-BJ2XPN77.js.map → chunk-4DAG3RFS.js.map} +1 -1
- package/dist/chunk-4P6ZOARG.cjs +33 -0
- package/dist/chunk-4P6ZOARG.cjs.map +1 -0
- package/dist/{chunk-U54FTVFH.js → chunk-7HNQYHK4.js} +3 -3
- package/dist/{chunk-U54FTVFH.js.map → chunk-7HNQYHK4.js.map} +1 -1
- package/dist/chunk-CJ4PD2TZ.cjs +1207 -0
- package/dist/chunk-CJ4PD2TZ.cjs.map +1 -0
- package/dist/{chunk-HPUGKUMZ.js → chunk-DAAJLUTO.js} +13 -640
- package/dist/chunk-DAAJLUTO.js.map +1 -0
- package/dist/{chunk-B3ZHLLMP.js → chunk-DSMSIVTG.js} +2 -2
- package/dist/chunk-DSMSIVTG.js.map +1 -0
- package/dist/{chunk-6YGUN7IY.cjs → chunk-DWOBIBLY.cjs} +18 -17
- package/dist/chunk-DWOBIBLY.cjs.map +1 -0
- package/dist/{chunk-QC5MNKVF.js → chunk-IUDXKLS4.js} +13 -12
- package/dist/chunk-IUDXKLS4.js.map +1 -0
- package/dist/{chunk-OBWXM4NN.cjs → chunk-KHGA4OST.cjs} +15 -14
- package/dist/chunk-KHGA4OST.cjs.map +1 -0
- package/dist/chunk-KIL5CUN6.js +31 -0
- package/dist/chunk-KIL5CUN6.js.map +1 -0
- package/dist/{chunk-YEVCD6DR.cjs → chunk-L7JDUDJD.cjs} +7 -7
- package/dist/{chunk-YEVCD6DR.cjs.map → chunk-L7JDUDJD.cjs.map} +1 -1
- package/dist/{chunk-UTZR7P7E.cjs → chunk-MOK3E54E.cjs} +29 -659
- package/dist/chunk-MOK3E54E.cjs.map +1 -0
- package/dist/{chunk-GML3FBOT.cjs → chunk-NCSMD3TK.cjs} +2 -2
- package/dist/chunk-NCSMD3TK.cjs.map +1 -0
- package/dist/chunk-QG3U5ONP.js +1183 -0
- package/dist/chunk-QG3U5ONP.js.map +1 -0
- package/dist/chunk-SEO6NAQT.js +14 -0
- package/dist/chunk-SEO6NAQT.js.map +1 -0
- package/dist/chunk-VQTCQKHQ.cjs +17 -0
- package/dist/chunk-VQTCQKHQ.cjs.map +1 -0
- package/dist/{chunk-WZOKY3PW.cjs → chunk-ZSABTI3C.cjs} +8 -8
- package/dist/{chunk-WZOKY3PW.cjs.map → chunk-ZSABTI3C.cjs.map} +1 -1
- package/dist/correlation-id.cjs +22 -10
- package/dist/correlation-id.js +14 -2
- package/dist/decorators.cjs +5 -6
- package/dist/decorators.cjs.map +1 -1
- package/dist/decorators.d.cts +1 -1
- package/dist/decorators.d.ts +1 -1
- package/dist/decorators.js +4 -5
- package/dist/decorators.js.map +1 -1
- package/dist/event.cjs +6 -7
- package/dist/event.js +3 -4
- package/dist/functional.cjs +11 -12
- package/dist/functional.d.cts +1 -1
- package/dist/functional.d.ts +1 -1
- package/dist/functional.js +4 -5
- package/dist/http.cjs +13 -2
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +12 -1
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +134 -243
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -8
- package/dist/index.d.ts +23 -8
- package/dist/index.js +52 -176
- package/dist/index.js.map +1 -1
- package/dist/messaging-adapters.d.cts +1 -1
- package/dist/messaging-adapters.d.ts +1 -1
- package/dist/messaging-testing.d.cts +1 -1
- package/dist/messaging-testing.d.ts +1 -1
- package/dist/messaging.cjs +9 -9
- package/dist/messaging.d.cts +1 -1
- package/dist/messaging.d.ts +1 -1
- package/dist/messaging.js +6 -6
- package/dist/semantic-helpers.cjs +9 -10
- package/dist/semantic-helpers.d.cts +1 -1
- package/dist/semantic-helpers.d.ts +1 -1
- package/dist/semantic-helpers.js +5 -6
- package/dist/{trace-context-t5X1AP-e.d.ts → trace-context-DbGKd1Rn.d.cts} +18 -5
- package/dist/{trace-context-t5X1AP-e.d.cts → trace-context-DbGKd1Rn.d.ts} +18 -5
- package/dist/trace-helpers.cjs +13 -13
- package/dist/trace-helpers.d.cts +2 -2
- package/dist/trace-helpers.d.ts +2 -2
- package/dist/trace-helpers.js +1 -1
- package/dist/{utils-CbUkl8r1.d.cts → utils-BahBCFtJ.d.cts} +1 -1
- package/dist/{utils-Buel3cj0.d.ts → utils-CLKwaUlG.d.ts} +1 -1
- package/dist/webhook.cjs +19 -10
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +18 -9
- package/dist/webhook.js.map +1 -1
- package/dist/workflow-distributed.cjs +23 -19
- package/dist/workflow-distributed.cjs.map +1 -1
- package/dist/workflow-distributed.d.cts +1 -1
- package/dist/workflow-distributed.d.ts +1 -1
- package/dist/workflow-distributed.js +21 -17
- package/dist/workflow-distributed.js.map +1 -1
- package/dist/workflow.cjs +10 -10
- package/dist/workflow.d.cts +1 -1
- package/dist/workflow.d.ts +1 -1
- package/dist/workflow.js +6 -6
- package/package.json +1 -1
- package/skills/autotel-core/SKILL.md +2 -0
- package/skills/autotel-events/SKILL.md +2 -0
- package/skills/autotel-frameworks/SKILL.md +2 -0
- package/skills/autotel-instrumentation/SKILL.md +2 -0
- package/skills/autotel-request-logging/SKILL.md +2 -0
- package/skills/autotel-structured-errors/SKILL.md +2 -0
- package/src/correlated-events.test.ts +151 -0
- package/src/correlated-events.ts +47 -0
- package/src/functional.ts +2 -0
- package/src/gen-ai-events.ts +14 -5
- package/src/index.ts +20 -4
- package/src/messaging.ts +10 -9
- package/src/request-logger.ts +4 -3
- package/src/structured-error.test.ts +83 -1
- package/src/structured-error.ts +9 -2
- package/src/trace-context.ts +39 -11
- package/src/trace-helpers.ts +2 -2
- package/src/trace-hybrid.test.ts +42 -0
- package/src/trace-hybrid.ts +37 -0
- package/src/webhook.ts +16 -7
- package/src/workflow-distributed.ts +18 -13
- package/src/workflow.ts +7 -6
- package/dist/chunk-6YGUN7IY.cjs.map +0 -1
- package/dist/chunk-B3ZHLLMP.js.map +0 -1
- package/dist/chunk-BBBWDIYQ.js +0 -211
- package/dist/chunk-BBBWDIYQ.js.map +0 -1
- package/dist/chunk-D5LMF53P.cjs +0 -150
- package/dist/chunk-D5LMF53P.cjs.map +0 -1
- package/dist/chunk-GML3FBOT.cjs.map +0 -1
- package/dist/chunk-HPUGKUMZ.js.map +0 -1
- package/dist/chunk-HZ3FYBJG.cjs +0 -217
- package/dist/chunk-HZ3FYBJG.cjs.map +0 -1
- package/dist/chunk-JSNUWSBH.cjs +0 -62
- package/dist/chunk-JSNUWSBH.cjs.map +0 -1
- package/dist/chunk-OBWXM4NN.cjs.map +0 -1
- package/dist/chunk-QC5MNKVF.js.map +0 -1
- package/dist/chunk-S4OFEXLA.js +0 -53
- package/dist/chunk-S4OFEXLA.js.map +0 -1
- package/dist/chunk-UTZR7P7E.cjs.map +0 -1
- package/dist/chunk-WD4RP6IV.js +0 -146
- package/dist/chunk-WD4RP6IV.js.map +0 -1
- package/dist/chunk-YN7USLHW.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -81,7 +81,6 @@ export type {
|
|
|
81
81
|
InstrumentOptions,
|
|
82
82
|
} from './functional';
|
|
83
83
|
export {
|
|
84
|
-
trace,
|
|
85
84
|
instrument,
|
|
86
85
|
withTracing,
|
|
87
86
|
span,
|
|
@@ -89,6 +88,9 @@ export {
|
|
|
89
88
|
withBaggage,
|
|
90
89
|
ctx,
|
|
91
90
|
} from './functional';
|
|
91
|
+
// `trace` is the hybrid: callable like autotel's `trace(fn)` AND carries the
|
|
92
|
+
// full `@opentelemetry/api` TraceAPI surface (getActiveSpan, getTracer, etc).
|
|
93
|
+
export { trace } from './trace-hybrid';
|
|
92
94
|
|
|
93
95
|
// Operation context (for advanced usage)
|
|
94
96
|
export type { OperationContext } from './operation-context';
|
|
@@ -333,19 +335,33 @@ export {
|
|
|
333
335
|
type TLSAttrs,
|
|
334
336
|
} from './attributes';
|
|
335
337
|
|
|
336
|
-
// Re-export common OpenTelemetry types and utilities
|
|
337
|
-
//
|
|
338
|
+
// Re-export common OpenTelemetry types and utilities so plugins, apps, and
|
|
339
|
+
// existing OTel code can `import { ... } from 'autotel'` without also taking
|
|
340
|
+
// a separate `@opentelemetry/api` dependency.
|
|
338
341
|
export type {
|
|
339
342
|
Span,
|
|
340
343
|
SpanContext,
|
|
344
|
+
SpanAttributes,
|
|
341
345
|
Tracer,
|
|
346
|
+
TracerProvider,
|
|
342
347
|
Context,
|
|
348
|
+
Attributes,
|
|
349
|
+
AttributeValue,
|
|
350
|
+
Link,
|
|
343
351
|
Link as SpanLink,
|
|
352
|
+
TimeInput,
|
|
353
|
+
HrTime,
|
|
354
|
+
Baggage,
|
|
355
|
+
BaggageEntry,
|
|
356
|
+
Exception,
|
|
357
|
+
TraceFlags,
|
|
358
|
+
TraceState,
|
|
344
359
|
TextMapSetter,
|
|
345
360
|
TextMapGetter,
|
|
346
361
|
} from '@opentelemetry/api';
|
|
347
362
|
export { SpanKind, ROOT_CONTEXT } from '@opentelemetry/api';
|
|
348
|
-
// Note: trace
|
|
363
|
+
// Note: trace, context, propagation, SpanStatusCode already exported above
|
|
364
|
+
// (`trace` is the hybrid; `otelTrace` is the pure TraceAPI singleton).
|
|
349
365
|
|
|
350
366
|
// Export typed baggage helper
|
|
351
367
|
export { defineBaggageSchema } from './trace-context';
|
package/src/messaging.ts
CHANGED
|
@@ -50,6 +50,7 @@ import type {
|
|
|
50
50
|
} from '@opentelemetry/api';
|
|
51
51
|
import { trace } from './functional';
|
|
52
52
|
import type { TraceContext } from './trace-context';
|
|
53
|
+
import { emitCorrelatedEvent } from './correlated-events';
|
|
53
54
|
import { createLinkFromHeaders, extractLinksFromBatch } from './sampling';
|
|
54
55
|
|
|
55
56
|
// ============================================================================
|
|
@@ -1406,7 +1407,7 @@ function extendContextForConsumer(
|
|
|
1406
1407
|
producerLink.context.spanId;
|
|
1407
1408
|
}
|
|
1408
1409
|
|
|
1409
|
-
baseCtx
|
|
1410
|
+
emitCorrelatedEvent(baseCtx, 'dlq_routed', eventAttrs);
|
|
1410
1411
|
|
|
1411
1412
|
// Call user's onDLQ callback if provided
|
|
1412
1413
|
if (config.onDLQ) {
|
|
@@ -1447,7 +1448,7 @@ function extendContextForConsumer(
|
|
|
1447
1448
|
}),
|
|
1448
1449
|
};
|
|
1449
1450
|
|
|
1450
|
-
baseCtx
|
|
1451
|
+
emitCorrelatedEvent(baseCtx, 'dlq_replay', eventAttrs);
|
|
1451
1452
|
},
|
|
1452
1453
|
|
|
1453
1454
|
recordRetry(attemptNumber: number, maxAttempts?: number): void {
|
|
@@ -1455,7 +1456,7 @@ function extendContextForConsumer(
|
|
|
1455
1456
|
if (maxAttempts !== undefined) {
|
|
1456
1457
|
baseCtx.setAttribute('messaging.retry.max_attempts', maxAttempts);
|
|
1457
1458
|
}
|
|
1458
|
-
baseCtx
|
|
1459
|
+
emitCorrelatedEvent(baseCtx, 'retry_attempt', {
|
|
1459
1460
|
'messaging.retry.count': attemptNumber,
|
|
1460
1461
|
...(maxAttempts !== undefined && {
|
|
1461
1462
|
'messaging.retry.max_attempts': maxAttempts,
|
|
@@ -1585,7 +1586,7 @@ function extendContextForConsumer(
|
|
|
1585
1586
|
event.partitions.map((p) => `${p.topic}:${p.partition}`).join(',');
|
|
1586
1587
|
}
|
|
1587
1588
|
|
|
1588
|
-
baseCtx
|
|
1589
|
+
emitCorrelatedEvent(baseCtx, `consumer_group_${event.type}`, eventAttrs);
|
|
1589
1590
|
|
|
1590
1591
|
// Call user's onRebalance callback if provided
|
|
1591
1592
|
if (config.consumerGroupTracking?.onRebalance) {
|
|
@@ -1627,7 +1628,7 @@ function extendContextForConsumer(
|
|
|
1627
1628
|
);
|
|
1628
1629
|
}
|
|
1629
1630
|
|
|
1630
|
-
baseCtx
|
|
1631
|
+
emitCorrelatedEvent(baseCtx, 'consumer_group_heartbeat', {
|
|
1631
1632
|
'messaging.consumer_group.heartbeat.healthy': healthy,
|
|
1632
1633
|
'messaging.consumer_group.heartbeat.timestamp':
|
|
1633
1634
|
groupState.lastHeartbeat,
|
|
@@ -1644,7 +1645,7 @@ function extendContextForConsumer(
|
|
|
1644
1645
|
baseCtx.setAttribute(`${prefix}.end_offset`, lag.endOffset);
|
|
1645
1646
|
baseCtx.setAttribute(`${prefix}.lag`, lag.lag);
|
|
1646
1647
|
|
|
1647
|
-
baseCtx
|
|
1648
|
+
emitCorrelatedEvent(baseCtx, 'partition_lag_recorded', {
|
|
1648
1649
|
'messaging.consumer_group.lag.topic': lag.topic,
|
|
1649
1650
|
'messaging.consumer_group.lag.partition': lag.partition,
|
|
1650
1651
|
'messaging.consumer_group.lag.current_offset': lag.currentOffset,
|
|
@@ -1899,7 +1900,7 @@ async function extractLagMetrics(
|
|
|
1899
1900
|
ctx.setAttribute('messaging.kafka.consumer_lag', lag);
|
|
1900
1901
|
|
|
1901
1902
|
// Add lag event
|
|
1902
|
-
ctx
|
|
1903
|
+
emitCorrelatedEvent(ctx, 'consumer_lag_measured', {
|
|
1903
1904
|
'messaging.kafka.consumer_lag': lag,
|
|
1904
1905
|
'messaging.kafka.message.offset': currentOffset,
|
|
1905
1906
|
'messaging.kafka.high_watermark': endOffset,
|
|
@@ -2119,7 +2120,7 @@ function extractAndProcessOrdering(
|
|
|
2119
2120
|
}
|
|
2120
2121
|
|
|
2121
2122
|
// Add event for each out-of-order message
|
|
2122
|
-
ctx
|
|
2123
|
+
emitCorrelatedEvent(ctx, 'message_out_of_order', {
|
|
2123
2124
|
'messaging.ordering.batch_index': i,
|
|
2124
2125
|
'messaging.ordering.current_sequence': msgSequence,
|
|
2125
2126
|
'messaging.ordering.expected_sequence': expectedSequence,
|
|
@@ -2155,7 +2156,7 @@ function extractAndProcessOrdering(
|
|
|
2155
2156
|
duplicateCount++;
|
|
2156
2157
|
|
|
2157
2158
|
// Add event for each duplicate
|
|
2158
|
-
ctx
|
|
2159
|
+
emitCorrelatedEvent(ctx, 'message_duplicate', {
|
|
2159
2160
|
'messaging.ordering.batch_index': i,
|
|
2160
2161
|
'messaging.message.id': msgId,
|
|
2161
2162
|
});
|
package/src/request-logger.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { TraceContext } from './trace-context';
|
|
|
4
4
|
import { createTraceContext } from './trace-context';
|
|
5
5
|
import { recordStructuredError } from './structured-error';
|
|
6
6
|
import { flattenToAttributes } from './flatten-attributes';
|
|
7
|
+
import { emitCorrelatedEvent } from './correlated-events';
|
|
7
8
|
|
|
8
9
|
const POST_EMIT_FORK_HINT =
|
|
9
10
|
"For intentional background work tied to this request, use log.fork('label', fn) when available.";
|
|
@@ -101,9 +102,9 @@ export function getRequestLogger(
|
|
|
101
102
|
fields?: Record<string, unknown>,
|
|
102
103
|
) => {
|
|
103
104
|
const attrs = fields ? flattenToAttributes(fields) : undefined;
|
|
104
|
-
activeContext
|
|
105
|
+
emitCorrelatedEvent(activeContext, `log.${level}`, {
|
|
105
106
|
message,
|
|
106
|
-
...attrs,
|
|
107
|
+
...(attrs ?? {}),
|
|
107
108
|
});
|
|
108
109
|
};
|
|
109
110
|
|
|
@@ -191,7 +192,7 @@ export function getRequestLogger(
|
|
|
191
192
|
context: mergedContext,
|
|
192
193
|
};
|
|
193
194
|
|
|
194
|
-
activeContext
|
|
195
|
+
emitCorrelatedEvent(activeContext, 'log.emit.manual', {
|
|
195
196
|
...flattened,
|
|
196
197
|
});
|
|
197
198
|
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { Span, SpanContext } from '@opentelemetry/api';
|
|
2
3
|
import {
|
|
3
4
|
createStructuredError,
|
|
4
5
|
getStructuredErrorAttributes,
|
|
5
6
|
recordStructuredError,
|
|
6
7
|
} from './structured-error';
|
|
7
|
-
import type
|
|
8
|
+
import { createTraceContext, type TraceContext } from './trace-context';
|
|
9
|
+
|
|
10
|
+
function createFakeSpan(): {
|
|
11
|
+
span: Span;
|
|
12
|
+
recordException: ReturnType<typeof vi.fn>;
|
|
13
|
+
setStatus: ReturnType<typeof vi.fn>;
|
|
14
|
+
setAttributes: ReturnType<typeof vi.fn>;
|
|
15
|
+
} {
|
|
16
|
+
const recordException = vi.fn();
|
|
17
|
+
const setStatus = vi.fn();
|
|
18
|
+
const setAttributes = vi.fn();
|
|
19
|
+
const spanContext: SpanContext = {
|
|
20
|
+
traceId: '0123456789abcdef0123456789abcdef',
|
|
21
|
+
spanId: '0123456789abcdef',
|
|
22
|
+
traceFlags: 1,
|
|
23
|
+
};
|
|
24
|
+
const span = {
|
|
25
|
+
spanContext: () => spanContext,
|
|
26
|
+
setAttribute: vi.fn(),
|
|
27
|
+
setAttributes,
|
|
28
|
+
setStatus,
|
|
29
|
+
recordException,
|
|
30
|
+
addEvent: vi.fn(),
|
|
31
|
+
addLink: vi.fn(),
|
|
32
|
+
addLinks: vi.fn(),
|
|
33
|
+
updateName: vi.fn(),
|
|
34
|
+
isRecording: () => true,
|
|
35
|
+
end: vi.fn(),
|
|
36
|
+
} as unknown as Span;
|
|
37
|
+
return { span, recordException, setStatus, setAttributes };
|
|
38
|
+
}
|
|
8
39
|
|
|
9
40
|
describe('structured-error helpers', () => {
|
|
10
41
|
it('creates an error with structured diagnostic fields', () => {
|
|
@@ -104,3 +135,54 @@ describe('structured-error helpers', () => {
|
|
|
104
135
|
);
|
|
105
136
|
});
|
|
106
137
|
});
|
|
138
|
+
|
|
139
|
+
describe('ctx.recordError', () => {
|
|
140
|
+
it('records a structured error onto the underlying span', () => {
|
|
141
|
+
const { span, recordException, setStatus, setAttributes } =
|
|
142
|
+
createFakeSpan();
|
|
143
|
+
const ctx = createTraceContext(span);
|
|
144
|
+
const err = createStructuredError({
|
|
145
|
+
message: 'Order failed',
|
|
146
|
+
why: 'Inventory unavailable',
|
|
147
|
+
fix: 'Retry after restock',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
ctx.recordError(err);
|
|
151
|
+
|
|
152
|
+
expect(recordException).toHaveBeenCalledWith(err);
|
|
153
|
+
expect(setStatus).toHaveBeenCalledWith({ code: 2, message: 'Order failed' });
|
|
154
|
+
expect(setAttributes).toHaveBeenCalledWith(
|
|
155
|
+
expect.objectContaining({
|
|
156
|
+
'error.message': 'Order failed',
|
|
157
|
+
'error.why': 'Inventory unavailable',
|
|
158
|
+
'error.fix': 'Retry after restock',
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('coerces non-Error values to Error so it is safe in catch blocks', () => {
|
|
164
|
+
const { span, recordException, setStatus, setAttributes } =
|
|
165
|
+
createFakeSpan();
|
|
166
|
+
const ctx = createTraceContext(span);
|
|
167
|
+
|
|
168
|
+
ctx.recordError('boom');
|
|
169
|
+
|
|
170
|
+
expect(recordException).toHaveBeenCalledTimes(1);
|
|
171
|
+
const recorded = recordException.mock.calls[0][0];
|
|
172
|
+
expect(recorded).toBeInstanceOf(Error);
|
|
173
|
+
expect(recorded.message).toBe('boom');
|
|
174
|
+
expect(setStatus).toHaveBeenCalledWith({ code: 2, message: 'boom' });
|
|
175
|
+
expect(setAttributes).toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('ctx.track', () => {
|
|
180
|
+
it('exposes track on the trace context as the ergonomic replacement for ctx.addEvent', () => {
|
|
181
|
+
const { span } = createFakeSpan();
|
|
182
|
+
const ctx = createTraceContext(span);
|
|
183
|
+
|
|
184
|
+
expect(typeof ctx.track).toBe('function');
|
|
185
|
+
// Smoke test — should not throw without init() (track is a no-op when no queue is configured)
|
|
186
|
+
expect(() => ctx.track('test.event', { foo: 'bar' })).not.toThrow();
|
|
187
|
+
});
|
|
188
|
+
});
|
package/src/structured-error.ts
CHANGED
|
@@ -138,10 +138,17 @@ export function getStructuredErrorAttributes(
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
export function recordStructuredError(
|
|
141
|
-
ctx: Pick<TraceContext, '
|
|
141
|
+
ctx: Pick<TraceContext, 'setAttributes' | 'setStatus'>,
|
|
142
142
|
error: Error,
|
|
143
143
|
): void {
|
|
144
|
-
|
|
144
|
+
const maybeRecordException = (
|
|
145
|
+
ctx as unknown as {
|
|
146
|
+
recordException?: (e: Error) => void;
|
|
147
|
+
}
|
|
148
|
+
).recordException;
|
|
149
|
+
if (typeof maybeRecordException === 'function') {
|
|
150
|
+
maybeRecordException(error);
|
|
151
|
+
}
|
|
145
152
|
ctx.setStatus({
|
|
146
153
|
code: SpanStatusCode.ERROR,
|
|
147
154
|
message: error.message,
|
package/src/trace-context.ts
CHANGED
|
@@ -8,10 +8,11 @@ import type {
|
|
|
8
8
|
BaggageEntry,
|
|
9
9
|
Context,
|
|
10
10
|
Link,
|
|
11
|
-
TimeInput,
|
|
12
11
|
} from '@opentelemetry/api';
|
|
13
12
|
import { context, propagation } from '@opentelemetry/api';
|
|
14
13
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
14
|
+
import { recordStructuredError } from './structured-error';
|
|
15
|
+
import { track } from './track';
|
|
15
16
|
|
|
16
17
|
type AsyncLocalBox<T> = {
|
|
17
18
|
value: T;
|
|
@@ -132,14 +133,6 @@ export interface SpanMethods {
|
|
|
132
133
|
setAttributes(attrs: Record<string, AttributeValue>): void;
|
|
133
134
|
/** Set the status of the span */
|
|
134
135
|
setStatus(status: { code: SpanStatusCode; message?: string }): void;
|
|
135
|
-
/** Record an exception on the span */
|
|
136
|
-
recordException(exception: Error, time?: TimeInput): void;
|
|
137
|
-
/** Add an event to the span (for logging milestones/checkpoints) */
|
|
138
|
-
addEvent(
|
|
139
|
-
name: string,
|
|
140
|
-
attributesOrStartTime?: Record<string, AttributeValue> | TimeInput,
|
|
141
|
-
startTime?: TimeInput,
|
|
142
|
-
): void;
|
|
143
136
|
/** Add a link to another span */
|
|
144
137
|
addLink(link: Link): void;
|
|
145
138
|
/** Add multiple links to other spans */
|
|
@@ -148,6 +141,26 @@ export interface SpanMethods {
|
|
|
148
141
|
updateName(name: string): void;
|
|
149
142
|
/** Check if the span is recording */
|
|
150
143
|
isRecording(): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Record an error on the span: sets ERROR status, structured `error.*`
|
|
146
|
+
* attributes (including `why`/`fix`/`link` from `createStructuredError`),
|
|
147
|
+
* and during the OTel Span Event API back-compat window also records the
|
|
148
|
+
* exception via the legacy span event API.
|
|
149
|
+
*
|
|
150
|
+
* Replaces the deprecated `recordException` (OTEP 4430). Accepts `unknown`
|
|
151
|
+
* so it can be called directly with the value caught from a `catch` block.
|
|
152
|
+
*/
|
|
153
|
+
recordError(error: unknown): void;
|
|
154
|
+
/**
|
|
155
|
+
* Emit a tracked event correlated to this span. Equivalent to the standalone
|
|
156
|
+
* `track(event, data)` but reads naturally on `ctx`. Replaces the deprecated
|
|
157
|
+
* `ctx.addEvent` (OTEP 4430) — events become correlated logs rather than
|
|
158
|
+
* span events.
|
|
159
|
+
*/
|
|
160
|
+
track<Events extends Record<string, unknown> = Record<string, unknown>>(
|
|
161
|
+
event: keyof Events & string,
|
|
162
|
+
data?: Events[keyof Events & string],
|
|
163
|
+
): void;
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
/**
|
|
@@ -411,7 +424,13 @@ export function createTraceContext<
|
|
|
411
424
|
: never,
|
|
412
425
|
};
|
|
413
426
|
|
|
414
|
-
|
|
427
|
+
// `recordException` and `addEvent` are intentionally bound at runtime but
|
|
428
|
+
// omitted from the `SpanMethods` type. They exist solely so existing call
|
|
429
|
+
// sites keep working through the OTel Span Event API deprecation window
|
|
430
|
+
// (see MIGRATION.md). New code MUST go through `recordStructuredError`,
|
|
431
|
+
// `emitCorrelatedEvent`, or the request logger. The cast below is what hides
|
|
432
|
+
// these compatibility-only fields from the public type.
|
|
433
|
+
const traceCtx = {
|
|
415
434
|
traceId: spanContext.traceId,
|
|
416
435
|
spanId: spanContext.spanId,
|
|
417
436
|
correlationId: spanContext.traceId.slice(0, 16),
|
|
@@ -424,8 +443,17 @@ export function createTraceContext<
|
|
|
424
443
|
addLinks: span.addLinks.bind(span),
|
|
425
444
|
updateName: span.updateName.bind(span),
|
|
426
445
|
isRecording: span.isRecording.bind(span),
|
|
446
|
+
recordError: (error: unknown) => {
|
|
447
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
448
|
+
recordStructuredError(traceCtx, err);
|
|
449
|
+
},
|
|
450
|
+
track: (event: string, data?: Record<string, unknown>) => {
|
|
451
|
+
track(event, data);
|
|
452
|
+
},
|
|
427
453
|
...baggageHelpers,
|
|
428
|
-
}
|
|
454
|
+
} as unknown as TraceContext<TBaggage>;
|
|
455
|
+
|
|
456
|
+
return traceCtx;
|
|
429
457
|
}
|
|
430
458
|
|
|
431
459
|
/**
|
package/src/trace-helpers.ts
CHANGED
|
@@ -241,7 +241,7 @@ export function getTracer(name: string, version?: string): Tracer {
|
|
|
241
241
|
* Get the currently active span
|
|
242
242
|
*
|
|
243
243
|
* Returns undefined if no span is currently active.
|
|
244
|
-
* Useful for adding attributes
|
|
244
|
+
* Useful for adding attributes to the current span.
|
|
245
245
|
*
|
|
246
246
|
* @returns Active span or undefined
|
|
247
247
|
*
|
|
@@ -252,7 +252,7 @@ export function getTracer(name: string, version?: string): Tracer {
|
|
|
252
252
|
* const span = getActiveSpan();
|
|
253
253
|
* if (span) {
|
|
254
254
|
* span.setAttribute('user.id', userId);
|
|
255
|
-
* span.
|
|
255
|
+
* span.setAttribute('user.action', 'click');
|
|
256
256
|
* }
|
|
257
257
|
* ```
|
|
258
258
|
*
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { trace as otelTraceApi } from '@opentelemetry/api';
|
|
3
|
+
import { trace } from './trace-hybrid';
|
|
4
|
+
|
|
5
|
+
describe('hybrid trace', () => {
|
|
6
|
+
it('is callable like autotel trace', () => {
|
|
7
|
+
expect(typeof trace).toBe('function');
|
|
8
|
+
|
|
9
|
+
const wrapped = trace(async (x: number) => x * 2);
|
|
10
|
+
expect(typeof wrapped).toBe('function');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('exposes the OTel TraceAPI surface', () => {
|
|
14
|
+
expect(typeof trace.getActiveSpan).toBe('function');
|
|
15
|
+
expect(typeof trace.getTracer).toBe('function');
|
|
16
|
+
expect(typeof trace.getTracerProvider).toBe('function');
|
|
17
|
+
expect(typeof trace.setSpan).toBe('function');
|
|
18
|
+
expect(typeof trace.getSpan).toBe('function');
|
|
19
|
+
expect(typeof trace.setSpanContext).toBe('function');
|
|
20
|
+
expect(typeof trace.getSpanContext).toBe('function');
|
|
21
|
+
expect(typeof trace.deleteSpan).toBe('function');
|
|
22
|
+
expect(typeof trace.wrapSpanContext).toBe('function');
|
|
23
|
+
expect(typeof trace.isSpanContextValid).toBe('function');
|
|
24
|
+
expect(typeof trace.disable).toBe('function');
|
|
25
|
+
expect(typeof trace.setGlobalTracerProvider).toBe('function');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('forwards getActiveSpan / getTracerProvider to the OTel singleton', () => {
|
|
29
|
+
expect(trace.getActiveSpan()).toBe(otelTraceApi.getActiveSpan());
|
|
30
|
+
// Same TracerProvider singleton — guarantees getTracer goes through one
|
|
31
|
+
// place. (Tracer instances themselves may not be referentially identical
|
|
32
|
+
// because the proxy creates a new wrapper per call.)
|
|
33
|
+
expect(trace.getTracerProvider()).toBe(otelTraceApi.getTracerProvider());
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('preserves `this` for class methods (no unbound-this errors)', () => {
|
|
37
|
+
// setGlobalTracerProvider/getTracerProvider rely on `this._proxyTracerProvider`.
|
|
38
|
+
// Calling through the destructured reference must not throw.
|
|
39
|
+
const { getTracerProvider } = trace;
|
|
40
|
+
expect(() => getTracerProvider()).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid `trace` export: callable like autotel's `trace(fn)`, AND exposes the
|
|
3
|
+
* full `@opentelemetry/api` `TraceAPI` surface (`trace.getActiveSpan()`,
|
|
4
|
+
* `trace.getTracer()`, …) so existing OTel code "just works" when imported
|
|
5
|
+
* from `autotel`.
|
|
6
|
+
*
|
|
7
|
+
* Implementation: `Object.assign` mutates the autotel `trace` function to
|
|
8
|
+
* attach the OTel TraceAPI methods. Because every reference to `trace` across
|
|
9
|
+
* autotel resolves to the same function instance, this is a one-time, global
|
|
10
|
+
* augmentation.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { trace as otelTraceApi } from '@opentelemetry/api';
|
|
14
|
+
import { trace as autotelTraceFn } from './functional';
|
|
15
|
+
|
|
16
|
+
const otelMethods = {
|
|
17
|
+
// Class methods on TraceAPI — bind to the singleton.
|
|
18
|
+
setGlobalTracerProvider:
|
|
19
|
+
otelTraceApi.setGlobalTracerProvider.bind(otelTraceApi),
|
|
20
|
+
getTracerProvider: otelTraceApi.getTracerProvider.bind(otelTraceApi),
|
|
21
|
+
getTracer: otelTraceApi.getTracer.bind(otelTraceApi),
|
|
22
|
+
disable: otelTraceApi.disable.bind(otelTraceApi),
|
|
23
|
+
// Instance fields on TraceAPI — already standalone, copy by reference.
|
|
24
|
+
wrapSpanContext: otelTraceApi.wrapSpanContext,
|
|
25
|
+
isSpanContextValid: otelTraceApi.isSpanContextValid,
|
|
26
|
+
deleteSpan: otelTraceApi.deleteSpan,
|
|
27
|
+
getSpan: otelTraceApi.getSpan,
|
|
28
|
+
getActiveSpan: otelTraceApi.getActiveSpan,
|
|
29
|
+
getSpanContext: otelTraceApi.getSpanContext,
|
|
30
|
+
setSpan: otelTraceApi.setSpan,
|
|
31
|
+
setSpanContext: otelTraceApi.setSpanContext,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const trace: typeof autotelTraceFn & typeof otelTraceApi = Object.assign(
|
|
35
|
+
autotelTraceFn,
|
|
36
|
+
otelMethods,
|
|
37
|
+
) as typeof autotelTraceFn & typeof otelTraceApi;
|
package/src/webhook.ts
CHANGED
|
@@ -36,8 +36,10 @@
|
|
|
36
36
|
|
|
37
37
|
import { SpanKind, trace as otelTrace } from '@opentelemetry/api';
|
|
38
38
|
import type { SpanContext, Link } from '@opentelemetry/api';
|
|
39
|
+
import { emitCorrelatedEvent } from './correlated-events';
|
|
39
40
|
import { trace } from './functional';
|
|
40
|
-
import type { TraceContext } from './trace-context';
|
|
41
|
+
import type { AttributeValue, TraceContext } from './trace-context';
|
|
42
|
+
import { recordStructuredError } from './structured-error';
|
|
41
43
|
|
|
42
44
|
// ============================================================================
|
|
43
45
|
// Types
|
|
@@ -426,10 +428,9 @@ export function createParkingLot(config: ParkingLotConfig): ParkingLot {
|
|
|
426
428
|
|
|
427
429
|
await store.save(fullKey, storedContext);
|
|
428
430
|
|
|
429
|
-
// Add event to current span
|
|
430
431
|
const activeSpan = otelTrace.getActiveSpan();
|
|
431
432
|
if (activeSpan) {
|
|
432
|
-
|
|
433
|
+
const parkAttrs: Record<string, AttributeValue> = {
|
|
433
434
|
'parking_lot.correlation_key': correlationKey,
|
|
434
435
|
'parking_lot.ttl_ms': defaultTTLMs,
|
|
435
436
|
...(metadata &&
|
|
@@ -439,7 +440,16 @@ export function createParkingLot(config: ParkingLotConfig): ParkingLot {
|
|
|
439
440
|
v,
|
|
440
441
|
]),
|
|
441
442
|
)),
|
|
442
|
-
}
|
|
443
|
+
};
|
|
444
|
+
emitCorrelatedEvent(
|
|
445
|
+
{
|
|
446
|
+
setAttribute: (k, v) => activeSpan.setAttribute(k, v),
|
|
447
|
+
setAttributes: (a) => activeSpan.setAttributes(a),
|
|
448
|
+
addEvent: (n, a) => activeSpan.addEvent(n, a),
|
|
449
|
+
},
|
|
450
|
+
'trace_context_parked',
|
|
451
|
+
parkAttrs,
|
|
452
|
+
);
|
|
443
453
|
}
|
|
444
454
|
|
|
445
455
|
// Return the unprefixed key so callers can use the same key for retrieve()
|
|
@@ -520,8 +530,7 @@ export function createParkingLot(config: ParkingLotConfig): ParkingLot {
|
|
|
520
530
|
const link = parkingLot.createLink(parkedContext);
|
|
521
531
|
baseCtx.addLinks([link]);
|
|
522
532
|
|
|
523
|
-
|
|
524
|
-
baseCtx.addEvent('parked_context_retrieved', {
|
|
533
|
+
emitCorrelatedEvent(baseCtx, 'parked_context_retrieved', {
|
|
525
534
|
'parking_lot.correlation_key': correlationKey,
|
|
526
535
|
'parking_lot.elapsed_ms': elapsedMs!,
|
|
527
536
|
'parking_lot.original_trace_id': parkedContext.traceId,
|
|
@@ -533,7 +542,7 @@ export function createParkingLot(config: ParkingLotConfig): ParkingLot {
|
|
|
533
542
|
const error = new Error(
|
|
534
543
|
`Required parked context not found for key: ${correlationKey}`,
|
|
535
544
|
);
|
|
536
|
-
baseCtx
|
|
545
|
+
recordStructuredError(baseCtx, error);
|
|
537
546
|
throw error;
|
|
538
547
|
}
|
|
539
548
|
}
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
import { context, propagation, SpanKind } from '@opentelemetry/api';
|
|
51
51
|
import { createSafeBaggageSchema } from './business-baggage';
|
|
52
|
+
import { emitCorrelatedEvent } from './correlated-events';
|
|
52
53
|
import { trace } from './functional';
|
|
53
54
|
import type { TraceContext } from './trace-context';
|
|
54
55
|
|
|
@@ -422,7 +423,7 @@ export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
|
|
|
422
423
|
baggageValues.stepIndex = stepIndex;
|
|
423
424
|
WorkflowBaggage.set(baseCtx, baggageValues);
|
|
424
425
|
|
|
425
|
-
baseCtx
|
|
426
|
+
emitCorrelatedEvent(baseCtx, 'workflow.step_progress', {
|
|
426
427
|
'workflow.step.name': stepName,
|
|
427
428
|
'workflow.step.index': stepIndex,
|
|
428
429
|
});
|
|
@@ -433,7 +434,7 @@ export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
|
|
|
433
434
|
config.onStart?.(workflowCtx);
|
|
434
435
|
|
|
435
436
|
// Add start event
|
|
436
|
-
baseCtx
|
|
437
|
+
emitCorrelatedEvent(baseCtx, 'workflow.started', {
|
|
437
438
|
'workflow.id': workflowId,
|
|
438
439
|
'workflow.name': config.name,
|
|
439
440
|
});
|
|
@@ -446,7 +447,7 @@ export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
|
|
|
446
447
|
config.onComplete?.(workflowCtx, result);
|
|
447
448
|
|
|
448
449
|
// Add completion event
|
|
449
|
-
baseCtx
|
|
450
|
+
emitCorrelatedEvent(baseCtx, 'workflow.completed', {
|
|
450
451
|
'workflow.id': workflowId,
|
|
451
452
|
});
|
|
452
453
|
|
|
@@ -456,7 +457,7 @@ export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
|
|
|
456
457
|
config.onError?.(workflowCtx, error as Error);
|
|
457
458
|
|
|
458
459
|
// Add error event
|
|
459
|
-
baseCtx
|
|
460
|
+
emitCorrelatedEvent(baseCtx, 'workflow.failed', {
|
|
460
461
|
'workflow.id': workflowId,
|
|
461
462
|
'workflow.error': (error as Error).message,
|
|
462
463
|
});
|
|
@@ -634,12 +635,16 @@ export function traceDistributedStep<TArgs extends unknown[], TReturn>(
|
|
|
634
635
|
requiresCompensation(data?: Record<string, unknown>): void {
|
|
635
636
|
compensationData = data;
|
|
636
637
|
baseCtx.setAttribute('workflow.step.requires_compensation', true);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
638
|
+
emitCorrelatedEvent(
|
|
639
|
+
baseCtx,
|
|
640
|
+
'workflow.step.compensation_registered',
|
|
641
|
+
{
|
|
642
|
+
'workflow.step.name': config.name,
|
|
643
|
+
...(data && {
|
|
644
|
+
'workflow.step.compensation_data': JSON.stringify(data),
|
|
645
|
+
}),
|
|
646
|
+
},
|
|
647
|
+
);
|
|
643
648
|
},
|
|
644
649
|
};
|
|
645
650
|
|
|
@@ -647,7 +652,7 @@ export function traceDistributedStep<TArgs extends unknown[], TReturn>(
|
|
|
647
652
|
config.onStart?.(stepCtx);
|
|
648
653
|
|
|
649
654
|
// Add start event
|
|
650
|
-
baseCtx
|
|
655
|
+
emitCorrelatedEvent(baseCtx, 'workflow.step.started', {
|
|
651
656
|
'workflow.step.name': config.name,
|
|
652
657
|
...(baggageValues && { 'workflow.id': baggageValues.workflowId }),
|
|
653
658
|
});
|
|
@@ -660,7 +665,7 @@ export function traceDistributedStep<TArgs extends unknown[], TReturn>(
|
|
|
660
665
|
config.onComplete?.(stepCtx, result);
|
|
661
666
|
|
|
662
667
|
// Add completion event
|
|
663
|
-
baseCtx
|
|
668
|
+
emitCorrelatedEvent(baseCtx, 'workflow.step.completed', {
|
|
664
669
|
'workflow.step.name': config.name,
|
|
665
670
|
});
|
|
666
671
|
|
|
@@ -670,7 +675,7 @@ export function traceDistributedStep<TArgs extends unknown[], TReturn>(
|
|
|
670
675
|
config.onError?.(stepCtx, error as Error);
|
|
671
676
|
|
|
672
677
|
// Add error event with compensation info if registered
|
|
673
|
-
baseCtx
|
|
678
|
+
emitCorrelatedEvent(baseCtx, 'workflow.step.failed', {
|
|
674
679
|
'workflow.step.name': config.name,
|
|
675
680
|
'workflow.step.error': (error as Error).message,
|
|
676
681
|
...(compensationData && {
|