autotel 4.1.0 → 4.2.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/package.json +1 -2
- package/src/attribute-redacting-processor.test.ts +0 -763
- package/src/attribute-redacting-processor.ts +0 -621
- package/src/attributes/attachers.ts +0 -161
- package/src/attributes/builders.ts +0 -529
- package/src/attributes/domains.ts +0 -42
- package/src/attributes/index.ts +0 -81
- package/src/attributes/registry.ts +0 -323
- package/src/attributes/types.ts +0 -211
- package/src/attributes/utils.ts +0 -64
- package/src/attributes/validators.ts +0 -266
- package/src/attributes.test.ts +0 -292
- package/src/auto.ts +0 -67
- package/src/autotel-logger.test.ts +0 -548
- package/src/autotel-logger.ts +0 -364
- package/src/baggage-span-processor.test.ts +0 -202
- package/src/baggage-span-processor.ts +0 -100
- package/src/business-baggage.test.ts +0 -500
- package/src/business-baggage.ts +0 -669
- package/src/circuit-breaker.test.ts +0 -341
- package/src/circuit-breaker.ts +0 -184
- package/src/config.test.ts +0 -94
- package/src/config.ts +0 -172
- package/src/correlated-events.test.ts +0 -151
- package/src/correlated-events.ts +0 -47
- package/src/correlation-id.test.ts +0 -163
- package/src/correlation-id.ts +0 -206
- package/src/db.test.ts +0 -252
- package/src/db.ts +0 -447
- package/src/decorators.test.ts +0 -153
- package/src/decorators.ts +0 -188
- package/src/define-event.test.ts +0 -41
- package/src/define-event.ts +0 -58
- package/src/devtools.ts +0 -60
- package/src/drain-pipeline.test.ts +0 -68
- package/src/drain-pipeline.ts +0 -199
- package/src/drain-toolkit.test.ts +0 -113
- package/src/drain-toolkit.ts +0 -129
- package/src/enricher-toolkit.test.ts +0 -67
- package/src/enricher-toolkit.ts +0 -79
- package/src/enrichers.test.ts +0 -150
- package/src/enrichers.ts +0 -145
- package/src/env-config.test.ts +0 -323
- package/src/env-config.ts +0 -309
- package/src/error-catalog.test.ts +0 -133
- package/src/error-catalog.ts +0 -262
- package/src/event-queue.test.ts +0 -864
- package/src/event-queue.ts +0 -699
- package/src/event-subscriber.ts +0 -262
- package/src/event-testing.ts +0 -197
- package/src/event.test.ts +0 -1104
- package/src/event.ts +0 -988
- package/src/events-config.ts +0 -235
- package/src/exporters.ts +0 -165
- package/src/filtering-span-processor.test.ts +0 -281
- package/src/filtering-span-processor.ts +0 -111
- package/src/flatten-attributes.test.ts +0 -76
- package/src/flatten-attributes.ts +0 -80
- package/src/functional.strict-types.typecheck.ts +0 -53
- package/src/functional.test.ts +0 -1464
- package/src/functional.ts +0 -2539
- package/src/functional.types.test.ts +0 -135
- package/src/hook.mjs +0 -15
- package/src/http.test.ts +0 -485
- package/src/http.ts +0 -424
- package/src/index.ts +0 -433
- package/src/init-auto-redactor.test.ts +0 -53
- package/src/init-redactor.test.ts +0 -8
- package/src/init.customization.test.ts +0 -665
- package/src/init.integrations.test.ts +0 -399
- package/src/init.openllmetry.test.ts +0 -194
- package/src/init.protocol.test.ts +0 -215
- package/src/init.ts +0 -2439
- package/src/instrumentation.test.ts +0 -108
- package/src/instrumentation.ts +0 -319
- package/src/logger.test.ts +0 -125
- package/src/logger.ts +0 -341
- package/src/messaging-adapters.test.ts +0 -595
- package/src/messaging-adapters.ts +0 -583
- package/src/messaging-testing.test.ts +0 -573
- package/src/messaging-testing.ts +0 -935
- package/src/messaging.test.ts +0 -1646
- package/src/messaging.ts +0 -2245
- package/src/metric-helpers.ts +0 -47
- package/src/metric-testing.ts +0 -197
- package/src/metric.ts +0 -446
- package/src/metrics.test.ts +0 -241
- package/src/node-require.ts +0 -123
- package/src/operation-context.ts +0 -93
- package/src/parse-error.test.ts +0 -73
- package/src/parse-error.ts +0 -112
- package/src/posthog-logs.test.ts +0 -115
- package/src/posthog-logs.ts +0 -77
- package/src/pretty-console-exporter.test.ts +0 -545
- package/src/pretty-console-exporter.ts +0 -413
- package/src/pretty-log-formatter.test.ts +0 -123
- package/src/pretty-log-formatter.ts +0 -210
- package/src/processors/canonical-log-line-processor.test.ts +0 -523
- package/src/processors/canonical-log-line-processor.ts +0 -396
- package/src/processors.ts +0 -152
- package/src/rate-limiter.test.ts +0 -199
- package/src/rate-limiter.ts +0 -98
- package/src/redact-values.test.ts +0 -90
- package/src/redact-values.ts +0 -34
- package/src/register.ts +0 -37
- package/src/request-logger.test.ts +0 -545
- package/src/request-logger.ts +0 -342
- package/src/sampling.test.ts +0 -1060
- package/src/sampling.ts +0 -737
- package/src/security-schema.test.ts +0 -45
- package/src/security-schema.ts +0 -107
- package/src/semantic-conventions.ts +0 -15
- package/src/semantic-helpers.test.ts +0 -226
- package/src/semantic-helpers.ts +0 -438
- package/src/shutdown.test.ts +0 -364
- package/src/shutdown.ts +0 -246
- package/src/span-name-normalizer.test.ts +0 -377
- package/src/span-name-normalizer.ts +0 -213
- package/src/stable-hash.ts +0 -27
- package/src/structured-error.test.ts +0 -191
- package/src/structured-error.ts +0 -157
- package/src/stub.integration.test.ts +0 -361
- package/src/tail-sampling-processor.test.ts +0 -230
- package/src/tail-sampling-processor.ts +0 -55
- package/src/test-span-collector.test.ts +0 -234
- package/src/test-span-collector.ts +0 -150
- package/src/testing.ts +0 -705
- package/src/trace-context.test.ts +0 -73
- package/src/trace-context.ts +0 -567
- package/src/trace-helpers.new.test.ts +0 -278
- package/src/trace-helpers.test.ts +0 -290
- package/src/trace-helpers.ts +0 -710
- package/src/trace-hybrid.test.ts +0 -42
- package/src/trace-hybrid.ts +0 -37
- package/src/tracer-provider.test.ts +0 -183
- package/src/tracer-provider.ts +0 -266
- package/src/track.test.ts +0 -154
- package/src/track.ts +0 -216
- package/src/validate.test.ts +0 -287
- package/src/validate.ts +0 -307
- package/src/validation-attributes.ts +0 -43
- package/src/validation.test.ts +0 -330
- package/src/validation.ts +0 -246
- package/src/variable-name-inference.test.ts +0 -178
- package/src/variable-name-inference.ts +0 -242
- package/src/webhook.test.ts +0 -649
- package/src/webhook.ts +0 -637
- package/src/workflow-distributed.test.ts +0 -786
- package/src/workflow-distributed.ts +0 -916
- package/src/workflow.async-safety.integration.test.ts +0 -345
- package/src/workflow.test.ts +0 -647
- package/src/workflow.ts +0 -810
- package/src/yaml-config.test.ts +0 -373
- package/src/yaml-config.ts +0 -351
package/src/decorators.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeScript 5+ Decorators for autotel
|
|
3
|
-
*
|
|
4
|
-
* Provides @Trace decorator for class-based code.
|
|
5
|
-
*
|
|
6
|
-
* **Requires TypeScript 5.0+**
|
|
7
|
-
*
|
|
8
|
-
* @example Method decorator
|
|
9
|
-
* ```typescript
|
|
10
|
-
* import { Trace } from 'autotel/decorators'
|
|
11
|
-
*
|
|
12
|
-
* class OrderService {
|
|
13
|
-
* @Trace('order.create', { withMetrics: true })
|
|
14
|
-
* async createOrder(data: OrderData) {
|
|
15
|
-
* return await db.orders.create(data)
|
|
16
|
-
* }
|
|
17
|
-
*
|
|
18
|
-
* @Trace() // Uses method name as span name
|
|
19
|
-
* async processPayment(orderId: string) {
|
|
20
|
-
* return await stripe.charge(orderId)
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import type { TracingOptions, TraceContext } from './functional';
|
|
27
|
-
import { getConfig } from './config';
|
|
28
|
-
import { SpanStatusCode } from '@opentelemetry/api';
|
|
29
|
-
import { createTraceContext } from './trace-context';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Options for @Trace method decorator
|
|
33
|
-
*/
|
|
34
|
-
export interface TraceDecoratorOptions extends Omit<TracingOptions, 'name'> {
|
|
35
|
-
/**
|
|
36
|
-
* Custom span name. If not provided, uses the method name.
|
|
37
|
-
*/
|
|
38
|
-
name?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @Trace - Method decorator for fine-grained tracing
|
|
43
|
-
*
|
|
44
|
-
* Wraps a class method with automatic tracing. Supports both patterns:
|
|
45
|
-
* - Simple: method doesn't use ctx
|
|
46
|
-
* - Advanced: method accesses ctx via this.ctx
|
|
47
|
-
*
|
|
48
|
-
* @example Simple usage (no ctx)
|
|
49
|
-
* ```typescript
|
|
50
|
-
* class OrderService {
|
|
51
|
-
* @Trace()
|
|
52
|
-
* async createOrder(data: OrderData) {
|
|
53
|
-
* return await db.orders.create(data)
|
|
54
|
-
* }
|
|
55
|
-
* }
|
|
56
|
-
* ```
|
|
57
|
-
*
|
|
58
|
-
* @example With custom name and options
|
|
59
|
-
* ```typescript
|
|
60
|
-
* class PaymentService {
|
|
61
|
-
* @Trace('payment.charge', { withMetrics: true })
|
|
62
|
-
* async chargeCard(amount: number) {
|
|
63
|
-
* return await stripe.charges.create({ amount })
|
|
64
|
-
* }
|
|
65
|
-
* }
|
|
66
|
-
* ```
|
|
67
|
-
*
|
|
68
|
-
* @example Accessing ctx
|
|
69
|
-
* ```typescript
|
|
70
|
-
* interface WithTraceContext {
|
|
71
|
-
* ctx?: TraceContext
|
|
72
|
-
* }
|
|
73
|
-
*
|
|
74
|
-
* class UserService {
|
|
75
|
-
* @Trace()
|
|
76
|
-
* async createUser(data: UserData) {
|
|
77
|
-
* // Access ctx via this.ctx (available during execution)
|
|
78
|
-
* const ctx = (this as unknown as WithTraceContext).ctx
|
|
79
|
-
* if (ctx) {
|
|
80
|
-
* ctx.setAttribute('user.id', data.id)
|
|
81
|
-
* }
|
|
82
|
-
* return await db.users.create(data)
|
|
83
|
-
* }
|
|
84
|
-
* }
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
export function Trace(
|
|
88
|
-
options?: TraceDecoratorOptions,
|
|
89
|
-
): <T extends (...args: unknown[]) => Promise<unknown>>(
|
|
90
|
-
originalMethod: T,
|
|
91
|
-
context: ClassMethodDecoratorContext,
|
|
92
|
-
) => T;
|
|
93
|
-
export function Trace(
|
|
94
|
-
name?: string,
|
|
95
|
-
options?: TraceDecoratorOptions,
|
|
96
|
-
): <T extends (...args: unknown[]) => Promise<unknown>>(
|
|
97
|
-
originalMethod: T,
|
|
98
|
-
context: ClassMethodDecoratorContext,
|
|
99
|
-
) => T;
|
|
100
|
-
export function Trace(
|
|
101
|
-
nameOrOptions?: string | TraceDecoratorOptions,
|
|
102
|
-
maybeOptions?: TraceDecoratorOptions,
|
|
103
|
-
): <T extends (...args: unknown[]) => Promise<unknown>>(
|
|
104
|
-
originalMethod: T,
|
|
105
|
-
context: ClassMethodDecoratorContext,
|
|
106
|
-
) => T {
|
|
107
|
-
// Parse arguments
|
|
108
|
-
const name =
|
|
109
|
-
typeof nameOrOptions === 'string' ? nameOrOptions : nameOrOptions?.name;
|
|
110
|
-
// Options are used in the returned decorator function, not here
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
112
|
-
const _options: TraceDecoratorOptions =
|
|
113
|
-
typeof nameOrOptions === 'string'
|
|
114
|
-
? maybeOptions || {}
|
|
115
|
-
: nameOrOptions || {};
|
|
116
|
-
|
|
117
|
-
// TypeScript 5+ decorator signature
|
|
118
|
-
return function <T extends (...args: unknown[]) => Promise<unknown>>(
|
|
119
|
-
originalMethod: T,
|
|
120
|
-
context: ClassMethodDecoratorContext,
|
|
121
|
-
): T {
|
|
122
|
-
const methodName = String(context.name);
|
|
123
|
-
|
|
124
|
-
// Skip if not an async function
|
|
125
|
-
// Check multiple ways to detect async functions (for different transpilation environments)
|
|
126
|
-
// TypeScript decorators run at class definition time, so we need robust detection
|
|
127
|
-
const methodStr = originalMethod?.toString() || '';
|
|
128
|
-
const isAsync =
|
|
129
|
-
originalMethod &&
|
|
130
|
-
(originalMethod.constructor?.name === 'AsyncFunction' ||
|
|
131
|
-
methodStr.trim().startsWith('async ') ||
|
|
132
|
-
(methodStr.includes('[native code]') && methodStr.includes('async')) ||
|
|
133
|
-
// Fallback: if function has async in its string representation
|
|
134
|
-
/async\s+/.test(methodStr));
|
|
135
|
-
|
|
136
|
-
if (!isAsync) {
|
|
137
|
-
// Not an async function, return as-is
|
|
138
|
-
return originalMethod;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const spanName = name || methodName;
|
|
142
|
-
|
|
143
|
-
return async function <This>(
|
|
144
|
-
this: This,
|
|
145
|
-
...args: unknown[]
|
|
146
|
-
): Promise<unknown> {
|
|
147
|
-
const config = getConfig();
|
|
148
|
-
const tracer = config.tracer;
|
|
149
|
-
|
|
150
|
-
return tracer.startActiveSpan(spanName, async (span) => {
|
|
151
|
-
try {
|
|
152
|
-
// Make ctx available via this.ctx for methods that need it
|
|
153
|
-
const ctx: TraceContext = createTraceContext(span);
|
|
154
|
-
|
|
155
|
-
const originalCtx = (this as { ctx?: TraceContext }).ctx;
|
|
156
|
-
try {
|
|
157
|
-
(this as { ctx?: TraceContext }).ctx = ctx;
|
|
158
|
-
const result = await originalMethod.apply(this, args as []);
|
|
159
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
160
|
-
return result;
|
|
161
|
-
} finally {
|
|
162
|
-
// Restore original ctx
|
|
163
|
-
if (originalCtx === undefined) {
|
|
164
|
-
delete (this as { ctx?: TraceContext }).ctx;
|
|
165
|
-
} else {
|
|
166
|
-
(this as { ctx?: TraceContext }).ctx = originalCtx;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} catch (error) {
|
|
170
|
-
span.setStatus({
|
|
171
|
-
code: SpanStatusCode.ERROR,
|
|
172
|
-
message: error instanceof Error ? error.message : 'Unknown error',
|
|
173
|
-
});
|
|
174
|
-
span.recordException(
|
|
175
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
176
|
-
);
|
|
177
|
-
throw error;
|
|
178
|
-
} finally {
|
|
179
|
-
span.end();
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
} as T;
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Re-export types for convenience
|
|
187
|
-
|
|
188
|
-
export { type TraceContext, type TracingOptions } from './functional';
|
package/src/define-event.test.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { defineEvent } from './define-event';
|
|
3
|
-
|
|
4
|
-
describe('defineEvent', () => {
|
|
5
|
-
it('validates payload and exposes schema metadata when provided', () => {
|
|
6
|
-
const event = defineEvent(
|
|
7
|
-
'order.placed',
|
|
8
|
-
{
|
|
9
|
-
safeParse(input: unknown) {
|
|
10
|
-
if (
|
|
11
|
-
typeof input === 'object' &&
|
|
12
|
-
input !== null &&
|
|
13
|
-
'orderId' in input &&
|
|
14
|
-
typeof (input as Record<string, unknown>).orderId === 'string'
|
|
15
|
-
) {
|
|
16
|
-
return {
|
|
17
|
-
success: true as const,
|
|
18
|
-
data: input as { orderId: string },
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
return { success: false as const, error: new Error('invalid') };
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
toJsonSchema: () => ({
|
|
26
|
-
type: 'object',
|
|
27
|
-
properties: { orderId: { type: 'string' } },
|
|
28
|
-
required: ['orderId'],
|
|
29
|
-
}),
|
|
30
|
-
},
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
expect(event.name).toBe('order.placed');
|
|
34
|
-
expect(event.schemaMetadata?.source).toBe('zod');
|
|
35
|
-
expect(event.schemaMetadata?.hash).toMatch(/^[a-f0-9]{64}$/);
|
|
36
|
-
expect(() => event.track({ orderId: 'o-1' })).not.toThrow();
|
|
37
|
-
expect(() => event.track({} as { orderId: string })).toThrow(
|
|
38
|
-
/Schema validation failed/,
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
});
|
package/src/define-event.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { track } from './track';
|
|
2
|
-
import { hashJson } from './stable-hash';
|
|
3
|
-
import type { EventSchemaMetadata } from './event-subscriber';
|
|
4
|
-
|
|
5
|
-
type SafeParseResult<T> =
|
|
6
|
-
| { success: true; data: T }
|
|
7
|
-
| { success: false; error: unknown };
|
|
8
|
-
|
|
9
|
-
export interface SchemaLike<T> {
|
|
10
|
-
safeParse(input: unknown): SafeParseResult<T>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface DefineEventOptions<S> {
|
|
14
|
-
toJsonSchema?: (schema: S) => unknown;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface DefinedEvent<Name extends string, Payload> {
|
|
18
|
-
readonly name: Name;
|
|
19
|
-
readonly schemaMetadata?: EventSchemaMetadata;
|
|
20
|
-
track(payload: Payload): void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function defineEvent<
|
|
24
|
-
Name extends string,
|
|
25
|
-
Payload,
|
|
26
|
-
S extends SchemaLike<Payload>,
|
|
27
|
-
>(
|
|
28
|
-
name: Name,
|
|
29
|
-
schema: S,
|
|
30
|
-
options: DefineEventOptions<S> = {},
|
|
31
|
-
): DefinedEvent<Name, Payload> {
|
|
32
|
-
const jsonSchema = options.toJsonSchema?.(schema);
|
|
33
|
-
const schemaMetadata = jsonSchema
|
|
34
|
-
? {
|
|
35
|
-
source: 'zod' as const,
|
|
36
|
-
jsonSchema,
|
|
37
|
-
hash: hashJson(jsonSchema),
|
|
38
|
-
}
|
|
39
|
-
: undefined;
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
name,
|
|
43
|
-
schemaMetadata,
|
|
44
|
-
track(payload: Payload) {
|
|
45
|
-
const parsed = schema.safeParse(payload);
|
|
46
|
-
if (!parsed.success) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`Invalid payload for event "${name}". Schema validation failed.`,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
track(
|
|
52
|
-
name,
|
|
53
|
-
parsed.data,
|
|
54
|
-
schemaMetadata ? { schema: schemaMetadata } : undefined,
|
|
55
|
-
);
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
package/src/devtools.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export interface AutotelDevtoolsConfig {
|
|
2
|
-
enabled?: boolean;
|
|
3
|
-
endpoint?: string;
|
|
4
|
-
embedded?: boolean;
|
|
5
|
-
host?: string;
|
|
6
|
-
port?: number;
|
|
7
|
-
verbose?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ResolvedAutotelDevtoolsConfig {
|
|
11
|
-
enabled: boolean;
|
|
12
|
-
endpoint?: string;
|
|
13
|
-
embedded: boolean;
|
|
14
|
-
host: string;
|
|
15
|
-
port: number;
|
|
16
|
-
verbose: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const defaultHost = '127.0.0.1';
|
|
20
|
-
const defaultPort = 4318;
|
|
21
|
-
|
|
22
|
-
export function resolveDevtoolsConfig(
|
|
23
|
-
config: boolean | AutotelDevtoolsConfig | undefined,
|
|
24
|
-
): ResolvedAutotelDevtoolsConfig {
|
|
25
|
-
if (!config) {
|
|
26
|
-
return {
|
|
27
|
-
enabled: false,
|
|
28
|
-
endpoint: undefined,
|
|
29
|
-
embedded: false,
|
|
30
|
-
host: defaultHost,
|
|
31
|
-
port: defaultPort,
|
|
32
|
-
verbose: false,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (config === true) {
|
|
37
|
-
return {
|
|
38
|
-
enabled: true,
|
|
39
|
-
endpoint: `http://${defaultHost}:${defaultPort}`,
|
|
40
|
-
embedded: false,
|
|
41
|
-
host: defaultHost,
|
|
42
|
-
port: defaultPort,
|
|
43
|
-
verbose: false,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const enabled = config.enabled ?? true;
|
|
48
|
-
const host = config.host ?? defaultHost;
|
|
49
|
-
const port = config.port ?? defaultPort;
|
|
50
|
-
const endpoint = config.endpoint ?? `http://${host}:${port}`;
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
enabled,
|
|
54
|
-
endpoint: enabled ? endpoint : undefined,
|
|
55
|
-
embedded: enabled && (config.embedded ?? false),
|
|
56
|
-
host,
|
|
57
|
-
port,
|
|
58
|
-
verbose: config.verbose ?? false,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
});
|
package/src/drain-pipeline.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
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
|
-
}
|