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,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation context tracking using AsyncLocalStorage
|
|
3
|
+
*
|
|
4
|
+
* This module provides a way to track operation names across async boundaries
|
|
5
|
+
* so they can be automatically captured in events events.
|
|
6
|
+
*
|
|
7
|
+
* We cannot read span attributes from OpenTelemetry's API (it's write-only),
|
|
8
|
+
* so we maintain our own async context storage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Operation context that flows through async operations
|
|
15
|
+
*/
|
|
16
|
+
export interface OperationContext {
|
|
17
|
+
/**
|
|
18
|
+
* The name of the current operation
|
|
19
|
+
* This is set by trace() or span() and can be read by events
|
|
20
|
+
*/
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* AsyncLocalStorage instance for tracking operation context
|
|
26
|
+
*/
|
|
27
|
+
const operationStorage = new AsyncLocalStorage<OperationContext>();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the current operation context (if any)
|
|
31
|
+
*
|
|
32
|
+
* @returns The current operation context, or undefined if not in an operation
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const ctx = getOperationContext();
|
|
37
|
+
* if (ctx) {
|
|
38
|
+
* console.log('Current operation:', ctx.name);
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function getOperationContext(): OperationContext | undefined {
|
|
43
|
+
return operationStorage.getStore();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run a function within an operation context
|
|
48
|
+
*
|
|
49
|
+
* This sets the operation name for the duration of the function execution,
|
|
50
|
+
* including all async operations spawned from it.
|
|
51
|
+
*
|
|
52
|
+
* @param name - The operation name to set
|
|
53
|
+
* @param fn - The function to execute within the context
|
|
54
|
+
* @returns The result of the function
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const result = await runInOperationContext('user.create', async () => {
|
|
59
|
+
* // Any events.trackEvent() calls here will automatically capture
|
|
60
|
+
* // 'operation.name': 'user.create'
|
|
61
|
+
* await createUser();
|
|
62
|
+
* return 'success';
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function runInOperationContext<T>(name: string, fn: () => T): T {
|
|
67
|
+
return operationStorage.run({ name }, fn);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Update the operation name in the current context
|
|
72
|
+
*
|
|
73
|
+
* This is useful when you want to change the operation name within
|
|
74
|
+
* an already-established context (e.g., when entering a nested span).
|
|
75
|
+
*
|
|
76
|
+
* @param name - The new operation name
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* runInOperationContext('parent.operation', () => {
|
|
81
|
+
* // operation.name is 'parent.operation'
|
|
82
|
+
*
|
|
83
|
+
* updateOperationName('nested.operation');
|
|
84
|
+
* // operation.name is now 'nested.operation'
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function updateOperationName(name: string): void {
|
|
89
|
+
const store = operationStorage.getStore();
|
|
90
|
+
if (store) {
|
|
91
|
+
store.name = name;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Span Processors
|
|
3
|
+
*
|
|
4
|
+
* Re-exports commonly-needed OpenTelemetry span processors for custom configurations.
|
|
5
|
+
*
|
|
6
|
+
* These processors are already included in autotel's dependencies, so re-exporting
|
|
7
|
+
* them provides a "one install is all you need" developer experience without any
|
|
8
|
+
* bundle size impact.
|
|
9
|
+
*
|
|
10
|
+
* Use these when you need custom span processing logic beyond what `init()` provides.
|
|
11
|
+
*
|
|
12
|
+
* @example Simple processor (synchronous, for testing)
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { init } from 'autotel'
|
|
15
|
+
* import { InMemorySpanExporter } from 'autotel/exporters'
|
|
16
|
+
* import { SimpleSpanProcessor } from 'autotel/processors'
|
|
17
|
+
*
|
|
18
|
+
* const exporter = new InMemorySpanExporter()
|
|
19
|
+
* init({
|
|
20
|
+
* service: 'test',
|
|
21
|
+
* spanProcessor: new SimpleSpanProcessor(exporter),
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example Batch processor (async batching, for production)
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { init } from 'autotel'
|
|
28
|
+
* import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
|
29
|
+
* import { BatchSpanProcessor } from 'autotel/processors'
|
|
30
|
+
*
|
|
31
|
+
* const exporter = new OTLPTraceExporter({ url: 'http://collector:4318/v1/traces' })
|
|
32
|
+
* init({
|
|
33
|
+
* service: 'my-app',
|
|
34
|
+
* spanProcessor: new BatchSpanProcessor(exporter, {
|
|
35
|
+
* maxQueueSize: 2048,
|
|
36
|
+
* scheduledDelayMillis: 5000,
|
|
37
|
+
* exportTimeoutMillis: 30000,
|
|
38
|
+
* maxExportBatchSize: 512,
|
|
39
|
+
* }),
|
|
40
|
+
* })
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Note: Most users don't need to use processors directly - `init()` configures
|
|
44
|
+
* BatchSpanProcessor by default. Use these when you need custom processing logic.
|
|
45
|
+
*
|
|
46
|
+
* @module autotel/processors
|
|
47
|
+
* @see {@link https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-processor | OTel Span Processor Spec}
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
/**
|
|
52
|
+
* Simple span processor - processes spans synchronously.
|
|
53
|
+
*
|
|
54
|
+
* Perfect for:
|
|
55
|
+
* - Unit testing (synchronous span export)
|
|
56
|
+
* - Development (immediate span visibility)
|
|
57
|
+
* - Debugging (no batching delays)
|
|
58
|
+
*
|
|
59
|
+
* How it works:
|
|
60
|
+
* - Spans are exported immediately when they end
|
|
61
|
+
* - No batching or queuing
|
|
62
|
+
* - Blocking (export happens on the same thread)
|
|
63
|
+
*
|
|
64
|
+
* Warning: Not recommended for production - use BatchSpanProcessor instead.
|
|
65
|
+
* SimpleSpanProcessor can impact performance in high-throughput scenarios.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import { SimpleSpanProcessor } from 'autotel/processors'
|
|
70
|
+
* import { ConsoleSpanExporter } from 'autotel/exporters'
|
|
71
|
+
*
|
|
72
|
+
* const processor = new SimpleSpanProcessor(new ConsoleSpanExporter())
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
SimpleSpanProcessor,
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Batch span processor - batches spans before exporting.
|
|
79
|
+
*
|
|
80
|
+
* Perfect for:
|
|
81
|
+
* - Production use (efficient, non-blocking)
|
|
82
|
+
* - High-throughput applications
|
|
83
|
+
* - Custom export configurations
|
|
84
|
+
*
|
|
85
|
+
* How it works:
|
|
86
|
+
* - Spans are queued in memory
|
|
87
|
+
* - Exported in batches at regular intervals
|
|
88
|
+
* - Non-blocking (export happens on background thread)
|
|
89
|
+
* - Configurable batch size, delay, queue size
|
|
90
|
+
*
|
|
91
|
+
* This is the default processor used by `init()`.
|
|
92
|
+
*
|
|
93
|
+
* @example Custom configuration
|
|
94
|
+
* ```typescript
|
|
95
|
+
* import { BatchSpanProcessor } from 'autotel/processors'
|
|
96
|
+
*
|
|
97
|
+
* const processor = new BatchSpanProcessor(exporter, {
|
|
98
|
+
* maxQueueSize: 4096, // Max spans in queue
|
|
99
|
+
* scheduledDelayMillis: 10000, // Export every 10s
|
|
100
|
+
* exportTimeoutMillis: 30000, // 30s export timeout
|
|
101
|
+
* maxExportBatchSize: 1024, // Max 1024 spans per batch
|
|
102
|
+
* })
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
BatchSpanProcessor,
|
|
106
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for token bucket rate limiter
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import { TokenBucketRateLimiter } from './rate-limiter';
|
|
7
|
+
|
|
8
|
+
describe('TokenBucketRateLimiter', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.useFakeTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('tryConsume()', () => {
|
|
14
|
+
it('should allow events within rate limit', () => {
|
|
15
|
+
const limiter = new TokenBucketRateLimiter({
|
|
16
|
+
maxEventsPerSecond: 10,
|
|
17
|
+
burstCapacity: 20,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Should allow first 20 events (burst capacity)
|
|
21
|
+
for (let i = 0; i < 20; i++) {
|
|
22
|
+
expect(limiter.tryConsume()).toBe(true);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 21st event should be rejected
|
|
26
|
+
expect(limiter.tryConsume()).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should refill tokens over time', () => {
|
|
30
|
+
const limiter = new TokenBucketRateLimiter({
|
|
31
|
+
maxEventsPerSecond: 10, // 10 events/sec = 1 event/100ms
|
|
32
|
+
burstCapacity: 10,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Consume all tokens
|
|
36
|
+
for (let i = 0; i < 10; i++) {
|
|
37
|
+
expect(limiter.tryConsume()).toBe(true);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Should be rate limited
|
|
41
|
+
expect(limiter.tryConsume()).toBe(false);
|
|
42
|
+
|
|
43
|
+
// Advance time by 100ms (1 token should be added)
|
|
44
|
+
vi.advanceTimersByTime(100);
|
|
45
|
+
|
|
46
|
+
// Should allow 1 more event
|
|
47
|
+
expect(limiter.tryConsume()).toBe(true);
|
|
48
|
+
expect(limiter.tryConsume()).toBe(false);
|
|
49
|
+
|
|
50
|
+
// Advance time by 500ms (5 tokens should be added)
|
|
51
|
+
vi.advanceTimersByTime(500);
|
|
52
|
+
|
|
53
|
+
// Should allow 5 more events
|
|
54
|
+
for (let i = 0; i < 5; i++) {
|
|
55
|
+
expect(limiter.tryConsume()).toBe(true);
|
|
56
|
+
}
|
|
57
|
+
expect(limiter.tryConsume()).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should not exceed max tokens', () => {
|
|
61
|
+
const limiter = new TokenBucketRateLimiter({
|
|
62
|
+
maxEventsPerSecond: 10,
|
|
63
|
+
burstCapacity: 20,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Wait a long time
|
|
67
|
+
vi.advanceTimersByTime(10_000);
|
|
68
|
+
|
|
69
|
+
// Should only have 20 tokens (burstCapacity), not more
|
|
70
|
+
expect(limiter.getAvailableTokens()).toBe(20);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should consume multiple tokens at once', () => {
|
|
74
|
+
const limiter = new TokenBucketRateLimiter({
|
|
75
|
+
maxEventsPerSecond: 100,
|
|
76
|
+
burstCapacity: 200,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Consume 50 tokens at once
|
|
80
|
+
expect(limiter.tryConsume(50)).toBe(true);
|
|
81
|
+
expect(limiter.getAvailableTokens()).toBe(150);
|
|
82
|
+
|
|
83
|
+
// Consume another 150 tokens
|
|
84
|
+
expect(limiter.tryConsume(150)).toBe(true);
|
|
85
|
+
expect(limiter.getAvailableTokens()).toBe(0);
|
|
86
|
+
|
|
87
|
+
// Should reject request for 1 token
|
|
88
|
+
expect(limiter.tryConsume(1)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('waitForToken()', () => {
|
|
93
|
+
it('should wait until token is available', async () => {
|
|
94
|
+
const limiter = new TokenBucketRateLimiter({
|
|
95
|
+
maxEventsPerSecond: 10, // 1 token every 100ms
|
|
96
|
+
burstCapacity: 1,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Consume the only token
|
|
100
|
+
expect(limiter.tryConsume()).toBe(true);
|
|
101
|
+
expect(limiter.tryConsume()).toBe(false);
|
|
102
|
+
|
|
103
|
+
// Wait for next token
|
|
104
|
+
const promise = limiter.waitForToken();
|
|
105
|
+
|
|
106
|
+
// Advance time by 100ms
|
|
107
|
+
vi.advanceTimersByTime(100);
|
|
108
|
+
|
|
109
|
+
// Should resolve after 100ms
|
|
110
|
+
await promise;
|
|
111
|
+
|
|
112
|
+
// Token should be consumed
|
|
113
|
+
expect(limiter.tryConsume()).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should calculate correct wait time for multiple tokens', async () => {
|
|
117
|
+
const limiter = new TokenBucketRateLimiter({
|
|
118
|
+
maxEventsPerSecond: 10, // 1 token every 100ms
|
|
119
|
+
burstCapacity: 10,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Consume all tokens
|
|
123
|
+
expect(limiter.tryConsume(10)).toBe(true);
|
|
124
|
+
|
|
125
|
+
// Request 5 tokens (should wait 500ms)
|
|
126
|
+
const promise = limiter.waitForToken(5);
|
|
127
|
+
|
|
128
|
+
// Advance by 400ms (not enough)
|
|
129
|
+
vi.advanceTimersByTime(400);
|
|
130
|
+
|
|
131
|
+
// Promise should not resolve yet
|
|
132
|
+
let resolved = false;
|
|
133
|
+
promise.then(() => {
|
|
134
|
+
resolved = true;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await vi.runAllTimersAsync();
|
|
138
|
+
|
|
139
|
+
// Should be resolved now
|
|
140
|
+
expect(resolved).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('getAvailableTokens()', () => {
|
|
145
|
+
it('should return current token count', () => {
|
|
146
|
+
const limiter = new TokenBucketRateLimiter({
|
|
147
|
+
maxEventsPerSecond: 100,
|
|
148
|
+
burstCapacity: 200,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(limiter.getAvailableTokens()).toBe(200);
|
|
152
|
+
|
|
153
|
+
limiter.tryConsume(50);
|
|
154
|
+
expect(limiter.getAvailableTokens()).toBe(150);
|
|
155
|
+
|
|
156
|
+
// Advance time by 100ms (10 tokens added)
|
|
157
|
+
vi.advanceTimersByTime(100);
|
|
158
|
+
expect(limiter.getAvailableTokens()).toBe(160);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('reset()', () => {
|
|
163
|
+
it('should reset to full capacity', () => {
|
|
164
|
+
const limiter = new TokenBucketRateLimiter({
|
|
165
|
+
maxEventsPerSecond: 10,
|
|
166
|
+
burstCapacity: 20,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Consume all tokens
|
|
170
|
+
limiter.tryConsume(20);
|
|
171
|
+
expect(limiter.getAvailableTokens()).toBe(0);
|
|
172
|
+
|
|
173
|
+
// Reset
|
|
174
|
+
limiter.reset();
|
|
175
|
+
expect(limiter.getAvailableTokens()).toBe(20);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('Burst capacity', () => {
|
|
180
|
+
it('should default to 2x rate if not specified', () => {
|
|
181
|
+
const limiter = new TokenBucketRateLimiter({
|
|
182
|
+
maxEventsPerSecond: 50,
|
|
183
|
+
// burstCapacity not specified
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Should have 100 tokens (2x rate)
|
|
187
|
+
expect(limiter.getAvailableTokens()).toBe(100);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should allow custom burst capacity', () => {
|
|
191
|
+
const limiter = new TokenBucketRateLimiter({
|
|
192
|
+
maxEventsPerSecond: 50,
|
|
193
|
+
burstCapacity: 500, // 10x rate
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(limiter.getAvailableTokens()).toBe(500);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token bucket rate limiter for event subscribers
|
|
3
|
+
*
|
|
4
|
+
* Prevents overwhelming downstream events platforms with too many events.
|
|
5
|
+
* Uses token bucket algorithm for smooth rate limiting with burst capacity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface RateLimiterConfig {
|
|
9
|
+
/** Maximum events per second (default: 100) */
|
|
10
|
+
maxEventsPerSecond: number;
|
|
11
|
+
/** Burst capacity - max events in a single burst (default: 2x rate) */
|
|
12
|
+
burstCapacity?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Token bucket rate limiter
|
|
17
|
+
*
|
|
18
|
+
* Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
|
|
19
|
+
* Thread-safe for async operations.
|
|
20
|
+
*/
|
|
21
|
+
export class TokenBucketRateLimiter {
|
|
22
|
+
private tokens: number;
|
|
23
|
+
private readonly maxTokens: number;
|
|
24
|
+
private readonly refillRate: number; // tokens per millisecond
|
|
25
|
+
private lastRefill: number;
|
|
26
|
+
|
|
27
|
+
constructor(config: RateLimiterConfig) {
|
|
28
|
+
this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
|
|
29
|
+
this.tokens = this.maxTokens; // Start with full bucket
|
|
30
|
+
this.refillRate = config.maxEventsPerSecond / 1000; // Convert to per-ms
|
|
31
|
+
this.lastRefill = Date.now();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Try to consume a token (allow an event)
|
|
36
|
+
* Returns true if allowed, false if rate limit exceeded
|
|
37
|
+
*/
|
|
38
|
+
tryConsume(count = 1): boolean {
|
|
39
|
+
this.refill();
|
|
40
|
+
|
|
41
|
+
if (this.tokens >= count) {
|
|
42
|
+
this.tokens -= count;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wait until a token is available (async rate limiting)
|
|
51
|
+
* Returns a promise that resolves when the event can be processed
|
|
52
|
+
*/
|
|
53
|
+
async waitForToken(count = 1): Promise<void> {
|
|
54
|
+
this.refill();
|
|
55
|
+
|
|
56
|
+
if (this.tokens >= count) {
|
|
57
|
+
this.tokens -= count;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Calculate wait time until we have enough tokens
|
|
62
|
+
const tokensNeeded = count - this.tokens;
|
|
63
|
+
const waitMs = Math.ceil(tokensNeeded / this.refillRate);
|
|
64
|
+
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
66
|
+
|
|
67
|
+
// After waiting, try again (recursive)
|
|
68
|
+
return this.waitForToken(count);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Refill tokens based on elapsed time
|
|
73
|
+
*/
|
|
74
|
+
private refill(): void {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const elapsed = now - this.lastRefill;
|
|
77
|
+
const tokensToAdd = elapsed * this.refillRate;
|
|
78
|
+
|
|
79
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
|
|
80
|
+
this.lastRefill = now;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get current available tokens (for testing/debugging)
|
|
85
|
+
*/
|
|
86
|
+
getAvailableTokens(): number {
|
|
87
|
+
this.refill();
|
|
88
|
+
return Math.floor(this.tokens);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Reset the rate limiter (for testing)
|
|
93
|
+
*/
|
|
94
|
+
reset(): void {
|
|
95
|
+
this.tokens = this.maxTokens;
|
|
96
|
+
this.lastRefill = Date.now();
|
|
97
|
+
}
|
|
98
|
+
}
|