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.
Files changed (110) hide show
  1. package/README.md +112 -6
  2. package/dist/auto.cjs +3 -3
  3. package/dist/auto.js +2 -2
  4. package/dist/{chunk-EWH2542B.js → chunk-3AMR5XLZ.js} +3 -3
  5. package/dist/{chunk-EWH2542B.js.map → chunk-3AMR5XLZ.js.map} +1 -1
  6. package/dist/chunk-3QXBFGKP.js +344 -0
  7. package/dist/chunk-3QXBFGKP.js.map +1 -0
  8. package/dist/{chunk-VQFF2WMP.cjs → chunk-3ZFDJJWZ.cjs} +37 -29
  9. package/dist/chunk-3ZFDJJWZ.cjs.map +1 -0
  10. package/dist/{chunk-CQC6RVLR.cjs → chunk-4RZ4JUBY.cjs} +5 -5
  11. package/dist/{chunk-CQC6RVLR.cjs.map → chunk-4RZ4JUBY.cjs.map} +1 -1
  12. package/dist/{chunk-PAVYKPCQ.js → chunk-5XUEHX7J.js} +3 -3
  13. package/dist/{chunk-PAVYKPCQ.js.map → chunk-5XUEHX7J.js.map} +1 -1
  14. package/dist/chunk-6S5RUKU3.cjs +347 -0
  15. package/dist/chunk-6S5RUKU3.cjs.map +1 -0
  16. package/dist/{chunk-BS757SL2.js → chunk-724XLWR3.js} +9 -4
  17. package/dist/chunk-724XLWR3.js.map +1 -0
  18. package/dist/chunk-7EQ4G4SI.cjs +146 -0
  19. package/dist/chunk-7EQ4G4SI.cjs.map +1 -0
  20. package/dist/{chunk-CQP5SQT4.cjs → chunk-AXFWWJF3.cjs} +7 -7
  21. package/dist/{chunk-CQP5SQT4.cjs.map → chunk-AXFWWJF3.cjs.map} +1 -1
  22. package/dist/{chunk-7NH625MS.cjs → chunk-BSZP4URK.cjs} +5 -5
  23. package/dist/{chunk-7NH625MS.cjs.map → chunk-BSZP4URK.cjs.map} +1 -1
  24. package/dist/{chunk-GZFH6P5U.js → chunk-GY4CRZSV.js} +14 -6
  25. package/dist/chunk-GY4CRZSV.js.map +1 -0
  26. package/dist/{chunk-QKUGUDXJ.cjs → chunk-HSEIUH7F.cjs} +10 -5
  27. package/dist/chunk-HSEIUH7F.cjs.map +1 -0
  28. package/dist/{chunk-DTW3WB7Z.js → chunk-IPKXURBW.js} +3 -3
  29. package/dist/{chunk-DTW3WB7Z.js.map → chunk-IPKXURBW.js.map} +1 -1
  30. package/dist/chunk-J7VGRIAJ.js +64 -0
  31. package/dist/chunk-J7VGRIAJ.js.map +1 -0
  32. package/dist/chunk-KFOHQK7X.js +144 -0
  33. package/dist/chunk-KFOHQK7X.js.map +1 -0
  34. package/dist/{chunk-4UYR46UP.cjs → chunk-MSUHW2I4.cjs} +13 -13
  35. package/dist/{chunk-4UYR46UP.cjs.map → chunk-MSUHW2I4.cjs.map} +1 -1
  36. package/dist/chunk-T4B5LB6E.cjs +66 -0
  37. package/dist/chunk-T4B5LB6E.cjs.map +1 -0
  38. package/dist/{chunk-QHT4MUED.js → chunk-WCIIFRGL.js} +3 -3
  39. package/dist/{chunk-QHT4MUED.js.map → chunk-WCIIFRGL.js.map} +1 -1
  40. package/dist/decorators.cjs +3 -3
  41. package/dist/decorators.js +3 -3
  42. package/dist/drain-pipeline.cjs +13 -0
  43. package/dist/drain-pipeline.cjs.map +1 -0
  44. package/dist/drain-pipeline.d.cts +37 -0
  45. package/dist/drain-pipeline.d.ts +37 -0
  46. package/dist/drain-pipeline.js +4 -0
  47. package/dist/drain-pipeline.js.map +1 -0
  48. package/dist/event.cjs +6 -6
  49. package/dist/event.js +3 -3
  50. package/dist/functional.cjs +10 -10
  51. package/dist/functional.js +3 -3
  52. package/dist/index.cjs +256 -41
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/index.d.cts +72 -3
  55. package/dist/index.d.ts +72 -3
  56. package/dist/index.js +210 -11
  57. package/dist/index.js.map +1 -1
  58. package/dist/{init-BMiXSJNM.d.cts → init-BC5aN8bh.d.cts} +18 -0
  59. package/dist/{init-ByRbNTRo.d.ts → init-_FG4IbhF.d.ts} +18 -0
  60. package/dist/instrumentation.cjs +9 -9
  61. package/dist/instrumentation.js +2 -2
  62. package/dist/messaging.cjs +7 -7
  63. package/dist/messaging.js +4 -4
  64. package/dist/parse-error.cjs +13 -0
  65. package/dist/parse-error.cjs.map +1 -0
  66. package/dist/parse-error.d.cts +13 -0
  67. package/dist/parse-error.d.ts +13 -0
  68. package/dist/parse-error.js +4 -0
  69. package/dist/parse-error.js.map +1 -0
  70. package/dist/processors.cjs +2 -2
  71. package/dist/processors.d.cts +40 -4
  72. package/dist/processors.d.ts +40 -4
  73. package/dist/processors.js +1 -1
  74. package/dist/semantic-helpers.cjs +8 -8
  75. package/dist/semantic-helpers.js +4 -4
  76. package/dist/webhook.cjs +4 -4
  77. package/dist/webhook.js +3 -3
  78. package/dist/workflow-distributed.cjs +5 -5
  79. package/dist/workflow-distributed.js +3 -3
  80. package/dist/workflow.cjs +8 -8
  81. package/dist/workflow.js +4 -4
  82. package/dist/yaml-config.d.cts +2 -1
  83. package/dist/yaml-config.d.ts +2 -1
  84. package/package.json +11 -1
  85. package/src/drain-pipeline.test.ts +68 -0
  86. package/src/drain-pipeline.ts +199 -0
  87. package/src/flatten-attributes.test.ts +76 -0
  88. package/src/flatten-attributes.ts +80 -0
  89. package/src/functional.test.ts +63 -0
  90. package/src/functional.ts +11 -3
  91. package/src/index.ts +33 -0
  92. package/src/init.ts +22 -0
  93. package/src/parse-error.test.ts +73 -0
  94. package/src/parse-error.ts +112 -0
  95. package/src/pretty-log-formatter.test.ts +123 -0
  96. package/src/pretty-log-formatter.ts +210 -0
  97. package/src/processors/canonical-log-line-processor.test.ts +81 -25
  98. package/src/processors/canonical-log-line-processor.ts +130 -42
  99. package/src/request-logger.test.ts +124 -0
  100. package/src/request-logger.ts +140 -0
  101. package/src/structured-error.test.ts +76 -0
  102. package/src/structured-error.ts +86 -0
  103. package/dist/chunk-2RQDNGV3.js +0 -126
  104. package/dist/chunk-2RQDNGV3.js.map +0 -1
  105. package/dist/chunk-BS757SL2.js.map +0 -1
  106. package/dist/chunk-GZFH6P5U.js.map +0 -1
  107. package/dist/chunk-ONK2Y22L.cjs +0 -128
  108. package/dist/chunk-ONK2Y22L.cjs.map +0 -1
  109. package/dist/chunk-QKUGUDXJ.cjs.map +0 -1
  110. package/dist/chunk-VQFF2WMP.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { A as AutotelConfig } from './init-ByRbNTRo.js';
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.22.0",
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
+ }
@@ -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
- (err) => {
2372
+ (error) => {
2365
2373
  // Restore original context before rejecting
2366
2374
  if (previousStored) {
2367
2375
  return ctxStorage.run(previousStored, () => {
2368
- throw err;
2376
+ throw error;
2369
2377
  });
2370
2378
  }
2371
- throw err;
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
  }