@clickhouse/clickstack-node 0.0.0-stage → 1.0.0-rc.1
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 +203 -0
- package/README.md +282 -2
- package/dist/_virtual/_rolldown/runtime.cjs +22 -0
- package/dist/body/http.cjs +115 -0
- package/dist/body/http.mjs +115 -0
- package/dist/body/rules.cjs +225 -0
- package/dist/body/rules.mjs +220 -0
- package/dist/body/undici.cjs +88 -0
- package/dist/body/undici.mjs +87 -0
- package/dist/compose.cjs +107 -0
- package/dist/compose.mjs +107 -0
- package/dist/config.cjs +27 -0
- package/dist/config.d.cts +61 -0
- package/dist/config.d.mts +61 -0
- package/dist/config.mjs +27 -0
- package/dist/env.cjs +88 -0
- package/dist/env.mjs +84 -0
- package/dist/exception.cjs +54 -0
- package/dist/exception.mjs +54 -0
- package/dist/hook.cjs +47 -0
- package/dist/hook.mjs +45 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +3 -0
- package/dist/init.cjs +139 -0
- package/dist/init.d.cts +28 -0
- package/dist/init.d.mts +28 -0
- package/dist/init.mjs +137 -0
- package/dist/lifecycle.cjs +249 -0
- package/dist/lifecycle.d.cts +68 -0
- package/dist/lifecycle.d.mts +68 -0
- package/dist/lifecycle.mjs +245 -0
- package/dist/register.cjs +12 -0
- package/dist/register.d.cts +1 -0
- package/dist/register.d.mts +1 -0
- package/dist/register.mjs +14 -0
- package/dist/semconv.cjs +16 -0
- package/dist/semconv.mjs +13 -0
- package/dist/state.cjs +12 -0
- package/dist/state.mjs +12 -0
- package/dist/version.cjs +4 -0
- package/dist/version.mjs +4 -0
- package/package.json +114 -4
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const require_state = require("./state.cjs");
|
|
2
|
+
const require_env = require("./env.cjs");
|
|
3
|
+
let node_worker_threads = require("node:worker_threads");
|
|
4
|
+
let _opentelemetry_api = require("@opentelemetry/api");
|
|
5
|
+
let _opentelemetry_core = require("@opentelemetry/core");
|
|
6
|
+
let node_util = require("node:util");
|
|
7
|
+
let node_os = require("node:os");
|
|
8
|
+
let _opentelemetry_api_logs = require("@opentelemetry/api-logs");
|
|
9
|
+
// src/lifecycle.ts
|
|
10
|
+
const DEFAULT_TIMEOUT_MILLIS = 5e3;
|
|
11
|
+
const SENTRY_SEND_MILLIS = 2e3;
|
|
12
|
+
const INVOCATION_MARGIN_MILLIS = 500;
|
|
13
|
+
const SIGNALS = ["SIGTERM", "SIGINT"];
|
|
14
|
+
function bounded(work, ms, keepAlive) {
|
|
15
|
+
let timer;
|
|
16
|
+
const deadline = new Promise((resolve) => {
|
|
17
|
+
timer = setTimeout(resolve, ms, false);
|
|
18
|
+
if (!keepAlive) timer.unref();
|
|
19
|
+
});
|
|
20
|
+
const settled = Promise.resolve().then(work).then(() => true);
|
|
21
|
+
return Promise.race([settled, deadline]).then((inTime) => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
if (!inTime) require_env.report("warn", `flush or shutdown did not finish within ${String(ms)} ms`);
|
|
24
|
+
return inTime;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function timeoutOf(timeoutMillis) {
|
|
28
|
+
if (timeoutMillis !== void 0 && timeoutMillis > 0) return Math.min(timeoutMillis, 2147483647);
|
|
29
|
+
const deadline = require_state.getState().invocationDeadline;
|
|
30
|
+
return deadline === void 0 ? DEFAULT_TIMEOUT_MILLIS : Math.max(1, Math.min(DEFAULT_TIMEOUT_MILLIS, deadline - Date.now()));
|
|
31
|
+
}
|
|
32
|
+
function sdkShutdown() {
|
|
33
|
+
const state = require_state.getState();
|
|
34
|
+
const sdk = state.sdk;
|
|
35
|
+
if (sdk === void 0) return Promise.resolve();
|
|
36
|
+
return state.shutdownPromise ??= (async () => {
|
|
37
|
+
await state.flushInFlight;
|
|
38
|
+
try {
|
|
39
|
+
await sdk.shutdown();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
require_env.report("warn", "shutdown failed", error);
|
|
42
|
+
}
|
|
43
|
+
state.unsubscribeUndici?.();
|
|
44
|
+
})();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Records when the current AWS Lambda invocation must return, so flushes end in time.
|
|
48
|
+
*
|
|
49
|
+
* @param context - The invocation context; anything without `getRemainingTimeInMillis` is ignored.
|
|
50
|
+
*/
|
|
51
|
+
function recordInvocationDeadline(context) {
|
|
52
|
+
const remaining = context?.getRemainingTimeInMillis?.();
|
|
53
|
+
if (typeof remaining === "number") require_state.getState().invocationDeadline = Date.now() + remaining - INVOCATION_MARGIN_MILLIS;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Exports the spans, metrics and log records the SDK has buffered, without stopping it.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* Calls `forceFlush` on the global tracer, meter and logger providers. Before `init`, or with
|
|
60
|
+
* `OTEL_SDK_DISABLED=true`, it resolves at once; after {@link shutdown} it waits for the shutdown.
|
|
61
|
+
* Failures and timeouts are reported through the OpenTelemetry `diag` logger. Inside a
|
|
62
|
+
* {@link withFlush} invocation the default bound also ends before the invocation's deadline.
|
|
63
|
+
*
|
|
64
|
+
* @param timeoutMillis - Upper bound in milliseconds, 5000 by default; zero, negative and NaN
|
|
65
|
+
* mean the default.
|
|
66
|
+
* @returns A promise that resolves when every provider has flushed or the bound has passed. It
|
|
67
|
+
* never rejects.
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
async function flush(timeoutMillis) {
|
|
71
|
+
const state = require_state.getState();
|
|
72
|
+
if (state.sdk === void 0) return;
|
|
73
|
+
const ms = timeoutOf(timeoutMillis);
|
|
74
|
+
const pending = state.shutdownPromise;
|
|
75
|
+
if (pending !== void 0) {
|
|
76
|
+
await bounded(() => pending, ms, false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const tracer = _opentelemetry_api.trace.getTracerProvider();
|
|
80
|
+
const providers = [
|
|
81
|
+
tracer.getDelegate?.() ?? tracer,
|
|
82
|
+
_opentelemetry_api.metrics.getMeterProvider(),
|
|
83
|
+
_opentelemetry_api_logs.logs.getLoggerProvider()
|
|
84
|
+
];
|
|
85
|
+
const work = Promise.all(providers.map(async (provider) => {
|
|
86
|
+
const p = provider;
|
|
87
|
+
if (typeof p.forceFlush !== "function") return;
|
|
88
|
+
try {
|
|
89
|
+
await p.forceFlush({ timeoutMillis: ms });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
require_env.report("warn", "flush failed", error);
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
state.flushInFlight = Promise.all([state.flushInFlight, work]);
|
|
95
|
+
await bounded(() => work, ms, false);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Flushes and stops the SDK started by `init`; telemetry recorded afterwards is dropped.
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* Later calls, and the automatic shutdown on `beforeExit`, SIGTERM, SIGINT and uncaught
|
|
102
|
+
* exceptions, share the first call's work. Scripts that end with `process.exit()`, and app signal
|
|
103
|
+
* handlers that exit, should await it first, because `process.exit()` skips `beforeExit`.
|
|
104
|
+
*
|
|
105
|
+
* @param timeoutMillis - Upper bound in milliseconds, 5000 by default; zero, negative and NaN
|
|
106
|
+
* mean the default.
|
|
107
|
+
* @returns A promise that resolves when the SDK has shut down or the bound has passed. It never
|
|
108
|
+
* rejects.
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* process.on('SIGTERM', async () => {
|
|
112
|
+
* await server.close();
|
|
113
|
+
* await shutdown();
|
|
114
|
+
* process.exit(0);
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
119
|
+
async function shutdown(timeoutMillis) {
|
|
120
|
+
if (require_state.getState().sdk === void 0) return;
|
|
121
|
+
await bounded(sdkShutdown, timeoutOf(timeoutMillis), false);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Wraps an async serverless handler so buffered telemetry is exported before each invocation
|
|
125
|
+
* returns.
|
|
126
|
+
*
|
|
127
|
+
* @remarks
|
|
128
|
+
* Awaits the handler, then {@link flush} in a `finally` block, so telemetry is also exported when
|
|
129
|
+
* the handler throws. Flushes during the invocation end 500 ms before its deadline
|
|
130
|
+
* (`context.getRemainingTimeInMillis()`). On its first call it installs console capture again if
|
|
131
|
+
* the AWS Lambda runtime replaced the console methods after the preload ran. Callback-style
|
|
132
|
+
* handlers are not supported.
|
|
133
|
+
*
|
|
134
|
+
* @typeParam E - The invocation event type.
|
|
135
|
+
* @typeParam C - The runtime context type, for example the AWS Lambda `Context`.
|
|
136
|
+
* @typeParam R - The value the handler's promise resolves to.
|
|
137
|
+
* @param handler - The async handler to wrap.
|
|
138
|
+
* @returns A handler with the same parameters and result.
|
|
139
|
+
* @example
|
|
140
|
+
* With `NODE_OPTIONS=--import @clickhouse/clickstack-node/register` set on the function:
|
|
141
|
+
* ```ts
|
|
142
|
+
* import { withFlush } from '@clickhouse/clickstack-node';
|
|
143
|
+
*
|
|
144
|
+
* export const handler = withFlush(async (event: { name?: string }) => {
|
|
145
|
+
* console.log('greeting', event.name);
|
|
146
|
+
* return { statusCode: 200, body: 'ok' };
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
* @public
|
|
150
|
+
*/
|
|
151
|
+
function withFlush(handler) {
|
|
152
|
+
let firstCall = true;
|
|
153
|
+
return async function flushingHandler(event, context) {
|
|
154
|
+
const state = require_state.getState();
|
|
155
|
+
try {
|
|
156
|
+
recordInvocationDeadline(context);
|
|
157
|
+
if (firstCall) {
|
|
158
|
+
firstCall = false;
|
|
159
|
+
const current = console.log;
|
|
160
|
+
if (state.repatchConsole !== void 0 && current !== state.consoleLog) {
|
|
161
|
+
state.repatchConsole();
|
|
162
|
+
state.consoleLog = console.log;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
require_env.report("warn", "could not prepare the invocation", error);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return await handler(event, context);
|
|
170
|
+
} finally {
|
|
171
|
+
await flush();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function writeFatal(error) {
|
|
176
|
+
try {
|
|
177
|
+
const body = typeof error === "string" ? error : (0, node_util.inspect)(error, {
|
|
178
|
+
customInspect: false,
|
|
179
|
+
depth: 5
|
|
180
|
+
});
|
|
181
|
+
process.getBuiltinModule("node:fs").writeSync(2, `${body}\n\nNode.js ${process.version}\n`);
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
function signalExitListeners() {
|
|
185
|
+
const v4 = globalThis[Symbol.for("signal-exit emitter")];
|
|
186
|
+
const v3 = process.__signal_exit_emitter__;
|
|
187
|
+
return [v4?.count, v3?.count].reduce((sum, count) => sum + (typeof count === "number" ? count : 0), 0);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Installs the `beforeExit`, SIGTERM, SIGINT and `uncaughtException` handlers, once per thread.
|
|
191
|
+
* Signal and crash handlers go on the main thread only: workers never receive signals, and a
|
|
192
|
+
* worker crash listener would hide the parent's `error` event.
|
|
193
|
+
*/
|
|
194
|
+
function installLifecycle() {
|
|
195
|
+
let terminating = false;
|
|
196
|
+
let terminatingSince = 0;
|
|
197
|
+
if ((0, _opentelemetry_core.getStringFromEnv)("AWS_LAMBDA_FUNCTION_NAME") === void 0) process.once("beforeExit", () => {
|
|
198
|
+
if (terminating) return;
|
|
199
|
+
bounded(sdkShutdown, DEFAULT_TIMEOUT_MILLIS, false).then((inTime) => {
|
|
200
|
+
if (!inTime) process.exit();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
if (!node_worker_threads.isMainThread) return;
|
|
204
|
+
const onSignal = (signal) => {
|
|
205
|
+
if (terminating) {
|
|
206
|
+
if (Date.now() - terminatingSince > 1e3) process.exit(128 + node_os.constants.signals[signal]);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (process.listeners(signal).filter((listener) => listener !== onSignal).length > signalExitListeners()) {
|
|
210
|
+
process.removeListener(signal, onSignal);
|
|
211
|
+
flush();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
terminating = true;
|
|
215
|
+
terminatingSince = Date.now();
|
|
216
|
+
bounded(sdkShutdown, DEFAULT_TIMEOUT_MILLIS, true).then(() => {
|
|
217
|
+
for (const name of SIGNALS) process.removeListener(name, onSignal);
|
|
218
|
+
try {
|
|
219
|
+
process.kill(process.pid, signal);
|
|
220
|
+
} catch {}
|
|
221
|
+
process.exit(128 + node_os.constants.signals[signal]);
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
for (const signal of SIGNALS) process.prependListener(signal, onSignal);
|
|
225
|
+
const onUncaughtException = (error) => {
|
|
226
|
+
try {
|
|
227
|
+
const listeners = process.listeners("uncaughtException");
|
|
228
|
+
const isSentry = (listener) => "_errorHandler" in listener && listener._errorHandler === true;
|
|
229
|
+
if (listeners.some((listener) => listener !== onUncaughtException && listener.name !== "domainUncaughtExceptionClear" && !isSentry(listener))) return;
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
if (terminating) return;
|
|
232
|
+
writeFatal(error);
|
|
233
|
+
terminating = true;
|
|
234
|
+
terminatingSince = Date.now();
|
|
235
|
+
const sentry = listeners.some(isSentry) ? new Promise((resolve) => setTimeout(resolve, SENTRY_SEND_MILLIS)) : void 0;
|
|
236
|
+
Promise.all([bounded(sdkShutdown, DEFAULT_TIMEOUT_MILLIS, true), sentry]).then(() => {
|
|
237
|
+
process.exit(1);
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
process.prependListener("uncaughtException", onUncaughtException);
|
|
244
|
+
}
|
|
245
|
+
exports.flush = flush;
|
|
246
|
+
exports.installLifecycle = installLifecycle;
|
|
247
|
+
exports.recordInvocationDeadline = recordInvocationDeadline;
|
|
248
|
+
exports.shutdown = shutdown;
|
|
249
|
+
exports.withFlush = withFlush;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exports the spans, metrics and log records the SDK has buffered, without stopping it.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Calls `forceFlush` on the global tracer, meter and logger providers. Before `init`, or with
|
|
6
|
+
* `OTEL_SDK_DISABLED=true`, it resolves at once; after {@link shutdown} it waits for the shutdown.
|
|
7
|
+
* Failures and timeouts are reported through the OpenTelemetry `diag` logger. Inside a
|
|
8
|
+
* {@link withFlush} invocation the default bound also ends before the invocation's deadline.
|
|
9
|
+
*
|
|
10
|
+
* @param timeoutMillis - Upper bound in milliseconds, 5000 by default; zero, negative and NaN
|
|
11
|
+
* mean the default.
|
|
12
|
+
* @returns A promise that resolves when every provider has flushed or the bound has passed. It
|
|
13
|
+
* never rejects.
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export declare function flush(timeoutMillis?: number): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Flushes and stops the SDK started by `init`; telemetry recorded afterwards is dropped.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Later calls, and the automatic shutdown on `beforeExit`, SIGTERM, SIGINT and uncaught
|
|
22
|
+
* exceptions, share the first call's work. Scripts that end with `process.exit()`, and app signal
|
|
23
|
+
* handlers that exit, should await it first, because `process.exit()` skips `beforeExit`.
|
|
24
|
+
*
|
|
25
|
+
* @param timeoutMillis - Upper bound in milliseconds, 5000 by default; zero, negative and NaN
|
|
26
|
+
* mean the default.
|
|
27
|
+
* @returns A promise that resolves when the SDK has shut down or the bound has passed. It never
|
|
28
|
+
* rejects.
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* process.on('SIGTERM', async () => {
|
|
32
|
+
* await server.close();
|
|
33
|
+
* await shutdown();
|
|
34
|
+
* process.exit(0);
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export declare function shutdown(timeoutMillis?: number): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Wraps an async serverless handler so buffered telemetry is exported before each invocation
|
|
42
|
+
* returns.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Awaits the handler, then {@link flush} in a `finally` block, so telemetry is also exported when
|
|
46
|
+
* the handler throws. Flushes during the invocation end 500 ms before its deadline
|
|
47
|
+
* (`context.getRemainingTimeInMillis()`). On its first call it installs console capture again if
|
|
48
|
+
* the AWS Lambda runtime replaced the console methods after the preload ran. Callback-style
|
|
49
|
+
* handlers are not supported.
|
|
50
|
+
*
|
|
51
|
+
* @typeParam E - The invocation event type.
|
|
52
|
+
* @typeParam C - The runtime context type, for example the AWS Lambda `Context`.
|
|
53
|
+
* @typeParam R - The value the handler's promise resolves to.
|
|
54
|
+
* @param handler - The async handler to wrap.
|
|
55
|
+
* @returns A handler with the same parameters and result.
|
|
56
|
+
* @example
|
|
57
|
+
* With `NODE_OPTIONS=--import @clickhouse/clickstack-node/register` set on the function:
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { withFlush } from '@clickhouse/clickstack-node';
|
|
60
|
+
*
|
|
61
|
+
* export const handler = withFlush(async (event: { name?: string }) => {
|
|
62
|
+
* console.log('greeting', event.name);
|
|
63
|
+
* return { statusCode: 200, body: 'ok' };
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export declare function withFlush<E, C, R>(handler: (event: E, context: C) => Promise<R>): (event: E, context: C) => Promise<R>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exports the spans, metrics and log records the SDK has buffered, without stopping it.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Calls `forceFlush` on the global tracer, meter and logger providers. Before `init`, or with
|
|
6
|
+
* `OTEL_SDK_DISABLED=true`, it resolves at once; after {@link shutdown} it waits for the shutdown.
|
|
7
|
+
* Failures and timeouts are reported through the OpenTelemetry `diag` logger. Inside a
|
|
8
|
+
* {@link withFlush} invocation the default bound also ends before the invocation's deadline.
|
|
9
|
+
*
|
|
10
|
+
* @param timeoutMillis - Upper bound in milliseconds, 5000 by default; zero, negative and NaN
|
|
11
|
+
* mean the default.
|
|
12
|
+
* @returns A promise that resolves when every provider has flushed or the bound has passed. It
|
|
13
|
+
* never rejects.
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export declare function flush(timeoutMillis?: number): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Flushes and stops the SDK started by `init`; telemetry recorded afterwards is dropped.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Later calls, and the automatic shutdown on `beforeExit`, SIGTERM, SIGINT and uncaught
|
|
22
|
+
* exceptions, share the first call's work. Scripts that end with `process.exit()`, and app signal
|
|
23
|
+
* handlers that exit, should await it first, because `process.exit()` skips `beforeExit`.
|
|
24
|
+
*
|
|
25
|
+
* @param timeoutMillis - Upper bound in milliseconds, 5000 by default; zero, negative and NaN
|
|
26
|
+
* mean the default.
|
|
27
|
+
* @returns A promise that resolves when the SDK has shut down or the bound has passed. It never
|
|
28
|
+
* rejects.
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* process.on('SIGTERM', async () => {
|
|
32
|
+
* await server.close();
|
|
33
|
+
* await shutdown();
|
|
34
|
+
* process.exit(0);
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export declare function shutdown(timeoutMillis?: number): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Wraps an async serverless handler so buffered telemetry is exported before each invocation
|
|
42
|
+
* returns.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Awaits the handler, then {@link flush} in a `finally` block, so telemetry is also exported when
|
|
46
|
+
* the handler throws. Flushes during the invocation end 500 ms before its deadline
|
|
47
|
+
* (`context.getRemainingTimeInMillis()`). On its first call it installs console capture again if
|
|
48
|
+
* the AWS Lambda runtime replaced the console methods after the preload ran. Callback-style
|
|
49
|
+
* handlers are not supported.
|
|
50
|
+
*
|
|
51
|
+
* @typeParam E - The invocation event type.
|
|
52
|
+
* @typeParam C - The runtime context type, for example the AWS Lambda `Context`.
|
|
53
|
+
* @typeParam R - The value the handler's promise resolves to.
|
|
54
|
+
* @param handler - The async handler to wrap.
|
|
55
|
+
* @returns A handler with the same parameters and result.
|
|
56
|
+
* @example
|
|
57
|
+
* With `NODE_OPTIONS=--import @clickhouse/clickstack-node/register` set on the function:
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { withFlush } from '@clickhouse/clickstack-node';
|
|
60
|
+
*
|
|
61
|
+
* export const handler = withFlush(async (event: { name?: string }) => {
|
|
62
|
+
* console.log('greeting', event.name);
|
|
63
|
+
* return { statusCode: 200, body: 'ok' };
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export declare function withFlush<E, C, R>(handler: (event: E, context: C) => Promise<R>): (event: E, context: C) => Promise<R>;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { getState } from "./state.mjs";
|
|
2
|
+
import { report } from "./env.mjs";
|
|
3
|
+
import { isMainThread } from "node:worker_threads";
|
|
4
|
+
import { metrics, trace } from "@opentelemetry/api";
|
|
5
|
+
import { getStringFromEnv } from "@opentelemetry/core";
|
|
6
|
+
import { inspect } from "node:util";
|
|
7
|
+
import { constants } from "node:os";
|
|
8
|
+
import { logs } from "@opentelemetry/api-logs";
|
|
9
|
+
// src/lifecycle.ts
|
|
10
|
+
const DEFAULT_TIMEOUT_MILLIS = 5e3;
|
|
11
|
+
const SENTRY_SEND_MILLIS = 2e3;
|
|
12
|
+
const INVOCATION_MARGIN_MILLIS = 500;
|
|
13
|
+
const SIGNALS = ["SIGTERM", "SIGINT"];
|
|
14
|
+
function bounded(work, ms, keepAlive) {
|
|
15
|
+
let timer;
|
|
16
|
+
const deadline = new Promise((resolve) => {
|
|
17
|
+
timer = setTimeout(resolve, ms, false);
|
|
18
|
+
if (!keepAlive) timer.unref();
|
|
19
|
+
});
|
|
20
|
+
const settled = Promise.resolve().then(work).then(() => true);
|
|
21
|
+
return Promise.race([settled, deadline]).then((inTime) => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
if (!inTime) report("warn", `flush or shutdown did not finish within ${String(ms)} ms`);
|
|
24
|
+
return inTime;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function timeoutOf(timeoutMillis) {
|
|
28
|
+
if (timeoutMillis !== void 0 && timeoutMillis > 0) return Math.min(timeoutMillis, 2147483647);
|
|
29
|
+
const deadline = getState().invocationDeadline;
|
|
30
|
+
return deadline === void 0 ? DEFAULT_TIMEOUT_MILLIS : Math.max(1, Math.min(DEFAULT_TIMEOUT_MILLIS, deadline - Date.now()));
|
|
31
|
+
}
|
|
32
|
+
function sdkShutdown() {
|
|
33
|
+
const state = getState();
|
|
34
|
+
const sdk = state.sdk;
|
|
35
|
+
if (sdk === void 0) return Promise.resolve();
|
|
36
|
+
return state.shutdownPromise ??= (async () => {
|
|
37
|
+
await state.flushInFlight;
|
|
38
|
+
try {
|
|
39
|
+
await sdk.shutdown();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
report("warn", "shutdown failed", error);
|
|
42
|
+
}
|
|
43
|
+
state.unsubscribeUndici?.();
|
|
44
|
+
})();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Records when the current AWS Lambda invocation must return, so flushes end in time.
|
|
48
|
+
*
|
|
49
|
+
* @param context - The invocation context; anything without `getRemainingTimeInMillis` is ignored.
|
|
50
|
+
*/
|
|
51
|
+
function recordInvocationDeadline(context) {
|
|
52
|
+
const remaining = context?.getRemainingTimeInMillis?.();
|
|
53
|
+
if (typeof remaining === "number") getState().invocationDeadline = Date.now() + remaining - INVOCATION_MARGIN_MILLIS;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Exports the spans, metrics and log records the SDK has buffered, without stopping it.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* Calls `forceFlush` on the global tracer, meter and logger providers. Before `init`, or with
|
|
60
|
+
* `OTEL_SDK_DISABLED=true`, it resolves at once; after {@link shutdown} it waits for the shutdown.
|
|
61
|
+
* Failures and timeouts are reported through the OpenTelemetry `diag` logger. Inside a
|
|
62
|
+
* {@link withFlush} invocation the default bound also ends before the invocation's deadline.
|
|
63
|
+
*
|
|
64
|
+
* @param timeoutMillis - Upper bound in milliseconds, 5000 by default; zero, negative and NaN
|
|
65
|
+
* mean the default.
|
|
66
|
+
* @returns A promise that resolves when every provider has flushed or the bound has passed. It
|
|
67
|
+
* never rejects.
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
async function flush(timeoutMillis) {
|
|
71
|
+
const state = getState();
|
|
72
|
+
if (state.sdk === void 0) return;
|
|
73
|
+
const ms = timeoutOf(timeoutMillis);
|
|
74
|
+
const pending = state.shutdownPromise;
|
|
75
|
+
if (pending !== void 0) {
|
|
76
|
+
await bounded(() => pending, ms, false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const tracer = trace.getTracerProvider();
|
|
80
|
+
const providers = [
|
|
81
|
+
tracer.getDelegate?.() ?? tracer,
|
|
82
|
+
metrics.getMeterProvider(),
|
|
83
|
+
logs.getLoggerProvider()
|
|
84
|
+
];
|
|
85
|
+
const work = Promise.all(providers.map(async (provider) => {
|
|
86
|
+
const p = provider;
|
|
87
|
+
if (typeof p.forceFlush !== "function") return;
|
|
88
|
+
try {
|
|
89
|
+
await p.forceFlush({ timeoutMillis: ms });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
report("warn", "flush failed", error);
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
state.flushInFlight = Promise.all([state.flushInFlight, work]);
|
|
95
|
+
await bounded(() => work, ms, false);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Flushes and stops the SDK started by `init`; telemetry recorded afterwards is dropped.
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* Later calls, and the automatic shutdown on `beforeExit`, SIGTERM, SIGINT and uncaught
|
|
102
|
+
* exceptions, share the first call's work. Scripts that end with `process.exit()`, and app signal
|
|
103
|
+
* handlers that exit, should await it first, because `process.exit()` skips `beforeExit`.
|
|
104
|
+
*
|
|
105
|
+
* @param timeoutMillis - Upper bound in milliseconds, 5000 by default; zero, negative and NaN
|
|
106
|
+
* mean the default.
|
|
107
|
+
* @returns A promise that resolves when the SDK has shut down or the bound has passed. It never
|
|
108
|
+
* rejects.
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* process.on('SIGTERM', async () => {
|
|
112
|
+
* await server.close();
|
|
113
|
+
* await shutdown();
|
|
114
|
+
* process.exit(0);
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
119
|
+
async function shutdown(timeoutMillis) {
|
|
120
|
+
if (getState().sdk === void 0) return;
|
|
121
|
+
await bounded(sdkShutdown, timeoutOf(timeoutMillis), false);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Wraps an async serverless handler so buffered telemetry is exported before each invocation
|
|
125
|
+
* returns.
|
|
126
|
+
*
|
|
127
|
+
* @remarks
|
|
128
|
+
* Awaits the handler, then {@link flush} in a `finally` block, so telemetry is also exported when
|
|
129
|
+
* the handler throws. Flushes during the invocation end 500 ms before its deadline
|
|
130
|
+
* (`context.getRemainingTimeInMillis()`). On its first call it installs console capture again if
|
|
131
|
+
* the AWS Lambda runtime replaced the console methods after the preload ran. Callback-style
|
|
132
|
+
* handlers are not supported.
|
|
133
|
+
*
|
|
134
|
+
* @typeParam E - The invocation event type.
|
|
135
|
+
* @typeParam C - The runtime context type, for example the AWS Lambda `Context`.
|
|
136
|
+
* @typeParam R - The value the handler's promise resolves to.
|
|
137
|
+
* @param handler - The async handler to wrap.
|
|
138
|
+
* @returns A handler with the same parameters and result.
|
|
139
|
+
* @example
|
|
140
|
+
* With `NODE_OPTIONS=--import @clickhouse/clickstack-node/register` set on the function:
|
|
141
|
+
* ```ts
|
|
142
|
+
* import { withFlush } from '@clickhouse/clickstack-node';
|
|
143
|
+
*
|
|
144
|
+
* export const handler = withFlush(async (event: { name?: string }) => {
|
|
145
|
+
* console.log('greeting', event.name);
|
|
146
|
+
* return { statusCode: 200, body: 'ok' };
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
* @public
|
|
150
|
+
*/
|
|
151
|
+
function withFlush(handler) {
|
|
152
|
+
let firstCall = true;
|
|
153
|
+
return async function flushingHandler(event, context) {
|
|
154
|
+
const state = getState();
|
|
155
|
+
try {
|
|
156
|
+
recordInvocationDeadline(context);
|
|
157
|
+
if (firstCall) {
|
|
158
|
+
firstCall = false;
|
|
159
|
+
const current = console.log;
|
|
160
|
+
if (state.repatchConsole !== void 0 && current !== state.consoleLog) {
|
|
161
|
+
state.repatchConsole();
|
|
162
|
+
state.consoleLog = console.log;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
report("warn", "could not prepare the invocation", error);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return await handler(event, context);
|
|
170
|
+
} finally {
|
|
171
|
+
await flush();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function writeFatal(error) {
|
|
176
|
+
try {
|
|
177
|
+
const body = typeof error === "string" ? error : inspect(error, {
|
|
178
|
+
customInspect: false,
|
|
179
|
+
depth: 5
|
|
180
|
+
});
|
|
181
|
+
process.getBuiltinModule("node:fs").writeSync(2, `${body}\n\nNode.js ${process.version}\n`);
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
function signalExitListeners() {
|
|
185
|
+
const v4 = globalThis[Symbol.for("signal-exit emitter")];
|
|
186
|
+
const v3 = process.__signal_exit_emitter__;
|
|
187
|
+
return [v4?.count, v3?.count].reduce((sum, count) => sum + (typeof count === "number" ? count : 0), 0);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Installs the `beforeExit`, SIGTERM, SIGINT and `uncaughtException` handlers, once per thread.
|
|
191
|
+
* Signal and crash handlers go on the main thread only: workers never receive signals, and a
|
|
192
|
+
* worker crash listener would hide the parent's `error` event.
|
|
193
|
+
*/
|
|
194
|
+
function installLifecycle() {
|
|
195
|
+
let terminating = false;
|
|
196
|
+
let terminatingSince = 0;
|
|
197
|
+
if (getStringFromEnv("AWS_LAMBDA_FUNCTION_NAME") === void 0) process.once("beforeExit", () => {
|
|
198
|
+
if (terminating) return;
|
|
199
|
+
bounded(sdkShutdown, DEFAULT_TIMEOUT_MILLIS, false).then((inTime) => {
|
|
200
|
+
if (!inTime) process.exit();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
if (!isMainThread) return;
|
|
204
|
+
const onSignal = (signal) => {
|
|
205
|
+
if (terminating) {
|
|
206
|
+
if (Date.now() - terminatingSince > 1e3) process.exit(128 + constants.signals[signal]);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (process.listeners(signal).filter((listener) => listener !== onSignal).length > signalExitListeners()) {
|
|
210
|
+
process.removeListener(signal, onSignal);
|
|
211
|
+
flush();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
terminating = true;
|
|
215
|
+
terminatingSince = Date.now();
|
|
216
|
+
bounded(sdkShutdown, DEFAULT_TIMEOUT_MILLIS, true).then(() => {
|
|
217
|
+
for (const name of SIGNALS) process.removeListener(name, onSignal);
|
|
218
|
+
try {
|
|
219
|
+
process.kill(process.pid, signal);
|
|
220
|
+
} catch {}
|
|
221
|
+
process.exit(128 + constants.signals[signal]);
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
for (const signal of SIGNALS) process.prependListener(signal, onSignal);
|
|
225
|
+
const onUncaughtException = (error) => {
|
|
226
|
+
try {
|
|
227
|
+
const listeners = process.listeners("uncaughtException");
|
|
228
|
+
const isSentry = (listener) => "_errorHandler" in listener && listener._errorHandler === true;
|
|
229
|
+
if (listeners.some((listener) => listener !== onUncaughtException && listener.name !== "domainUncaughtExceptionClear" && !isSentry(listener))) return;
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
if (terminating) return;
|
|
232
|
+
writeFatal(error);
|
|
233
|
+
terminating = true;
|
|
234
|
+
terminatingSince = Date.now();
|
|
235
|
+
const sentry = listeners.some(isSentry) ? new Promise((resolve) => setTimeout(resolve, SENTRY_SEND_MILLIS)) : void 0;
|
|
236
|
+
Promise.all([bounded(sdkShutdown, DEFAULT_TIMEOUT_MILLIS, true), sentry]).then(() => {
|
|
237
|
+
process.exit(1);
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
process.prependListener("uncaughtException", onUncaughtException);
|
|
244
|
+
}
|
|
245
|
+
export { flush, installLifecycle, recordInvocationDeadline, shutdown, withFlush };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const require_init = require("./init.cjs");
|
|
2
|
+
// src/register.ts
|
|
3
|
+
/**
|
|
4
|
+
* Zero-code entry: starts the SDK from environment variables only, as `init()` without options.
|
|
5
|
+
* Worker threads are skipped; call `init()` inside a worker to instrument it.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```sh
|
|
9
|
+
* NODE_OPTIONS="--import @clickhouse/clickstack-node/register" node app.mjs
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
if (require("node:worker_threads").isMainThread) require_init.init();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { init } from "./init.mjs";
|
|
2
|
+
import { isMainThread } from "node:worker_threads";
|
|
3
|
+
// src/register.ts
|
|
4
|
+
/**
|
|
5
|
+
* Zero-code entry: starts the SDK from environment variables only, as `init()` without options.
|
|
6
|
+
* Worker threads are skipped; call `init()` inside a worker to instrument it.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```sh
|
|
10
|
+
* NODE_OPTIONS="--import @clickhouse/clickstack-node/register" node app.mjs
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
if (isMainThread) init();
|
|
14
|
+
export {};
|
package/dist/semconv.cjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/semconv.ts
|
|
2
|
+
/** `http.request.body.size`: semantic-conventions v1.43.0, development. */
|
|
3
|
+
const ATTR_HTTP_REQUEST_BODY_SIZE = "http.request.body.size";
|
|
4
|
+
/** `http.response.body.size`: semantic-conventions v1.43.0, development. */
|
|
5
|
+
const ATTR_HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
|
|
6
|
+
/**
|
|
7
|
+
* `http.request.body.content`: development. Added on semantic-conventions `main`
|
|
8
|
+
* by PR #3521 (commit bcd7e291081f, 2026-08-14); not in a release as of v1.44.0.
|
|
9
|
+
*/
|
|
10
|
+
const ATTR_HTTP_REQUEST_BODY_CONTENT = "http.request.body.content";
|
|
11
|
+
/** `http.response.body.content`: same source as {@link ATTR_HTTP_REQUEST_BODY_CONTENT}. */
|
|
12
|
+
const ATTR_HTTP_RESPONSE_BODY_CONTENT = "http.response.body.content";
|
|
13
|
+
exports.ATTR_HTTP_REQUEST_BODY_CONTENT = ATTR_HTTP_REQUEST_BODY_CONTENT;
|
|
14
|
+
exports.ATTR_HTTP_REQUEST_BODY_SIZE = ATTR_HTTP_REQUEST_BODY_SIZE;
|
|
15
|
+
exports.ATTR_HTTP_RESPONSE_BODY_CONTENT = ATTR_HTTP_RESPONSE_BODY_CONTENT;
|
|
16
|
+
exports.ATTR_HTTP_RESPONSE_BODY_SIZE = ATTR_HTTP_RESPONSE_BODY_SIZE;
|