autotel 2.1.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/LICENSE +21 -0
- package/README.md +1946 -0
- package/dist/chunk-2LNRY4QK.js +273 -0
- package/dist/chunk-2LNRY4QK.js.map +1 -0
- package/dist/chunk-3HENGDW2.js +587 -0
- package/dist/chunk-3HENGDW2.js.map +1 -0
- package/dist/chunk-4OAT42CA.cjs +73 -0
- package/dist/chunk-4OAT42CA.cjs.map +1 -0
- package/dist/chunk-5GWX5LFW.js +70 -0
- package/dist/chunk-5GWX5LFW.js.map +1 -0
- package/dist/chunk-5R2M36QB.js +195 -0
- package/dist/chunk-5R2M36QB.js.map +1 -0
- package/dist/chunk-5ZN622AO.js +73 -0
- package/dist/chunk-5ZN622AO.js.map +1 -0
- package/dist/chunk-77MSMAUQ.cjs +498 -0
- package/dist/chunk-77MSMAUQ.cjs.map +1 -0
- package/dist/chunk-ABPEQ6RK.cjs +596 -0
- package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
- package/dist/chunk-BWYGJKRB.js +95 -0
- package/dist/chunk-BWYGJKRB.js.map +1 -0
- package/dist/chunk-BZHG5IZ4.js +73 -0
- package/dist/chunk-BZHG5IZ4.js.map +1 -0
- package/dist/chunk-G7VZBCD6.cjs +35 -0
- package/dist/chunk-G7VZBCD6.cjs.map +1 -0
- package/dist/chunk-GVLK7YUU.cjs +30 -0
- package/dist/chunk-GVLK7YUU.cjs.map +1 -0
- package/dist/chunk-HCCXC7XG.js +205 -0
- package/dist/chunk-HCCXC7XG.js.map +1 -0
- package/dist/chunk-HE6T6FIX.cjs +203 -0
- package/dist/chunk-HE6T6FIX.cjs.map +1 -0
- package/dist/chunk-KIXWPOCO.cjs +100 -0
- package/dist/chunk-KIXWPOCO.cjs.map +1 -0
- package/dist/chunk-KVGNW3FC.js +87 -0
- package/dist/chunk-KVGNW3FC.js.map +1 -0
- package/dist/chunk-LITNXTTT.js +3 -0
- package/dist/chunk-LITNXTTT.js.map +1 -0
- package/dist/chunk-M4ANN7RL.js +114 -0
- package/dist/chunk-M4ANN7RL.js.map +1 -0
- package/dist/chunk-NC52UBR2.cjs +32 -0
- package/dist/chunk-NC52UBR2.cjs.map +1 -0
- package/dist/chunk-NHCNRQD3.cjs +212 -0
- package/dist/chunk-NHCNRQD3.cjs.map +1 -0
- package/dist/chunk-NZ72VDNY.cjs +4 -0
- package/dist/chunk-NZ72VDNY.cjs.map +1 -0
- package/dist/chunk-P6JUDYNO.js +57 -0
- package/dist/chunk-P6JUDYNO.js.map +1 -0
- package/dist/chunk-RJYY7BWX.js +1349 -0
- package/dist/chunk-RJYY7BWX.js.map +1 -0
- package/dist/chunk-TRI4V5BF.cjs +126 -0
- package/dist/chunk-TRI4V5BF.cjs.map +1 -0
- package/dist/chunk-UL33I6IS.js +139 -0
- package/dist/chunk-UL33I6IS.js.map +1 -0
- package/dist/chunk-URRW6M2C.cjs +61 -0
- package/dist/chunk-URRW6M2C.cjs.map +1 -0
- package/dist/chunk-UY3UYPBZ.cjs +77 -0
- package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
- package/dist/chunk-W3253FGB.cjs +277 -0
- package/dist/chunk-W3253FGB.cjs.map +1 -0
- package/dist/chunk-W7LHZVQF.js +26 -0
- package/dist/chunk-W7LHZVQF.js.map +1 -0
- package/dist/chunk-WBWNM6LB.cjs +1360 -0
- package/dist/chunk-WBWNM6LB.cjs.map +1 -0
- package/dist/chunk-WFJ7L2RV.js +494 -0
- package/dist/chunk-WFJ7L2RV.js.map +1 -0
- package/dist/chunk-X4RMFFMR.js +28 -0
- package/dist/chunk-X4RMFFMR.js.map +1 -0
- package/dist/chunk-Y4Y2S7BM.cjs +92 -0
- package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
- package/dist/chunk-YLPNXZFI.cjs +143 -0
- package/dist/chunk-YLPNXZFI.cjs.map +1 -0
- package/dist/chunk-YTXEZ4SD.cjs +77 -0
- package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
- package/dist/chunk-Z6ZWNWWR.js +30 -0
- package/dist/chunk-Z6ZWNWWR.js.map +1 -0
- package/dist/config.cjs +26 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +75 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -0
- package/dist/db.cjs +233 -0
- package/dist/db.cjs.map +1 -0
- package/dist/db.d.cts +123 -0
- package/dist/db.d.ts +123 -0
- package/dist/db.js +228 -0
- package/dist/db.js.map +1 -0
- package/dist/decorators.cjs +67 -0
- package/dist/decorators.cjs.map +1 -0
- package/dist/decorators.d.cts +91 -0
- package/dist/decorators.d.ts +91 -0
- package/dist/decorators.js +65 -0
- package/dist/decorators.js.map +1 -0
- package/dist/event-subscriber.cjs +6 -0
- package/dist/event-subscriber.cjs.map +1 -0
- package/dist/event-subscriber.d.cts +116 -0
- package/dist/event-subscriber.d.ts +116 -0
- package/dist/event-subscriber.js +3 -0
- package/dist/event-subscriber.js.map +1 -0
- package/dist/event-testing.cjs +21 -0
- package/dist/event-testing.cjs.map +1 -0
- package/dist/event-testing.d.cts +110 -0
- package/dist/event-testing.d.ts +110 -0
- package/dist/event-testing.js +4 -0
- package/dist/event-testing.js.map +1 -0
- package/dist/event.cjs +30 -0
- package/dist/event.cjs.map +1 -0
- package/dist/event.d.cts +282 -0
- package/dist/event.d.ts +282 -0
- package/dist/event.js +13 -0
- package/dist/event.js.map +1 -0
- package/dist/exporters.cjs +17 -0
- package/dist/exporters.cjs.map +1 -0
- package/dist/exporters.d.cts +1 -0
- package/dist/exporters.d.ts +1 -0
- package/dist/exporters.js +4 -0
- package/dist/exporters.js.map +1 -0
- package/dist/functional.cjs +46 -0
- package/dist/functional.cjs.map +1 -0
- package/dist/functional.d.cts +478 -0
- package/dist/functional.d.ts +478 -0
- package/dist/functional.js +13 -0
- package/dist/functional.js.map +1 -0
- package/dist/http.cjs +189 -0
- package/dist/http.cjs.map +1 -0
- package/dist/http.d.cts +169 -0
- package/dist/http.d.ts +169 -0
- package/dist/http.js +184 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +333 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +758 -0
- package/dist/index.d.ts +758 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.cjs +182 -0
- package/dist/instrumentation.cjs.map +1 -0
- package/dist/instrumentation.d.cts +49 -0
- package/dist/instrumentation.d.ts +49 -0
- package/dist/instrumentation.js +179 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/logger.cjs +19 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +146 -0
- package/dist/logger.d.ts +146 -0
- package/dist/logger.js +6 -0
- package/dist/logger.js.map +1 -0
- package/dist/metric-helpers.cjs +31 -0
- package/dist/metric-helpers.cjs.map +1 -0
- package/dist/metric-helpers.d.cts +13 -0
- package/dist/metric-helpers.d.ts +13 -0
- package/dist/metric-helpers.js +6 -0
- package/dist/metric-helpers.js.map +1 -0
- package/dist/metric-testing.cjs +21 -0
- package/dist/metric-testing.cjs.map +1 -0
- package/dist/metric-testing.d.cts +110 -0
- package/dist/metric-testing.d.ts +110 -0
- package/dist/metric-testing.js +4 -0
- package/dist/metric-testing.js.map +1 -0
- package/dist/metric.cjs +26 -0
- package/dist/metric.cjs.map +1 -0
- package/dist/metric.d.cts +240 -0
- package/dist/metric.d.ts +240 -0
- package/dist/metric.js +9 -0
- package/dist/metric.js.map +1 -0
- package/dist/processors.cjs +17 -0
- package/dist/processors.cjs.map +1 -0
- package/dist/processors.d.cts +1 -0
- package/dist/processors.d.ts +1 -0
- package/dist/processors.js +4 -0
- package/dist/processors.js.map +1 -0
- package/dist/sampling.cjs +40 -0
- package/dist/sampling.cjs.map +1 -0
- package/dist/sampling.d.cts +260 -0
- package/dist/sampling.d.ts +260 -0
- package/dist/sampling.js +7 -0
- package/dist/sampling.js.map +1 -0
- package/dist/semantic-helpers.cjs +35 -0
- package/dist/semantic-helpers.cjs.map +1 -0
- package/dist/semantic-helpers.d.cts +442 -0
- package/dist/semantic-helpers.d.ts +442 -0
- package/dist/semantic-helpers.js +14 -0
- package/dist/semantic-helpers.js.map +1 -0
- package/dist/tail-sampling-processor.cjs +13 -0
- package/dist/tail-sampling-processor.cjs.map +1 -0
- package/dist/tail-sampling-processor.d.cts +27 -0
- package/dist/tail-sampling-processor.d.ts +27 -0
- package/dist/tail-sampling-processor.js +4 -0
- package/dist/tail-sampling-processor.js.map +1 -0
- package/dist/testing.cjs +286 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +291 -0
- package/dist/testing.d.ts +291 -0
- package/dist/testing.js +263 -0
- package/dist/testing.js.map +1 -0
- package/dist/trace-context-DRZdUvVY.d.cts +181 -0
- package/dist/trace-context-DRZdUvVY.d.ts +181 -0
- package/dist/trace-helpers.cjs +54 -0
- package/dist/trace-helpers.cjs.map +1 -0
- package/dist/trace-helpers.d.cts +524 -0
- package/dist/trace-helpers.d.ts +524 -0
- package/dist/trace-helpers.js +5 -0
- package/dist/trace-helpers.js.map +1 -0
- package/dist/tracer-provider.cjs +21 -0
- package/dist/tracer-provider.cjs.map +1 -0
- package/dist/tracer-provider.d.cts +169 -0
- package/dist/tracer-provider.d.ts +169 -0
- package/dist/tracer-provider.js +4 -0
- package/dist/tracer-provider.js.map +1 -0
- package/package.json +280 -0
- package/src/baggage-span-processor.test.ts +202 -0
- package/src/baggage-span-processor.ts +98 -0
- package/src/circuit-breaker.test.ts +341 -0
- package/src/circuit-breaker.ts +184 -0
- package/src/config.test.ts +94 -0
- package/src/config.ts +169 -0
- package/src/db.test.ts +252 -0
- package/src/db.ts +447 -0
- package/src/decorators.test.ts +203 -0
- package/src/decorators.ts +188 -0
- package/src/env-config.test.ts +246 -0
- package/src/env-config.ts +158 -0
- package/src/event-queue.test.ts +222 -0
- package/src/event-queue.ts +203 -0
- package/src/event-subscriber.ts +136 -0
- package/src/event-testing.ts +197 -0
- package/src/event.test.ts +718 -0
- package/src/event.ts +556 -0
- package/src/exporters.ts +96 -0
- package/src/functional.test.ts +1059 -0
- package/src/functional.ts +2295 -0
- package/src/http.test.ts +487 -0
- package/src/http.ts +424 -0
- package/src/index.ts +158 -0
- package/src/init.customization.test.ts +210 -0
- package/src/init.integrations.test.ts +366 -0
- package/src/init.openllmetry.test.ts +282 -0
- package/src/init.protocol.test.ts +215 -0
- package/src/init.ts +1426 -0
- package/src/instrumentation.test.ts +108 -0
- package/src/instrumentation.ts +308 -0
- package/src/logger.test.ts +117 -0
- package/src/logger.ts +246 -0
- package/src/metric-helpers.ts +47 -0
- package/src/metric-testing.ts +197 -0
- package/src/metric.ts +434 -0
- package/src/metrics.test.ts +205 -0
- package/src/operation-context.ts +93 -0
- package/src/processors.ts +106 -0
- package/src/rate-limiter.test.ts +199 -0
- package/src/rate-limiter.ts +98 -0
- package/src/sampling.test.ts +513 -0
- package/src/sampling.ts +428 -0
- package/src/semantic-helpers.test.ts +311 -0
- package/src/semantic-helpers.ts +584 -0
- package/src/shutdown.test.ts +311 -0
- package/src/shutdown.ts +222 -0
- package/src/stub.integration.test.ts +361 -0
- package/src/tail-sampling-processor.test.ts +226 -0
- package/src/tail-sampling-processor.ts +51 -0
- package/src/testing.ts +670 -0
- package/src/trace-context.ts +470 -0
- package/src/trace-helpers.new.test.ts +278 -0
- package/src/trace-helpers.test.ts +242 -0
- package/src/trace-helpers.ts +690 -0
- package/src/tracer-provider.test.ts +183 -0
- package/src/tracer-provider.ts +266 -0
- package/src/track.test.ts +153 -0
- package/src/track.ts +120 -0
- package/src/validation.test.ts +306 -0
- package/src/validation.ts +239 -0
- package/src/variable-name-inference.test.ts +178 -0
- package/src/variable-name-inference.ts +242 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for graceful shutdown
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import { flush, shutdown } from './shutdown';
|
|
7
|
+
import { init } from './init';
|
|
8
|
+
import { track, getEventQueue } from './track';
|
|
9
|
+
import { EventSubscriber } from './event-subscriber';
|
|
10
|
+
|
|
11
|
+
// Mock adapter for testing
|
|
12
|
+
class MockAdapter implements EventSubscriber {
|
|
13
|
+
name = 'mock-adapter';
|
|
14
|
+
public events: Array<{ name: string; attributes?: Record<string, unknown> }> =
|
|
15
|
+
[];
|
|
16
|
+
public shutdownCalled = false;
|
|
17
|
+
|
|
18
|
+
async trackEvent(
|
|
19
|
+
name: string,
|
|
20
|
+
attributes?: Record<string, unknown>,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
this.events.push({ name, attributes });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async trackFunnelStep(): Promise<void> {}
|
|
26
|
+
async trackOutcome(): Promise<void> {}
|
|
27
|
+
async trackValue(): Promise<void> {}
|
|
28
|
+
|
|
29
|
+
async shutdown(): Promise<void> {
|
|
30
|
+
this.shutdownCalled = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('shutdown module', () => {
|
|
35
|
+
let mockAdapter: MockAdapter;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
mockAdapter = new MockAdapter();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
// Clean up after each test
|
|
44
|
+
const queue = getEventQueue();
|
|
45
|
+
if (queue) {
|
|
46
|
+
await queue.flush();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('flush()', () => {
|
|
51
|
+
it('should flush events queue', async () => {
|
|
52
|
+
// Initialize with adapter
|
|
53
|
+
init({
|
|
54
|
+
service: 'test-service',
|
|
55
|
+
subscribers: [mockAdapter],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Track events
|
|
59
|
+
track('test.event1', { foo: 'bar' });
|
|
60
|
+
track('test.event2', { baz: 'qux' });
|
|
61
|
+
|
|
62
|
+
// Events should be in queue
|
|
63
|
+
const queue = getEventQueue();
|
|
64
|
+
expect(queue?.size()).toBeGreaterThan(0);
|
|
65
|
+
|
|
66
|
+
// Flush
|
|
67
|
+
await flush();
|
|
68
|
+
|
|
69
|
+
// Queue should be empty (all events sent)
|
|
70
|
+
expect(queue?.size()).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should be safe to call multiple times', async () => {
|
|
74
|
+
init({
|
|
75
|
+
service: 'test-service',
|
|
76
|
+
subscribers: [mockAdapter],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
track('test.event', { data: 'value' });
|
|
80
|
+
|
|
81
|
+
await flush();
|
|
82
|
+
await flush();
|
|
83
|
+
await flush();
|
|
84
|
+
|
|
85
|
+
// Should not throw
|
|
86
|
+
expect(getEventQueue()?.size()).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should be no-op if no events queue initialized', async () => {
|
|
90
|
+
init({ service: 'test-service' }); // No adapters
|
|
91
|
+
|
|
92
|
+
await expect(flush()).resolves.toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should flush OpenTelemetry spans', async () => {
|
|
96
|
+
// Mock tracer provider with forceFlush
|
|
97
|
+
const mockForceFlush = vi.fn();
|
|
98
|
+
const mockTracerProvider = {
|
|
99
|
+
forceFlush: mockForceFlush,
|
|
100
|
+
};
|
|
101
|
+
const mockSdk = {
|
|
102
|
+
getTracerProvider: () => mockTracerProvider,
|
|
103
|
+
shutdown: vi.fn(),
|
|
104
|
+
start: vi.fn(),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
init({
|
|
108
|
+
service: 'test-service',
|
|
109
|
+
sdkFactory: () => mockSdk as any,
|
|
110
|
+
subscribers: [mockAdapter],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Flush should call tracer provider's forceFlush
|
|
114
|
+
await flush();
|
|
115
|
+
|
|
116
|
+
expect(mockForceFlush).toHaveBeenCalledOnce();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle flush timeout', async () => {
|
|
120
|
+
// Mock tracer provider that hangs
|
|
121
|
+
const mockForceFlush = vi.fn().mockImplementation(
|
|
122
|
+
() =>
|
|
123
|
+
new Promise((resolve) => {
|
|
124
|
+
// Never resolves (simulates hanging)
|
|
125
|
+
setTimeout(resolve, 10_000);
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
const mockTracerProvider = {
|
|
129
|
+
forceFlush: mockForceFlush,
|
|
130
|
+
};
|
|
131
|
+
const mockSdk = {
|
|
132
|
+
getTracerProvider: () => mockTracerProvider,
|
|
133
|
+
shutdown: vi.fn(),
|
|
134
|
+
start: vi.fn(),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
init({
|
|
138
|
+
service: 'test-service',
|
|
139
|
+
sdkFactory: () => mockSdk as any,
|
|
140
|
+
subscribers: [mockAdapter],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Flush should timeout and throw
|
|
144
|
+
await expect(flush({ timeout: 100 })).rejects.toThrow('Flush timeout');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should respect custom timeout', async () => {
|
|
148
|
+
const mockForceFlush = vi.fn();
|
|
149
|
+
const mockTracerProvider = {
|
|
150
|
+
forceFlush: mockForceFlush,
|
|
151
|
+
};
|
|
152
|
+
const mockSdk = {
|
|
153
|
+
getTracerProvider: () => mockTracerProvider,
|
|
154
|
+
shutdown: vi.fn(),
|
|
155
|
+
start: vi.fn(),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
init({
|
|
159
|
+
service: 'test-service',
|
|
160
|
+
sdkFactory: () => mockSdk as any,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Flush with custom timeout should work
|
|
164
|
+
await flush({ timeout: 5000 });
|
|
165
|
+
|
|
166
|
+
expect(mockForceFlush).toHaveBeenCalledOnce();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle SDK without forceFlush gracefully', async () => {
|
|
170
|
+
// Mock SDK without forceFlush method
|
|
171
|
+
const mockTracerProvider = {};
|
|
172
|
+
const mockSdk = {
|
|
173
|
+
getTracerProvider: () => mockTracerProvider,
|
|
174
|
+
shutdown: vi.fn(),
|
|
175
|
+
start: vi.fn(),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
init({
|
|
179
|
+
service: 'test-service',
|
|
180
|
+
sdkFactory: () => mockSdk as any,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Should not throw even if forceFlush doesn't exist
|
|
184
|
+
await expect(flush()).resolves.toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle missing tracer provider gracefully', async () => {
|
|
188
|
+
// Mock SDK that returns null tracer provider
|
|
189
|
+
const mockSdk = {
|
|
190
|
+
getTracerProvider: () => null,
|
|
191
|
+
shutdown: vi.fn(),
|
|
192
|
+
start: vi.fn(),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
init({
|
|
196
|
+
service: 'test-service',
|
|
197
|
+
sdkFactory: () => mockSdk as any,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Should not throw
|
|
201
|
+
await expect(flush()).resolves.toBeUndefined();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('shutdown()', () => {
|
|
206
|
+
it('should flush and shutdown SDK', async () => {
|
|
207
|
+
init({
|
|
208
|
+
service: 'test-service',
|
|
209
|
+
subscribers: [mockAdapter],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
track('test.event', { foo: 'bar' });
|
|
213
|
+
|
|
214
|
+
// Shutdown should flush queue and shutdown SDK
|
|
215
|
+
await shutdown();
|
|
216
|
+
|
|
217
|
+
// After shutdown, queue should be null (cleaned up to prevent memory leaks)
|
|
218
|
+
const queue = getEventQueue();
|
|
219
|
+
expect(queue).toBeNull();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should be safe to call multiple times', async () => {
|
|
223
|
+
init({
|
|
224
|
+
service: 'test-service',
|
|
225
|
+
subscribers: [mockAdapter],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await shutdown();
|
|
229
|
+
await shutdown(); // Should not throw
|
|
230
|
+
await shutdown();
|
|
231
|
+
|
|
232
|
+
expect(true).toBe(true); // Test passes if no errors
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should flush before SDK shutdown', async () => {
|
|
236
|
+
init({
|
|
237
|
+
service: 'test-service',
|
|
238
|
+
subscribers: [mockAdapter],
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
track('test.event', { foo: 'bar' });
|
|
242
|
+
|
|
243
|
+
const queue = getEventQueue();
|
|
244
|
+
const queueSizeBefore = queue?.size() || 0;
|
|
245
|
+
expect(queueSizeBefore).toBeGreaterThan(0);
|
|
246
|
+
|
|
247
|
+
await shutdown();
|
|
248
|
+
|
|
249
|
+
// Queue should be empty after shutdown
|
|
250
|
+
const queueSizeAfter = queue?.size() || 0;
|
|
251
|
+
expect(queueSizeAfter).toBe(0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should handle errors during shutdown gracefully', async () => {
|
|
255
|
+
const failingAdapter: EventSubscriber = {
|
|
256
|
+
name: 'failing-adapter',
|
|
257
|
+
trackEvent: vi.fn(),
|
|
258
|
+
trackFunnelStep: vi.fn(),
|
|
259
|
+
trackOutcome: vi.fn(),
|
|
260
|
+
trackValue: vi.fn(),
|
|
261
|
+
shutdown: vi.fn().mockRejectedValue(new Error('Shutdown failed')),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
init({
|
|
265
|
+
service: 'test-service',
|
|
266
|
+
subscribers: [failingAdapter],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Should not throw even if adapter shutdown fails
|
|
270
|
+
await expect(shutdown()).resolves.toBeUndefined();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('Integration', () => {
|
|
275
|
+
it('should properly shutdown in correct order', async () => {
|
|
276
|
+
init({
|
|
277
|
+
service: 'test-service',
|
|
278
|
+
subscribers: [mockAdapter],
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
track('user.signup', { userId: '123' });
|
|
282
|
+
track('order.completed', { orderId: '456' });
|
|
283
|
+
|
|
284
|
+
const queue = getEventQueue();
|
|
285
|
+
expect(queue?.size()).toBeGreaterThan(0);
|
|
286
|
+
|
|
287
|
+
// Shutdown should flush all pending events
|
|
288
|
+
await shutdown();
|
|
289
|
+
|
|
290
|
+
// Queue should be empty (all events flushed)
|
|
291
|
+
expect(queue?.size()).toBe(0);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should work with no events to flush', async () => {
|
|
295
|
+
init({
|
|
296
|
+
service: 'test-service',
|
|
297
|
+
subscribers: [mockAdapter],
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// No events tracked
|
|
301
|
+
const queue = getEventQueue();
|
|
302
|
+
const queueSize = queue?.size() || 0;
|
|
303
|
+
expect(queueSize).toBe(0);
|
|
304
|
+
|
|
305
|
+
await shutdown();
|
|
306
|
+
|
|
307
|
+
// Should complete without error
|
|
308
|
+
expect(mockAdapter.events).toHaveLength(0);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
package/src/shutdown.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graceful shutdown with flush and cleanup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getSdk, getLogger } from './init';
|
|
6
|
+
import { getEventQueue, resetEventQueue } from './track';
|
|
7
|
+
import { resetEvents } from './event';
|
|
8
|
+
import { resetMetrics } from './metric';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Flush all pending telemetry
|
|
12
|
+
*
|
|
13
|
+
* Flushes both events events and OpenTelemetry spans to their destinations.
|
|
14
|
+
* Includes timeout protection to prevent hanging in serverless environments.
|
|
15
|
+
*
|
|
16
|
+
* Safe to call multiple times.
|
|
17
|
+
*
|
|
18
|
+
* @param options - Optional configuration
|
|
19
|
+
* @param options.timeout - Timeout in milliseconds (default: 2000ms)
|
|
20
|
+
*
|
|
21
|
+
* @example Manual flush in serverless
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { flush } from 'autotel';
|
|
24
|
+
*
|
|
25
|
+
* export const handler = async (event) => {
|
|
26
|
+
* // ... process event
|
|
27
|
+
* await flush(); // Flush before function returns
|
|
28
|
+
* return result;
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example With custom timeout
|
|
33
|
+
* ```typescript
|
|
34
|
+
* await flush({ timeout: 5000 }); // 5 second timeout
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export async function flush(options?: { timeout?: number }): Promise<void> {
|
|
38
|
+
const timeout = options?.timeout ?? 2000;
|
|
39
|
+
|
|
40
|
+
const doFlush = async () => {
|
|
41
|
+
// Flush events queue
|
|
42
|
+
const eventsQueue = getEventQueue();
|
|
43
|
+
if (eventsQueue) {
|
|
44
|
+
await eventsQueue.flush();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Flush OpenTelemetry spans
|
|
48
|
+
// This ensures spans are exported immediately, critical for serverless
|
|
49
|
+
const sdk = getSdk();
|
|
50
|
+
if (sdk) {
|
|
51
|
+
try {
|
|
52
|
+
// Type assertion needed as getTracerProvider is not in the public NodeSDK interface
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
const sdkAny = sdk as any;
|
|
55
|
+
if (typeof sdkAny.getTracerProvider === 'function') {
|
|
56
|
+
const tracerProvider = sdkAny.getTracerProvider();
|
|
57
|
+
if (
|
|
58
|
+
tracerProvider &&
|
|
59
|
+
typeof tracerProvider.forceFlush === 'function'
|
|
60
|
+
) {
|
|
61
|
+
await tracerProvider.forceFlush();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore errors when accessing tracer provider (may not be available in test mocks)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Add timeout protection to prevent hanging
|
|
71
|
+
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
72
|
+
try {
|
|
73
|
+
await Promise.race([
|
|
74
|
+
doFlush().finally(() => {
|
|
75
|
+
// Clear timeout as soon as flush completes
|
|
76
|
+
if (timeoutHandle) {
|
|
77
|
+
clearTimeout(timeoutHandle);
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
new Promise<void>((_, reject) => {
|
|
81
|
+
timeoutHandle = setTimeout(
|
|
82
|
+
() => reject(new Error('Flush timeout')),
|
|
83
|
+
timeout,
|
|
84
|
+
);
|
|
85
|
+
// Use unref() to allow Node to exit if flush completes first
|
|
86
|
+
// This prevents the 2s delay in serverless when flush succeeds immediately
|
|
87
|
+
timeoutHandle.unref();
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Clear timeout on error too
|
|
92
|
+
if (timeoutHandle) {
|
|
93
|
+
clearTimeout(timeoutHandle);
|
|
94
|
+
}
|
|
95
|
+
const logger = getLogger();
|
|
96
|
+
logger.error(
|
|
97
|
+
'[autotel] Flush error:',
|
|
98
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
99
|
+
);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Shutdown telemetry and cleanup resources
|
|
106
|
+
*
|
|
107
|
+
* - Flushes all pending data
|
|
108
|
+
* - Shuts down OpenTelemetry SDK
|
|
109
|
+
* - Cleans up resources
|
|
110
|
+
*
|
|
111
|
+
* Call this before process exit.
|
|
112
|
+
*
|
|
113
|
+
* Always performs cleanup even if flush fails, preventing resource leaks
|
|
114
|
+
* in serverless handlers or tests.
|
|
115
|
+
*
|
|
116
|
+
* @example Express server
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const server = app.listen(3000)
|
|
119
|
+
*
|
|
120
|
+
* process.on('SIGTERM', async () => {
|
|
121
|
+
* await server.close()
|
|
122
|
+
* await shutdown()
|
|
123
|
+
* process.exit(0)
|
|
124
|
+
* })
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export async function shutdown(): Promise<void> {
|
|
128
|
+
const logger = getLogger();
|
|
129
|
+
let shutdownError: Error | null = null;
|
|
130
|
+
|
|
131
|
+
// Attempt to flush, but continue with cleanup even if it fails
|
|
132
|
+
try {
|
|
133
|
+
await flush();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
136
|
+
shutdownError = err;
|
|
137
|
+
logger.error(
|
|
138
|
+
'[autotel] Flush failed during shutdown, continuing cleanup',
|
|
139
|
+
err,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Always shutdown SDK and clean up resources
|
|
144
|
+
try {
|
|
145
|
+
// Shutdown OpenTelemetry SDK
|
|
146
|
+
const sdk = getSdk();
|
|
147
|
+
if (sdk) {
|
|
148
|
+
await sdk.shutdown();
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
152
|
+
|
|
153
|
+
// Ignore ECONNREFUSED errors - this happens when no OTLP endpoint was configured
|
|
154
|
+
// The SDK tries to flush exporters that don't exist, which is harmless
|
|
155
|
+
const isConnectionRefused =
|
|
156
|
+
typeof error === 'object' &&
|
|
157
|
+
error !== null &&
|
|
158
|
+
'code' in error &&
|
|
159
|
+
error.code === 'ECONNREFUSED';
|
|
160
|
+
|
|
161
|
+
if (!isConnectionRefused) {
|
|
162
|
+
// Only store/log non-connection errors
|
|
163
|
+
if (!shutdownError) {
|
|
164
|
+
shutdownError = err;
|
|
165
|
+
}
|
|
166
|
+
logger.error('[autotel] SDK shutdown failed', err);
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
// Clean up singleton Maps and queues to prevent memory leaks
|
|
170
|
+
// This runs even if SDK shutdown fails
|
|
171
|
+
resetEvents();
|
|
172
|
+
resetMetrics();
|
|
173
|
+
resetEventQueue();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Rethrow first error after cleanup completes
|
|
177
|
+
// This allows tests and CI to detect failures while still ensuring cleanup
|
|
178
|
+
if (shutdownError) {
|
|
179
|
+
throw shutdownError;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Register automatic shutdown hooks for common signals
|
|
185
|
+
*
|
|
186
|
+
* Handles:
|
|
187
|
+
* - SIGTERM (Docker/K8s graceful shutdown)
|
|
188
|
+
* - SIGINT (Ctrl+C)
|
|
189
|
+
*
|
|
190
|
+
* @internal Called automatically on module load
|
|
191
|
+
*/
|
|
192
|
+
function registerShutdownHooks(): void {
|
|
193
|
+
if (typeof process === 'undefined') return; // Not in Node.js
|
|
194
|
+
|
|
195
|
+
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
|
|
196
|
+
let shuttingDown = false;
|
|
197
|
+
|
|
198
|
+
for (const signal of signals) {
|
|
199
|
+
process.on(signal, async () => {
|
|
200
|
+
if (shuttingDown) return; // Prevent double shutdown
|
|
201
|
+
shuttingDown = true;
|
|
202
|
+
|
|
203
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
204
|
+
getLogger().info(`[autotel] Received ${signal}, flushing telemetry...`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await shutdown();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
getLogger().error(
|
|
211
|
+
'[autotel] Error during shutdown',
|
|
212
|
+
error instanceof Error ? error : undefined,
|
|
213
|
+
);
|
|
214
|
+
} finally {
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Auto-register shutdown hooks
|
|
222
|
+
registerShutdownHooks();
|