lambda-deadline-middleware 0.0.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 +278 -0
- package/SECURITY.md +95 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -0
- package/dist/context-store.d.ts +7 -0
- package/dist/context-store.d.ts.map +1 -0
- package/dist/context-store.js +24 -0
- package/dist/context-store.js.map +1 -0
- package/dist/error.d.ts +17 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +21 -0
- package/dist/error.js.map +1 -0
- package/dist/handler-wrapper.d.ts +13 -0
- package/dist/handler-wrapper.d.ts.map +1 -0
- package/dist/handler-wrapper.js +8 -0
- package/dist/handler-wrapper.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +12 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +76 -0
- package/dist/middleware.js.map +1 -0
- package/dist/registration.d.ts +10 -0
- package/dist/registration.d.ts.map +1 -0
- package/dist/registration.js +23 -0
- package/dist/registration.js.map +1 -0
- package/dist/telemetry.d.ts +5 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +82 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +73 -0
- package/src/config.ts +16 -0
- package/src/context-store.ts +36 -0
- package/src/error.ts +34 -0
- package/src/handler-wrapper.ts +19 -0
- package/src/index.ts +18 -0
- package/src/middleware.ts +109 -0
- package/src/registration.ts +36 -0
- package/src/telemetry.ts +129 -0
- package/src/types.ts +50 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { Pluggable } from "@smithy/types";
|
|
5
|
+
|
|
6
|
+
import { parseConfig } from "./config.js";
|
|
7
|
+
import { deadlineMiddlewareHandler } from "./middleware.js";
|
|
8
|
+
import type { DeadlineOptions } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export function deadlineMiddleware(options?: DeadlineOptions): Pluggable<object, object> {
|
|
11
|
+
const config = parseConfig(options);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
applyToStack(stack) {
|
|
15
|
+
stack.add(deadlineMiddlewareHandler(config), {
|
|
16
|
+
step: "finalizeRequest",
|
|
17
|
+
name: "deadlineMiddleware",
|
|
18
|
+
override: true,
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// WeakSet allows GC of discarded clients in long-running processes
|
|
25
|
+
// while still preventing duplicate middleware registration.
|
|
26
|
+
const registeredClients = new WeakSet<object>();
|
|
27
|
+
|
|
28
|
+
export function withDeadline<
|
|
29
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- AWS SDK clients use varying ServiceInput/OutputTypes generics, making `any` the only correct constraint here
|
|
30
|
+
T extends { middlewareStack: { use: (pluggable: Pluggable<any, any>) => void } },
|
|
31
|
+
>(client: T, options?: DeadlineOptions): T {
|
|
32
|
+
if (registeredClients.has(client)) return client;
|
|
33
|
+
registeredClients.add(client);
|
|
34
|
+
client.middlewareStack.use(deadlineMiddleware(options));
|
|
35
|
+
return client;
|
|
36
|
+
}
|
package/src/telemetry.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { DeadlineExceededError } from "./error.js";
|
|
5
|
+
import type { DeadlineMiddlewareConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
interface AbortDetails {
|
|
8
|
+
readonly deadlineMs: number;
|
|
9
|
+
readonly flushBufferMs: number;
|
|
10
|
+
readonly remainingMs: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TelemetryEmitter {
|
|
14
|
+
recordDeadlineAbort: (details: AbortDetails) => void;
|
|
15
|
+
setDeadlineErrorStatus: (error: DeadlineExceededError) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let emitter: TelemetryEmitter | undefined;
|
|
19
|
+
let detected = false;
|
|
20
|
+
|
|
21
|
+
async function detectEmitter(): Promise<TelemetryEmitter | undefined> {
|
|
22
|
+
if (detected) return emitter;
|
|
23
|
+
detected = true;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Variable indirection prevents TypeScript from resolving the module
|
|
27
|
+
// at compile time — keeps @opentelemetry/api as a purely optional runtime dep.
|
|
28
|
+
const moduleName = "@opentelemetry/api";
|
|
29
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- dynamic import of optional runtime dependency
|
|
30
|
+
const otelApi: Record<string, unknown> = await import(/* webpackIgnore: true */ moduleName);
|
|
31
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface
|
|
32
|
+
const trace = otelApi["trace"] as { getActiveSpan: () => unknown } | undefined;
|
|
33
|
+
|
|
34
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing optional OTel API surface
|
|
35
|
+
const SpanStatusCode = otelApi["SpanStatusCode"] as
|
|
36
|
+
| {
|
|
37
|
+
ERROR: number;
|
|
38
|
+
}
|
|
39
|
+
| undefined;
|
|
40
|
+
|
|
41
|
+
if (!trace || !SpanStatusCode) {
|
|
42
|
+
emitter = undefined;
|
|
43
|
+
return emitter;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
emitter = {
|
|
47
|
+
recordDeadlineAbort(details: AbortDetails): void {
|
|
48
|
+
try {
|
|
49
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()
|
|
50
|
+
const span = trace.getActiveSpan() as Record<string, unknown> | undefined;
|
|
51
|
+
if (!span) return;
|
|
52
|
+
|
|
53
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.addEvent method
|
|
54
|
+
const addEvent = span["addEvent"] as
|
|
55
|
+
| ((name: string, attributes: Record<string, unknown>) => void)
|
|
56
|
+
| undefined;
|
|
57
|
+
if (typeof addEvent !== "function") return;
|
|
58
|
+
|
|
59
|
+
addEvent.call(span, "lambda-deadline-middleware.abort", {
|
|
60
|
+
"deadline.duration_ms": details.deadlineMs,
|
|
61
|
+
"deadline.flush_buffer_ms": details.flushBufferMs,
|
|
62
|
+
"deadline.remaining_ms": details.remainingMs,
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// Telemetry must never disrupt request processing
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
setDeadlineErrorStatus(error: DeadlineExceededError): void {
|
|
70
|
+
try {
|
|
71
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span from untyped getActiveSpan()
|
|
72
|
+
const span = trace.getActiveSpan() as Record<string, unknown> | undefined;
|
|
73
|
+
if (!span) return;
|
|
74
|
+
|
|
75
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setStatus method
|
|
76
|
+
const setStatus = span["setStatus"] as
|
|
77
|
+
| ((status: { code: number; message: string }) => void)
|
|
78
|
+
| undefined;
|
|
79
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- duck-typing OTel span.setAttribute method
|
|
80
|
+
const setAttribute = span["setAttribute"] as
|
|
81
|
+
| ((key: string, value: unknown) => void)
|
|
82
|
+
| undefined;
|
|
83
|
+
|
|
84
|
+
if (typeof setStatus === "function") {
|
|
85
|
+
setStatus.call(span, {
|
|
86
|
+
code: SpanStatusCode.ERROR,
|
|
87
|
+
message: error.message,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof setAttribute === "function") {
|
|
92
|
+
setAttribute.call(span, "error.type", "DeadlineExceededError");
|
|
93
|
+
setAttribute.call(span, "deadline.duration_ms", error.deadlineMs);
|
|
94
|
+
setAttribute.call(span, "deadline.flush_buffer_ms", error.flushBufferMs);
|
|
95
|
+
setAttribute.call(span, "deadline.remaining_ms", error.remainingMs);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Telemetry must never disrupt request processing
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
emitter = undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return emitter;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function emitDeadlineAbort(
|
|
110
|
+
error: DeadlineExceededError,
|
|
111
|
+
config: DeadlineMiddlewareConfig,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
try {
|
|
114
|
+
if (!config.telemetryEnabled) return;
|
|
115
|
+
|
|
116
|
+
const em = await detectEmitter();
|
|
117
|
+
if (!em) return;
|
|
118
|
+
|
|
119
|
+
em.recordDeadlineAbort({
|
|
120
|
+
deadlineMs: error.deadlineMs,
|
|
121
|
+
flushBufferMs: error.flushBufferMs,
|
|
122
|
+
remainingMs: error.remainingMs,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
em.setDeadlineErrorStatus(error);
|
|
126
|
+
} catch {
|
|
127
|
+
// Telemetry must never disrupt request processing
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 lambda-deadline-middleware contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
declare const BrandSymbol: unique symbol;
|
|
5
|
+
|
|
6
|
+
type Brand<T, B extends string> = T & { readonly [BrandSymbol]: B };
|
|
7
|
+
|
|
8
|
+
export type Milliseconds = Brand<number, "Milliseconds">;
|
|
9
|
+
|
|
10
|
+
export type FlushBufferMs = Brand<number, "FlushBufferMs">;
|
|
11
|
+
|
|
12
|
+
export type RequestDeadlineMs = Brand<number, "RequestDeadlineMs">;
|
|
13
|
+
|
|
14
|
+
export function milliseconds(value: number): Milliseconds {
|
|
15
|
+
if (!Number.isFinite(value)) {
|
|
16
|
+
throw new TypeError(`milliseconds value must be finite, received: ${value}`);
|
|
17
|
+
}
|
|
18
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
|
|
19
|
+
return value as Milliseconds;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function flushBufferMs(value: number): FlushBufferMs {
|
|
23
|
+
if (!Number.isFinite(value)) {
|
|
24
|
+
throw new TypeError(`flushBufferMs value must be finite, received: ${value}`);
|
|
25
|
+
}
|
|
26
|
+
if (value < 0) {
|
|
27
|
+
throw new TypeError(`flushBufferMs must be non-negative, received: ${value}`);
|
|
28
|
+
}
|
|
29
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- branded type constructor: value is validated above
|
|
30
|
+
return value as FlushBufferMs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type DeadlineComputation =
|
|
34
|
+
| { readonly kind: "deadline"; readonly value: RequestDeadlineMs }
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: "insufficient-time";
|
|
37
|
+
readonly remaining: Milliseconds;
|
|
38
|
+
readonly buffer: FlushBufferMs;
|
|
39
|
+
}
|
|
40
|
+
| { readonly kind: "no-context" };
|
|
41
|
+
|
|
42
|
+
export interface DeadlineMiddlewareConfig {
|
|
43
|
+
readonly flushBufferMs: FlushBufferMs;
|
|
44
|
+
readonly telemetryEnabled: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DeadlineOptions {
|
|
48
|
+
readonly flushBufferMs?: number;
|
|
49
|
+
readonly telemetryEnabled?: boolean;
|
|
50
|
+
}
|