autotel 2.22.0 → 2.23.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 +112 -6
- package/dist/auto.cjs +3 -3
- package/dist/auto.js +2 -2
- package/dist/{chunk-EWH2542B.js → chunk-3AMR5XLZ.js} +3 -3
- package/dist/{chunk-EWH2542B.js.map → chunk-3AMR5XLZ.js.map} +1 -1
- package/dist/chunk-3QXBFGKP.js +344 -0
- package/dist/chunk-3QXBFGKP.js.map +1 -0
- package/dist/{chunk-VQFF2WMP.cjs → chunk-3ZFDJJWZ.cjs} +37 -29
- package/dist/chunk-3ZFDJJWZ.cjs.map +1 -0
- package/dist/{chunk-CQC6RVLR.cjs → chunk-4RZ4JUBY.cjs} +5 -5
- package/dist/{chunk-CQC6RVLR.cjs.map → chunk-4RZ4JUBY.cjs.map} +1 -1
- package/dist/{chunk-PAVYKPCQ.js → chunk-5XUEHX7J.js} +3 -3
- package/dist/{chunk-PAVYKPCQ.js.map → chunk-5XUEHX7J.js.map} +1 -1
- package/dist/chunk-6S5RUKU3.cjs +347 -0
- package/dist/chunk-6S5RUKU3.cjs.map +1 -0
- package/dist/{chunk-BS757SL2.js → chunk-724XLWR3.js} +9 -4
- package/dist/chunk-724XLWR3.js.map +1 -0
- package/dist/chunk-7EQ4G4SI.cjs +146 -0
- package/dist/chunk-7EQ4G4SI.cjs.map +1 -0
- package/dist/{chunk-CQP5SQT4.cjs → chunk-AXFWWJF3.cjs} +7 -7
- package/dist/{chunk-CQP5SQT4.cjs.map → chunk-AXFWWJF3.cjs.map} +1 -1
- package/dist/{chunk-7NH625MS.cjs → chunk-BSZP4URK.cjs} +5 -5
- package/dist/{chunk-7NH625MS.cjs.map → chunk-BSZP4URK.cjs.map} +1 -1
- package/dist/{chunk-GZFH6P5U.js → chunk-GY4CRZSV.js} +14 -6
- package/dist/chunk-GY4CRZSV.js.map +1 -0
- package/dist/{chunk-QKUGUDXJ.cjs → chunk-HSEIUH7F.cjs} +10 -5
- package/dist/chunk-HSEIUH7F.cjs.map +1 -0
- package/dist/{chunk-DTW3WB7Z.js → chunk-IPKXURBW.js} +3 -3
- package/dist/{chunk-DTW3WB7Z.js.map → chunk-IPKXURBW.js.map} +1 -1
- package/dist/chunk-J7VGRIAJ.js +64 -0
- package/dist/chunk-J7VGRIAJ.js.map +1 -0
- package/dist/chunk-KFOHQK7X.js +144 -0
- package/dist/chunk-KFOHQK7X.js.map +1 -0
- package/dist/{chunk-4UYR46UP.cjs → chunk-MSUHW2I4.cjs} +13 -13
- package/dist/{chunk-4UYR46UP.cjs.map → chunk-MSUHW2I4.cjs.map} +1 -1
- package/dist/chunk-T4B5LB6E.cjs +66 -0
- package/dist/chunk-T4B5LB6E.cjs.map +1 -0
- package/dist/{chunk-QHT4MUED.js → chunk-WCIIFRGL.js} +3 -3
- package/dist/{chunk-QHT4MUED.js.map → chunk-WCIIFRGL.js.map} +1 -1
- package/dist/decorators.cjs +3 -3
- package/dist/decorators.js +3 -3
- package/dist/drain-pipeline.cjs +13 -0
- package/dist/drain-pipeline.cjs.map +1 -0
- package/dist/drain-pipeline.d.cts +37 -0
- package/dist/drain-pipeline.d.ts +37 -0
- package/dist/drain-pipeline.js +4 -0
- package/dist/drain-pipeline.js.map +1 -0
- package/dist/event.cjs +6 -6
- package/dist/event.js +3 -3
- package/dist/functional.cjs +10 -10
- package/dist/functional.js +3 -3
- package/dist/index.cjs +256 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -3
- package/dist/index.d.ts +72 -3
- package/dist/index.js +210 -11
- package/dist/index.js.map +1 -1
- package/dist/{init-BMiXSJNM.d.cts → init-BC5aN8bh.d.cts} +18 -0
- package/dist/{init-ByRbNTRo.d.ts → init-_FG4IbhF.d.ts} +18 -0
- package/dist/instrumentation.cjs +9 -9
- package/dist/instrumentation.js +2 -2
- package/dist/messaging.cjs +7 -7
- package/dist/messaging.js +4 -4
- package/dist/parse-error.cjs +13 -0
- package/dist/parse-error.cjs.map +1 -0
- package/dist/parse-error.d.cts +13 -0
- package/dist/parse-error.d.ts +13 -0
- package/dist/parse-error.js +4 -0
- package/dist/parse-error.js.map +1 -0
- package/dist/processors.cjs +2 -2
- package/dist/processors.d.cts +40 -4
- package/dist/processors.d.ts +40 -4
- package/dist/processors.js +1 -1
- package/dist/semantic-helpers.cjs +8 -8
- package/dist/semantic-helpers.js +4 -4
- package/dist/webhook.cjs +4 -4
- package/dist/webhook.js +3 -3
- package/dist/workflow-distributed.cjs +5 -5
- package/dist/workflow-distributed.js +3 -3
- package/dist/workflow.cjs +8 -8
- package/dist/workflow.js +4 -4
- package/dist/yaml-config.d.cts +2 -1
- package/dist/yaml-config.d.ts +2 -1
- package/package.json +11 -1
- package/src/drain-pipeline.test.ts +68 -0
- package/src/drain-pipeline.ts +199 -0
- package/src/flatten-attributes.test.ts +76 -0
- package/src/flatten-attributes.ts +80 -0
- package/src/functional.test.ts +63 -0
- package/src/functional.ts +11 -3
- package/src/index.ts +33 -0
- package/src/init.ts +22 -0
- package/src/parse-error.test.ts +73 -0
- package/src/parse-error.ts +112 -0
- package/src/pretty-log-formatter.test.ts +123 -0
- package/src/pretty-log-formatter.ts +210 -0
- package/src/processors/canonical-log-line-processor.test.ts +81 -25
- package/src/processors/canonical-log-line-processor.ts +130 -42
- package/src/request-logger.test.ts +124 -0
- package/src/request-logger.ts +140 -0
- package/src/structured-error.test.ts +76 -0
- package/src/structured-error.ts +86 -0
- package/dist/chunk-2RQDNGV3.js +0 -126
- package/dist/chunk-2RQDNGV3.js.map +0 -1
- package/dist/chunk-BS757SL2.js.map +0 -1
- package/dist/chunk-GZFH6P5U.js.map +0 -1
- package/dist/chunk-ONK2Y22L.cjs +0 -128
- package/dist/chunk-ONK2Y22L.cjs.map +0 -1
- package/dist/chunk-QKUGUDXJ.cjs.map +0 -1
- package/dist/chunk-VQFF2WMP.cjs.map +0 -1
package/dist/yaml-config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as AutotelConfig } from './init-
|
|
1
|
+
import { A as AutotelConfig } from './init-_FG4IbhF.js';
|
|
2
2
|
import '@opentelemetry/sdk-trace-base';
|
|
3
3
|
import '@opentelemetry/sdk-node';
|
|
4
4
|
import '@opentelemetry/resources';
|
|
@@ -12,6 +12,7 @@ import '@opentelemetry/sdk-logs';
|
|
|
12
12
|
import './filtering-span-processor.js';
|
|
13
13
|
import './span-name-normalizer.js';
|
|
14
14
|
import './attribute-redacting-processor.js';
|
|
15
|
+
import './processors.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* YAML configuration loader for autotel
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
4
4
|
"description": "Write Once, Observe Anywhere",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -202,6 +202,16 @@
|
|
|
202
202
|
"types": "./dist/test-span-collector.d.ts",
|
|
203
203
|
"import": "./dist/test-span-collector.js",
|
|
204
204
|
"require": "./dist/test-span-collector.cjs"
|
|
205
|
+
},
|
|
206
|
+
"./parse-error": {
|
|
207
|
+
"types": "./dist/parse-error.d.ts",
|
|
208
|
+
"import": "./dist/parse-error.js",
|
|
209
|
+
"require": "./dist/parse-error.cjs"
|
|
210
|
+
},
|
|
211
|
+
"./drain-pipeline": {
|
|
212
|
+
"types": "./dist/drain-pipeline.d.ts",
|
|
213
|
+
"import": "./dist/drain-pipeline.js",
|
|
214
|
+
"require": "./dist/drain-pipeline.cjs"
|
|
205
215
|
}
|
|
206
216
|
},
|
|
207
217
|
"files": [
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createDrainPipeline } from './drain-pipeline';
|
|
3
|
+
|
|
4
|
+
describe('createDrainPipeline', () => {
|
|
5
|
+
it('batches by size and sends to drain', async () => {
|
|
6
|
+
const batchDrain = vi.fn(async () => {});
|
|
7
|
+
const pipeline = createDrainPipeline<number>({
|
|
8
|
+
batch: { size: 2, intervalMs: 1000 },
|
|
9
|
+
});
|
|
10
|
+
const drain = pipeline(batchDrain);
|
|
11
|
+
|
|
12
|
+
drain(1);
|
|
13
|
+
drain(2);
|
|
14
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
15
|
+
|
|
16
|
+
expect(batchDrain).toHaveBeenCalledTimes(1);
|
|
17
|
+
expect(batchDrain).toHaveBeenCalledWith([1, 2]);
|
|
18
|
+
expect(drain.pending).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('retries failed batches and eventually succeeds', async () => {
|
|
22
|
+
let attempts = 0;
|
|
23
|
+
const batchDrain = vi.fn(async () => {
|
|
24
|
+
attempts++;
|
|
25
|
+
if (attempts < 2) throw new Error('temporary');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const pipeline = createDrainPipeline<number>({
|
|
29
|
+
batch: { size: 1, intervalMs: 1000 },
|
|
30
|
+
retry: {
|
|
31
|
+
maxAttempts: 3,
|
|
32
|
+
initialDelayMs: 1,
|
|
33
|
+
maxDelayMs: 2,
|
|
34
|
+
backoff: 'fixed',
|
|
35
|
+
jitter: false,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const drain = pipeline(batchDrain);
|
|
39
|
+
|
|
40
|
+
drain(42);
|
|
41
|
+
await drain.flush();
|
|
42
|
+
|
|
43
|
+
expect(batchDrain).toHaveBeenCalledTimes(2);
|
|
44
|
+
expect(drain.pending).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('drops overflowed events based on policy', async () => {
|
|
48
|
+
const dropped: number[] = [];
|
|
49
|
+
const batchDrain = vi.fn(async () => {});
|
|
50
|
+
const pipeline = createDrainPipeline<number>({
|
|
51
|
+
batch: { size: 10, intervalMs: 1000 },
|
|
52
|
+
maxBufferSize: 2,
|
|
53
|
+
dropPolicy: 'oldest',
|
|
54
|
+
onDropped: (events) => dropped.push(...events),
|
|
55
|
+
});
|
|
56
|
+
const drain = pipeline(batchDrain);
|
|
57
|
+
|
|
58
|
+
drain(1);
|
|
59
|
+
drain(2);
|
|
60
|
+
drain(3); // drops 1
|
|
61
|
+
|
|
62
|
+
expect(dropped).toEqual([1]);
|
|
63
|
+
expect(drain.pending).toBe(2);
|
|
64
|
+
|
|
65
|
+
await drain.flush();
|
|
66
|
+
expect(batchDrain).toHaveBeenCalledWith([2, 3]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export interface DrainPipelineOptions<T = unknown> {
|
|
2
|
+
batch?: {
|
|
3
|
+
/** Maximum events per batch. @default 50 */
|
|
4
|
+
size?: number;
|
|
5
|
+
/** Max time an event can stay buffered before flush. @default 5000 */
|
|
6
|
+
intervalMs?: number;
|
|
7
|
+
};
|
|
8
|
+
retry?: {
|
|
9
|
+
/** Total attempts including first try. @default 3 */
|
|
10
|
+
maxAttempts?: number;
|
|
11
|
+
/** Delay strategy between attempts. @default 'exponential' */
|
|
12
|
+
backoff?: 'exponential' | 'linear' | 'fixed';
|
|
13
|
+
/** Base delay for first retry. @default 1000 */
|
|
14
|
+
initialDelayMs?: number;
|
|
15
|
+
/** Max delay cap. @default 30000 */
|
|
16
|
+
maxDelayMs?: number;
|
|
17
|
+
/** Add random jitter to delays. @default true */
|
|
18
|
+
jitter?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** Max buffered events before dropping. @default 1000 */
|
|
21
|
+
maxBufferSize?: number;
|
|
22
|
+
/** Overflow policy. @default 'oldest' */
|
|
23
|
+
dropPolicy?: 'oldest' | 'newest';
|
|
24
|
+
/** Called when events are dropped from overflow or exhausted retries. */
|
|
25
|
+
onDropped?: (events: T[], error?: Error) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PipelineDrainFn<T> {
|
|
29
|
+
(ctx: T): void;
|
|
30
|
+
/** Flush all buffered events. */
|
|
31
|
+
flush: () => Promise<void>;
|
|
32
|
+
/** Flush and stop scheduling future timer work. */
|
|
33
|
+
shutdown: () => Promise<void>;
|
|
34
|
+
readonly pending: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function wait(ms: number): Promise<void> {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const timer = setTimeout(resolve, ms);
|
|
40
|
+
timer.unref?.();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createDrainPipeline<T = unknown>(
|
|
45
|
+
options?: DrainPipelineOptions<T>,
|
|
46
|
+
): (drain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T> {
|
|
47
|
+
const batchSize = options?.batch?.size ?? 50;
|
|
48
|
+
const intervalMs = options?.batch?.intervalMs ?? 5000;
|
|
49
|
+
const maxBufferSize = options?.maxBufferSize ?? 1000;
|
|
50
|
+
const maxAttempts = options?.retry?.maxAttempts ?? 3;
|
|
51
|
+
const backoff = options?.retry?.backoff ?? 'exponential';
|
|
52
|
+
const initialDelayMs = options?.retry?.initialDelayMs ?? 1000;
|
|
53
|
+
const maxDelayMs = options?.retry?.maxDelayMs ?? 30_000;
|
|
54
|
+
const jitter = options?.retry?.jitter ?? true;
|
|
55
|
+
const dropPolicy = options?.dropPolicy ?? 'oldest';
|
|
56
|
+
const onDropped = options?.onDropped;
|
|
57
|
+
|
|
58
|
+
if (!Number.isFinite(batchSize) || batchSize <= 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`[autotel/drain-pipeline] batch.size must be a positive finite number, got: ${batchSize}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[autotel/drain-pipeline] batch.intervalMs must be a positive finite number, got: ${intervalMs}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`[autotel/drain-pipeline] maxBufferSize must be a positive finite number, got: ${maxBufferSize}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isFinite(maxAttempts) || maxAttempts <= 0) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`[autotel/drain-pipeline] retry.maxAttempts must be a positive finite number, got: ${maxAttempts}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (drain: (batch: T[]) => void | Promise<void>): PipelineDrainFn<T> => {
|
|
80
|
+
const buffer: T[] = [];
|
|
81
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
let activeFlush: Promise<void> | null = null;
|
|
83
|
+
let isShutdown = false;
|
|
84
|
+
|
|
85
|
+
const clearTimer = () => {
|
|
86
|
+
if (timer) {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
timer = null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const computeDelay = (attempt: number): number => {
|
|
93
|
+
const base =
|
|
94
|
+
backoff === 'fixed'
|
|
95
|
+
? initialDelayMs
|
|
96
|
+
: backoff === 'linear'
|
|
97
|
+
? initialDelayMs * attempt
|
|
98
|
+
: initialDelayMs * 2 ** (attempt - 1);
|
|
99
|
+
|
|
100
|
+
const bounded = Math.min(base, maxDelayMs);
|
|
101
|
+
if (!jitter || bounded <= 0) return bounded;
|
|
102
|
+
const factor = 0.5 + Math.random(); // [0.5, 1.5)
|
|
103
|
+
return Math.max(0, Math.round(bounded * factor));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const sendWithRetry = async (batch: T[]): Promise<void> => {
|
|
107
|
+
let lastError: Error | undefined;
|
|
108
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
109
|
+
try {
|
|
110
|
+
await drain(batch);
|
|
111
|
+
return;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
114
|
+
if (attempt < maxAttempts) {
|
|
115
|
+
await wait(computeDelay(attempt));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
onDropped?.(batch, lastError);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const drainBuffer = async (): Promise<void> => {
|
|
123
|
+
while (buffer.length > 0) {
|
|
124
|
+
const batch = buffer.splice(0, batchSize);
|
|
125
|
+
await sendWithRetry(batch);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const scheduleFlush = () => {
|
|
130
|
+
if (isShutdown || timer || activeFlush) return;
|
|
131
|
+
timer = setTimeout(() => {
|
|
132
|
+
timer = null;
|
|
133
|
+
startFlush();
|
|
134
|
+
}, intervalMs);
|
|
135
|
+
timer.unref?.();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const startFlush = () => {
|
|
139
|
+
if (activeFlush || isShutdown) return;
|
|
140
|
+
activeFlush = drainBuffer().finally(() => {
|
|
141
|
+
activeFlush = null;
|
|
142
|
+
if (isShutdown) return;
|
|
143
|
+
if (buffer.length >= batchSize) {
|
|
144
|
+
startFlush();
|
|
145
|
+
} else if (buffer.length > 0) {
|
|
146
|
+
scheduleFlush();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const push = (ctx: T) => {
|
|
152
|
+
if (isShutdown) return;
|
|
153
|
+
|
|
154
|
+
if (buffer.length >= maxBufferSize) {
|
|
155
|
+
if (dropPolicy === 'newest') {
|
|
156
|
+
onDropped?.([ctx]);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const dropped = buffer.splice(0, 1);
|
|
160
|
+
onDropped?.(dropped);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
buffer.push(ctx);
|
|
164
|
+
if (buffer.length >= batchSize) {
|
|
165
|
+
clearTimer();
|
|
166
|
+
startFlush();
|
|
167
|
+
} else {
|
|
168
|
+
scheduleFlush();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const flush = async (): Promise<void> => {
|
|
173
|
+
clearTimer();
|
|
174
|
+
if (activeFlush) await activeFlush;
|
|
175
|
+
|
|
176
|
+
const snapshot = buffer.length;
|
|
177
|
+
if (snapshot <= 0) return;
|
|
178
|
+
const toFlush = buffer.splice(0, snapshot);
|
|
179
|
+
while (toFlush.length > 0) {
|
|
180
|
+
const batch = toFlush.splice(0, batchSize);
|
|
181
|
+
await sendWithRetry(batch);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const shutdown = async (): Promise<void> => {
|
|
186
|
+
isShutdown = true;
|
|
187
|
+
await flush();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const fn = push as PipelineDrainFn<T>;
|
|
191
|
+
fn.flush = flush;
|
|
192
|
+
fn.shutdown = shutdown;
|
|
193
|
+
Object.defineProperty(fn, 'pending', {
|
|
194
|
+
enumerable: true,
|
|
195
|
+
get: () => buffer.length,
|
|
196
|
+
});
|
|
197
|
+
return fn;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { toAttributeValue, flattenToAttributes } from './flatten-attributes';
|
|
3
|
+
|
|
4
|
+
describe('toAttributeValue', () => {
|
|
5
|
+
it('returns primitives as-is', () => {
|
|
6
|
+
expect(toAttributeValue('hello')).toBe('hello');
|
|
7
|
+
expect(toAttributeValue(42)).toBe(42);
|
|
8
|
+
expect(toAttributeValue(true)).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns homogeneous arrays as-is', () => {
|
|
12
|
+
expect(toAttributeValue(['a', 'b'])).toEqual(['a', 'b']);
|
|
13
|
+
expect(toAttributeValue([1, 2])).toEqual([1, 2]);
|
|
14
|
+
expect(toAttributeValue([true, false])).toEqual([true, false]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('serialises mixed arrays to JSON', () => {
|
|
18
|
+
expect(toAttributeValue([1, 'a'])).toBe('[1,"a"]');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('converts Date to ISO string', () => {
|
|
22
|
+
const d = new Date('2025-01-01T00:00:00Z');
|
|
23
|
+
expect(toAttributeValue(d)).toBe('2025-01-01T00:00:00.000Z');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('converts Error to its message', () => {
|
|
27
|
+
expect(toAttributeValue(new Error('boom'))).toBe('boom');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns undefined for plain objects', () => {
|
|
31
|
+
expect(toAttributeValue({ a: 1 })).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('flattenToAttributes', () => {
|
|
36
|
+
it('flattens nested objects with dot-notation keys', () => {
|
|
37
|
+
expect(
|
|
38
|
+
flattenToAttributes({ user: { id: 'u1', plan: 'pro' }, count: 3 }),
|
|
39
|
+
).toEqual({
|
|
40
|
+
'user.id': 'u1',
|
|
41
|
+
'user.plan': 'pro',
|
|
42
|
+
count: 3,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('uses prefix when provided', () => {
|
|
47
|
+
expect(flattenToAttributes({ key: 'val' }, 'error.details')).toEqual({
|
|
48
|
+
'error.details.key': 'val',
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('skips null and undefined values', () => {
|
|
53
|
+
expect(
|
|
54
|
+
flattenToAttributes({ a: 1, b: null, c: undefined, d: 'ok' }),
|
|
55
|
+
).toEqual({ a: 1, d: 'ok' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles circular references without stack overflow', () => {
|
|
59
|
+
const obj: Record<string, unknown> = { name: 'root' };
|
|
60
|
+
obj.self = obj;
|
|
61
|
+
|
|
62
|
+
const result = flattenToAttributes(obj);
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
name: 'root',
|
|
65
|
+
'self.name': 'root',
|
|
66
|
+
'self.self': '<circular-reference>',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('serialises non-plain objects to JSON', () => {
|
|
71
|
+
const result = flattenToAttributes({
|
|
72
|
+
date: new Date('2025-01-01T00:00:00Z'),
|
|
73
|
+
});
|
|
74
|
+
expect(result).toEqual({ date: '2025-01-01T00:00:00.000Z' });
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AttributeValue } from './trace-context';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert an unknown value to an OTel-compatible AttributeValue.
|
|
5
|
+
* Returns undefined when the value cannot be represented.
|
|
6
|
+
*/
|
|
7
|
+
export function toAttributeValue(value: unknown): AttributeValue | undefined {
|
|
8
|
+
if (
|
|
9
|
+
typeof value === 'string' ||
|
|
10
|
+
typeof value === 'number' ||
|
|
11
|
+
typeof value === 'boolean'
|
|
12
|
+
) {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
if (
|
|
17
|
+
value.every((v) => typeof v === 'string') ||
|
|
18
|
+
value.every((v) => typeof v === 'number') ||
|
|
19
|
+
value.every((v) => typeof v === 'boolean')
|
|
20
|
+
) {
|
|
21
|
+
return value as AttributeValue;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
} catch {
|
|
26
|
+
return '<serialization-failed>';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (value instanceof Date) {
|
|
30
|
+
return value.toISOString();
|
|
31
|
+
}
|
|
32
|
+
if (value instanceof Error) {
|
|
33
|
+
return value.message;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Recursively flatten a nested object into dot-notation OTel attributes.
|
|
40
|
+
* Includes circular reference protection via WeakSet.
|
|
41
|
+
*/
|
|
42
|
+
export function flattenToAttributes(
|
|
43
|
+
fields: Record<string, unknown>,
|
|
44
|
+
prefix = '',
|
|
45
|
+
): Record<string, AttributeValue> {
|
|
46
|
+
const out: Record<string, AttributeValue> = {};
|
|
47
|
+
const seen = new WeakSet<object>();
|
|
48
|
+
|
|
49
|
+
function flatten(obj: Record<string, unknown>, currentPrefix: string): void {
|
|
50
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
51
|
+
if (value == null) continue;
|
|
52
|
+
const nextKey = currentPrefix ? `${currentPrefix}.${key}` : key;
|
|
53
|
+
|
|
54
|
+
const attr = toAttributeValue(value);
|
|
55
|
+
if (attr !== undefined) {
|
|
56
|
+
out[nextKey] = attr;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof value === 'object' && value.constructor === Object) {
|
|
61
|
+
if (seen.has(value)) {
|
|
62
|
+
out[nextKey] = '<circular-reference>';
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
seen.add(value);
|
|
66
|
+
flatten(value as Record<string, unknown>, nextKey);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
out[nextKey] = JSON.stringify(value);
|
|
72
|
+
} catch {
|
|
73
|
+
out[nextKey] = '<serialization-failed>';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
flatten(fields, prefix);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
package/src/functional.test.ts
CHANGED
|
@@ -173,6 +173,69 @@ describe('Functional API', () => {
|
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
describe('zero-arg factory pattern (no ctx parameter)', () => {
|
|
177
|
+
it('should detect zero-arg sync factory and execute inner function', () => {
|
|
178
|
+
const collector = createTraceCollector();
|
|
179
|
+
|
|
180
|
+
const addOne = trace(() => (i: number) => {
|
|
181
|
+
return i + 1;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = addOne(1);
|
|
185
|
+
|
|
186
|
+
expect(result).toBe(2);
|
|
187
|
+
expect(result).not.toBeInstanceOf(Promise);
|
|
188
|
+
|
|
189
|
+
const spans = collector.getSpans();
|
|
190
|
+
expect(spans).toHaveLength(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should detect zero-arg async factory and execute inner function', async () => {
|
|
194
|
+
const collector = createTraceCollector();
|
|
195
|
+
|
|
196
|
+
const fetchData = trace(() => async (query: string) => {
|
|
197
|
+
return query.toUpperCase();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const result = await fetchData('test');
|
|
201
|
+
|
|
202
|
+
expect(result).toBe('TEST');
|
|
203
|
+
|
|
204
|
+
const spans = collector.getSpans();
|
|
205
|
+
expect(spans).toHaveLength(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should work with named zero-arg factory', () => {
|
|
209
|
+
const collector = createTraceCollector();
|
|
210
|
+
|
|
211
|
+
const addOne = trace('addOne', () => (i: number) => {
|
|
212
|
+
return i + 1;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result = addOne(1);
|
|
216
|
+
|
|
217
|
+
expect(result).toBe(2);
|
|
218
|
+
|
|
219
|
+
const spans = collector.getSpans();
|
|
220
|
+
expect(spans).toHaveLength(1);
|
|
221
|
+
expect(spans[0]!.name).toBe('addOne');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should handle multiple zero-arg factories combined', () => {
|
|
225
|
+
const collector = createTraceCollector();
|
|
226
|
+
|
|
227
|
+
const addOne = trace('addOne', () => (i: number) => i + 1);
|
|
228
|
+
const addTwo = trace('addTwo', () => (i: number) => i + 2);
|
|
229
|
+
|
|
230
|
+
const result = addOne(1) + addTwo(1);
|
|
231
|
+
|
|
232
|
+
expect(result).toBe(5);
|
|
233
|
+
|
|
234
|
+
const spans = collector.getSpans();
|
|
235
|
+
expect(spans).toHaveLength(2);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
176
239
|
describe('overload 2: trace(name, fn)', () => {
|
|
177
240
|
it('should use custom name', async () => {
|
|
178
241
|
const collector = createTraceCollector();
|
package/src/functional.ts
CHANGED
|
@@ -168,6 +168,14 @@ function looksLikeTraceFactory(fn: GenericFunction): boolean {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
if (fn.length === 0) {
|
|
171
|
+
if (!isAsyncFunction(fn)) {
|
|
172
|
+
try {
|
|
173
|
+
const result = fn();
|
|
174
|
+
return typeof result === 'function';
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
171
179
|
return false;
|
|
172
180
|
}
|
|
173
181
|
|
|
@@ -2361,14 +2369,14 @@ export function withBaggage<T = unknown>(
|
|
|
2361
2369
|
}
|
|
2362
2370
|
return value;
|
|
2363
2371
|
},
|
|
2364
|
-
(
|
|
2372
|
+
(error) => {
|
|
2365
2373
|
// Restore original context before rejecting
|
|
2366
2374
|
if (previousStored) {
|
|
2367
2375
|
return ctxStorage.run(previousStored, () => {
|
|
2368
|
-
throw
|
|
2376
|
+
throw error;
|
|
2369
2377
|
});
|
|
2370
2378
|
}
|
|
2371
|
-
throw
|
|
2379
|
+
throw error;
|
|
2372
2380
|
},
|
|
2373
2381
|
);
|
|
2374
2382
|
}
|
package/src/index.ts
CHANGED
|
@@ -110,6 +110,39 @@ export {
|
|
|
110
110
|
// Graceful shutdown
|
|
111
111
|
export { flush, shutdown } from './shutdown';
|
|
112
112
|
|
|
113
|
+
// Request logger
|
|
114
|
+
export {
|
|
115
|
+
getRequestLogger,
|
|
116
|
+
type RequestLogger,
|
|
117
|
+
type RequestLogSnapshot,
|
|
118
|
+
type RequestLoggerOptions,
|
|
119
|
+
} from './request-logger';
|
|
120
|
+
|
|
121
|
+
// Structured errors
|
|
122
|
+
export {
|
|
123
|
+
createStructuredError,
|
|
124
|
+
getStructuredErrorAttributes,
|
|
125
|
+
recordStructuredError,
|
|
126
|
+
type StructuredError,
|
|
127
|
+
type StructuredErrorInput,
|
|
128
|
+
} from './structured-error';
|
|
129
|
+
|
|
130
|
+
// parseError
|
|
131
|
+
export { parseError, type ParsedError } from './parse-error';
|
|
132
|
+
|
|
133
|
+
// Attribute flattening
|
|
134
|
+
export { toAttributeValue, flattenToAttributes } from './flatten-attributes';
|
|
135
|
+
|
|
136
|
+
// Drain pipeline
|
|
137
|
+
export {
|
|
138
|
+
createDrainPipeline,
|
|
139
|
+
type DrainPipelineOptions,
|
|
140
|
+
type PipelineDrainFn,
|
|
141
|
+
} from './drain-pipeline';
|
|
142
|
+
|
|
143
|
+
// Pretty log formatting
|
|
144
|
+
export { formatDuration } from './pretty-log-formatter';
|
|
145
|
+
|
|
113
146
|
// Re-export sampling strategies
|
|
114
147
|
export {
|
|
115
148
|
type Sampler,
|
package/src/init.ts
CHANGED
|
@@ -976,6 +976,23 @@ export interface AutotelConfig {
|
|
|
976
976
|
) => string;
|
|
977
977
|
/** Whether to include resource attributes (default: true) */
|
|
978
978
|
includeResourceAttributes?: boolean;
|
|
979
|
+
/** Predicate to decide whether to emit (runs after event is built). */
|
|
980
|
+
shouldEmit?: CanonicalLogLineOptions['shouldEmit'];
|
|
981
|
+
/**
|
|
982
|
+
* Declarative tail sampling conditions (OR logic).
|
|
983
|
+
* Ignored when `shouldEmit` is provided.
|
|
984
|
+
* @example keep: [{ status: 500 }, { durationMs: 1000 }]
|
|
985
|
+
*/
|
|
986
|
+
keep?: CanonicalLogLineOptions['keep'];
|
|
987
|
+
/** Callback invoked after emit for custom fan-out. */
|
|
988
|
+
drain?: CanonicalLogLineOptions['drain'];
|
|
989
|
+
/** Handler for drain failures. */
|
|
990
|
+
onDrainError?: CanonicalLogLineOptions['onDrainError'];
|
|
991
|
+
/**
|
|
992
|
+
* Pretty-print canonical log lines to console.
|
|
993
|
+
* Defaults to true when NODE_ENV is 'development'.
|
|
994
|
+
*/
|
|
995
|
+
pretty?: boolean;
|
|
979
996
|
};
|
|
980
997
|
}
|
|
981
998
|
|
|
@@ -1247,6 +1264,11 @@ export function init(cfg: AutotelConfig): void {
|
|
|
1247
1264
|
messageFormat: mergedConfig.canonicalLogLines.messageFormat,
|
|
1248
1265
|
includeResourceAttributes:
|
|
1249
1266
|
mergedConfig.canonicalLogLines.includeResourceAttributes,
|
|
1267
|
+
shouldEmit: mergedConfig.canonicalLogLines.shouldEmit,
|
|
1268
|
+
keep: mergedConfig.canonicalLogLines.keep,
|
|
1269
|
+
drain: mergedConfig.canonicalLogLines.drain,
|
|
1270
|
+
onDrainError: mergedConfig.canonicalLogLines.onDrainError,
|
|
1271
|
+
pretty: mergedConfig.canonicalLogLines.pretty,
|
|
1250
1272
|
};
|
|
1251
1273
|
spanProcessors.push(new CanonicalLogLineProcessor(canonicalOptions));
|
|
1252
1274
|
}
|