@clickhouse/clickstack-node 0.0.0-stage → 1.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.
@@ -0,0 +1,54 @@
1
+ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
2
+ // src/exception.ts
3
+ const MAX_CAUSES = 5;
4
+ /**
5
+ * Builds the `exception.*` attributes of the crash record that the logs SDK does not derive from
6
+ * the error itself. Attributes set here win over the SDK's.
7
+ *
8
+ * @remarks
9
+ * An error's `cause` chain, up to five deep, is appended to its stack as `Caused by:` sections, the
10
+ * layout of Java's `printStackTrace`, which ClickStack splits on. A thrown value that is not an
11
+ * error is recorded as JSON text, because the logs SDK records only strings and numbers. Stacks
12
+ * stay raw; ClickStack parses them.
13
+ *
14
+ * @param value - The uncaught exception.
15
+ * @returns The attributes; empty when the SDK's own are complete.
16
+ */
17
+ function exceptionAttributes(value) {
18
+ try {
19
+ if (typeof value === "string") return {};
20
+ if (!isErrorLike(value)) return { [_opentelemetry_semantic_conventions.ATTR_EXCEPTION_MESSAGE]: toText(value) };
21
+ const { cause } = value;
22
+ return cause === void 0 ? {} : { [_opentelemetry_semantic_conventions.ATTR_EXCEPTION_STACKTRACE]: stackWithCauses(value) };
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+ function isErrorLike(value) {
28
+ return typeof value === "object" && value !== null && ("message" in value || "stack" in value);
29
+ }
30
+ function stackWithCauses(error) {
31
+ let text = errorText(error);
32
+ const seen = /* @__PURE__ */ new Set([error]);
33
+ let { cause } = error;
34
+ for (let depth = 0; depth < MAX_CAUSES && cause !== void 0 && !seen.has(cause); depth++) {
35
+ seen.add(cause);
36
+ text += `\nCaused by: ${isErrorLike(cause) ? errorText(cause) : toText(cause)}`;
37
+ cause = isErrorLike(cause) ? cause.cause : void 0;
38
+ }
39
+ return text;
40
+ }
41
+ function errorText(error) {
42
+ const header = Error.prototype.toString.call(error);
43
+ const { stack } = error;
44
+ if (typeof stack !== "string") return header;
45
+ return stack.startsWith(header) ? stack : `${header}\n${stack}`;
46
+ }
47
+ function toText(value) {
48
+ try {
49
+ const json = JSON.stringify(value);
50
+ if (json !== void 0) return json;
51
+ } catch {}
52
+ return String(value);
53
+ }
54
+ exports.exceptionAttributes = exceptionAttributes;
@@ -0,0 +1,54 @@
1
+ import { ATTR_EXCEPTION_MESSAGE, ATTR_EXCEPTION_STACKTRACE } from "@opentelemetry/semantic-conventions";
2
+ // src/exception.ts
3
+ const MAX_CAUSES = 5;
4
+ /**
5
+ * Builds the `exception.*` attributes of the crash record that the logs SDK does not derive from
6
+ * the error itself. Attributes set here win over the SDK's.
7
+ *
8
+ * @remarks
9
+ * An error's `cause` chain, up to five deep, is appended to its stack as `Caused by:` sections, the
10
+ * layout of Java's `printStackTrace`, which ClickStack splits on. A thrown value that is not an
11
+ * error is recorded as JSON text, because the logs SDK records only strings and numbers. Stacks
12
+ * stay raw; ClickStack parses them.
13
+ *
14
+ * @param value - The uncaught exception.
15
+ * @returns The attributes; empty when the SDK's own are complete.
16
+ */
17
+ function exceptionAttributes(value) {
18
+ try {
19
+ if (typeof value === "string") return {};
20
+ if (!isErrorLike(value)) return { [ATTR_EXCEPTION_MESSAGE]: toText(value) };
21
+ const { cause } = value;
22
+ return cause === void 0 ? {} : { [ATTR_EXCEPTION_STACKTRACE]: stackWithCauses(value) };
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+ function isErrorLike(value) {
28
+ return typeof value === "object" && value !== null && ("message" in value || "stack" in value);
29
+ }
30
+ function stackWithCauses(error) {
31
+ let text = errorText(error);
32
+ const seen = /* @__PURE__ */ new Set([error]);
33
+ let { cause } = error;
34
+ for (let depth = 0; depth < MAX_CAUSES && cause !== void 0 && !seen.has(cause); depth++) {
35
+ seen.add(cause);
36
+ text += `\nCaused by: ${isErrorLike(cause) ? errorText(cause) : toText(cause)}`;
37
+ cause = isErrorLike(cause) ? cause.cause : void 0;
38
+ }
39
+ return text;
40
+ }
41
+ function errorText(error) {
42
+ const header = Error.prototype.toString.call(error);
43
+ const { stack } = error;
44
+ if (typeof stack !== "string") return header;
45
+ return stack.startsWith(header) ? stack : `${header}\n${stack}`;
46
+ }
47
+ function toText(value) {
48
+ try {
49
+ const json = JSON.stringify(value);
50
+ if (json !== void 0) return json;
51
+ } catch {}
52
+ return String(value);
53
+ }
54
+ export { exceptionAttributes };
package/dist/hook.cjs ADDED
@@ -0,0 +1,47 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ let node_module = require("node:module");
3
+ node_module = require_runtime.__toESM(node_module, 1);
4
+ let node_url = require("node:url");
5
+ // src/hook.ts
6
+ const restoreFormat = (specifier, context, nextResolve) => {
7
+ const isEntry = context.parentURL === void 0;
8
+ const result = nextResolve(specifier, context);
9
+ if (result.format !== "commonjs") return result;
10
+ if (result.url.startsWith("node:")) return {
11
+ ...result,
12
+ format: "builtin"
13
+ };
14
+ return isEntry && result.url.startsWith("file:") && !/\.[cm]?[jt]s$/.test(result.url) ? {
15
+ ...result,
16
+ format: void 0
17
+ } : result;
18
+ };
19
+ /**
20
+ * Installs the import-in-the-middle ESM loader hook in this thread.
21
+ *
22
+ * import-in-the-middle is loaded from `@opentelemetry/instrumentation`'s own location: only the
23
+ * copy next to it sees the `Hook()` calls instrumentations make. The package's own dependency on
24
+ * it is never imported; it raises that copy to the 3.5 line, whose behavior the repair above
25
+ * expects (`@opentelemetry/instrumentation` accepts any 3.x). Synchronous hooks
26
+ * (`module.registerHooks`) are used where import-in-the-middle supports them; `module.register`,
27
+ * which Node 26 deprecates (DEP0205), is the fallback for older Node versions and for runtimes
28
+ * that disable `require(esm)` (AWS Lambda).
29
+ *
30
+ * @returns `'sync'` for `module.registerHooks`, `'async'` for `module.register`.
31
+ * @throws If `@opentelemetry/instrumentation/hook.mjs` cannot be resolved or Node rejects the hook.
32
+ */
33
+ function installEsmHook() {
34
+ const hookFile = node_module.createRequire(require("url").pathToFileURL(__filename).href).resolve("@opentelemetry/instrumentation/hook.mjs");
35
+ let syncHooks;
36
+ try {
37
+ syncHooks = node_module.createRequire(hookFile)("import-in-the-middle/register-hooks.mjs");
38
+ } catch {}
39
+ if (syncHooks?.supportsSyncHooks() === true) {
40
+ syncHooks.register();
41
+ node_module.registerHooks({ resolve: restoreFormat });
42
+ return "sync";
43
+ }
44
+ node_module.register((0, node_url.pathToFileURL)(hookFile));
45
+ return "async";
46
+ }
47
+ exports.installEsmHook = installEsmHook;
package/dist/hook.mjs ADDED
@@ -0,0 +1,45 @@
1
+ import * as nodeModule from "node:module";
2
+ import { pathToFileURL } from "node:url";
3
+ // src/hook.ts
4
+ const restoreFormat = (specifier, context, nextResolve) => {
5
+ const isEntry = context.parentURL === void 0;
6
+ const result = nextResolve(specifier, context);
7
+ if (result.format !== "commonjs") return result;
8
+ if (result.url.startsWith("node:")) return {
9
+ ...result,
10
+ format: "builtin"
11
+ };
12
+ return isEntry && result.url.startsWith("file:") && !/\.[cm]?[jt]s$/.test(result.url) ? {
13
+ ...result,
14
+ format: void 0
15
+ } : result;
16
+ };
17
+ /**
18
+ * Installs the import-in-the-middle ESM loader hook in this thread.
19
+ *
20
+ * import-in-the-middle is loaded from `@opentelemetry/instrumentation`'s own location: only the
21
+ * copy next to it sees the `Hook()` calls instrumentations make. The package's own dependency on
22
+ * it is never imported; it raises that copy to the 3.5 line, whose behavior the repair above
23
+ * expects (`@opentelemetry/instrumentation` accepts any 3.x). Synchronous hooks
24
+ * (`module.registerHooks`) are used where import-in-the-middle supports them; `module.register`,
25
+ * which Node 26 deprecates (DEP0205), is the fallback for older Node versions and for runtimes
26
+ * that disable `require(esm)` (AWS Lambda).
27
+ *
28
+ * @returns `'sync'` for `module.registerHooks`, `'async'` for `module.register`.
29
+ * @throws If `@opentelemetry/instrumentation/hook.mjs` cannot be resolved or Node rejects the hook.
30
+ */
31
+ function installEsmHook() {
32
+ const hookFile = nodeModule.createRequire(import.meta.url).resolve("@opentelemetry/instrumentation/hook.mjs");
33
+ let syncHooks;
34
+ try {
35
+ syncHooks = nodeModule.createRequire(hookFile)("import-in-the-middle/register-hooks.mjs");
36
+ } catch {}
37
+ if (syncHooks?.supportsSyncHooks() === true) {
38
+ syncHooks.register();
39
+ nodeModule.registerHooks({ resolve: restoreFormat });
40
+ return "sync";
41
+ }
42
+ nodeModule.register(pathToFileURL(hookFile));
43
+ return "async";
44
+ }
45
+ export { installEsmHook };
package/dist/index.cjs ADDED
@@ -0,0 +1,7 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_lifecycle = require("./lifecycle.cjs");
3
+ const require_init = require("./init.cjs");
4
+ exports.flush = require_lifecycle.flush;
5
+ exports.init = require_init.init;
6
+ exports.shutdown = require_lifecycle.shutdown;
7
+ exports.withFlush = require_lifecycle.withFlush;
@@ -0,0 +1,4 @@
1
+ import { ClickStackNodeOptions } from "./config.cjs";
2
+ import { init } from "./init.cjs";
3
+ import { flush, shutdown, withFlush } from "./lifecycle.cjs";
4
+ export { type ClickStackNodeOptions, flush, init, shutdown, withFlush };
@@ -0,0 +1,4 @@
1
+ import { ClickStackNodeOptions } from "./config.mjs";
2
+ import { init } from "./init.mjs";
3
+ import { flush, shutdown, withFlush } from "./lifecycle.mjs";
4
+ export { type ClickStackNodeOptions, flush, init, shutdown, withFlush };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { flush, shutdown, withFlush } from "./lifecycle.mjs";
2
+ import { init } from "./init.mjs";
3
+ export { flush, init, shutdown, withFlush };
package/dist/init.cjs ADDED
@@ -0,0 +1,139 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_state = require("./state.cjs");
3
+ const require_env = require("./env.cjs");
4
+ const require_lifecycle = require("./lifecycle.cjs");
5
+ const require_compose = require("./compose.cjs");
6
+ const require_config = require("./config.cjs");
7
+ const require_exception = require("./exception.cjs");
8
+ const require_hook = require("./hook.cjs");
9
+ const require_version = require("./version.cjs");
10
+ let node_worker_threads = require("node:worker_threads");
11
+ node_worker_threads = require_runtime.__toESM(node_worker_threads, 1);
12
+ let _opentelemetry_api = require("@opentelemetry/api");
13
+ let _opentelemetry_auto_instrumentations_node = require("@opentelemetry/auto-instrumentations-node");
14
+ let _opentelemetry_core = require("@opentelemetry/core");
15
+ let _opentelemetry_instrumentation_console = require("@opentelemetry/instrumentation-console");
16
+ let _opentelemetry_resources = require("@opentelemetry/resources");
17
+ let _opentelemetry_sdk_node = require("@opentelemetry/sdk-node");
18
+ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
19
+ // src/init.ts
20
+ /**
21
+ * Starts the OpenTelemetry SDK with ClickStack defaults: traces, metrics and logs over OTLP,
22
+ * `console` capture, and a flush on shutdown, termination signals and crashes.
23
+ *
24
+ * Options with an environment variable are written to `process.env` only when that variable is
25
+ * unset, so a variable set by an operator always wins. A second call, or a call after another
26
+ * OpenTelemetry SDK registered a tracer provider, logs a warning and does nothing. With
27
+ * `OTEL_SDK_DISABLED=true` it does nothing at all. It never throws.
28
+ *
29
+ * Call it before the app loads the modules to instrument. In an ES module entry, static imports
30
+ * run first, so start the process with the register entry instead:
31
+ * `node --import @clickhouse/clickstack-node/register app.mjs` (it also works with `--require` and
32
+ * in `NODE_OPTIONS`); that entry calls `init()` without options.
33
+ *
34
+ * @param options - Code options; each loses to its environment variable.
35
+ * @example
36
+ * At the top of a CommonJS entry, before anything else is required:
37
+ * ```js
38
+ * const { init } = require('@clickhouse/clickstack-node');
39
+ *
40
+ * init({ serviceName: 'checkout', endpoint: 'https://otel.example.com:4318', apiKey: process.env.KEY });
41
+ *
42
+ * const express = require('express');
43
+ * ```
44
+ * @public
45
+ */
46
+ function init(options = {}) {
47
+ try {
48
+ start(options);
49
+ } catch (err) {
50
+ require_state.getState().unsubscribeUndici?.();
51
+ require_env.report("error", "init() failed; the application is not instrumented", err);
52
+ }
53
+ }
54
+ function start(options) {
55
+ if (node_worker_threads.isInternalThread) return;
56
+ if ((0, _opentelemetry_core.getBooleanFromEnv)("OTEL_SDK_DISABLED")) return;
57
+ const state = require_state.getState();
58
+ const registry = require_env.apiRegistry();
59
+ if (state.initialized === true || registry?.trace !== void 0) {
60
+ require_env.report("warn", "an OpenTelemetry SDK is already started; init() does nothing");
61
+ return;
62
+ }
63
+ state.initialized = true;
64
+ require_config.applyOptions(options);
65
+ try {
66
+ state.esmHook = require_hook.installEsmHook();
67
+ } catch (err) {
68
+ state.esmHook = "failed";
69
+ require_env.warnOnce("esm-hook", "ESM loader hook not installed; ES modules are not instrumented", err);
70
+ }
71
+ const bodies = (0, _opentelemetry_core.getBooleanFromEnv)("CLICKSTACK_CAPTURE_HTTP_BODY");
72
+ const { map, unsubscribe } = require_compose.composeInstrumentationConfig(options.instrumentations, bodies);
73
+ if (unsubscribe !== void 0) state.unsubscribeUndici = unsubscribe;
74
+ const runtimeNode = "@opentelemetry/instrumentation-runtime-node";
75
+ const runtime = map[runtimeNode] ?? {};
76
+ map[runtimeNode] = {
77
+ captureUncaughtException: true,
78
+ ...runtime,
79
+ applyCustomExceptionAttributes: (error, eventType) => ({
80
+ ...require_exception.exceptionAttributes(error),
81
+ ...runtime.applyCustomExceptionAttributes?.(error, eventType)
82
+ })
83
+ };
84
+ const disabled = (0, _opentelemetry_core.getStringListFromEnv)("OTEL_NODE_DISABLED_INSTRUMENTATIONS");
85
+ const enabled = (0, _opentelemetry_core.getStringListFromEnv)("OTEL_NODE_ENABLED_INSTRUMENTATIONS");
86
+ const router = "@opentelemetry/instrumentation-router";
87
+ if (enabled === void 0) map[router] = {
88
+ enabled: false,
89
+ ...map[router]
90
+ };
91
+ const consoleListed = disabled?.includes("console") !== true && (enabled?.includes("console") ?? true);
92
+ const consoleExporter = [
93
+ "OTEL_TRACES_EXPORTER",
94
+ "OTEL_METRICS_EXPORTER",
95
+ "OTEL_LOGS_EXPORTER"
96
+ ].find((name) => (0, _opentelemetry_core.getStringListFromEnv)(name)?.includes("console"));
97
+ if (consoleListed && consoleExporter !== void 0) require_env.warnOnce("console-exporter", `console capture is off because ${consoleExporter} includes console`);
98
+ const consoleCapture = consoleListed && consoleExporter === void 0;
99
+ const instrumentations = [
100
+ ...require_env.withStartupLogger(() => (0, _opentelemetry_auto_instrumentations_node.getNodeAutoInstrumentations)(map)),
101
+ ...consoleCapture ? [new _opentelemetry_instrumentation_console.ConsoleInstrumentation()] : [],
102
+ ...options.additionalInstrumentations ?? []
103
+ ];
104
+ const build = () => new _opentelemetry_sdk_node.NodeSDK({
105
+ resource: (0, _opentelemetry_resources.defaultResource)().merge((0, _opentelemetry_resources.resourceFromAttributes)({
106
+ ...options.environment ? { [_opentelemetry_semantic_conventions.ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: options.environment } : {},
107
+ [_opentelemetry_semantic_conventions.ATTR_TELEMETRY_DISTRO_NAME]: "clickstack-node",
108
+ [_opentelemetry_semantic_conventions.ATTR_TELEMETRY_DISTRO_VERSION]: require_version.VERSION
109
+ })),
110
+ resourceDetectors: withDefault("OTEL_NODE_RESOURCE_DETECTORS", "container,host,os,process,serviceinstance,env", () => require_env.withStartupLogger(_opentelemetry_auto_instrumentations_node.getResourceDetectors)),
111
+ instrumentations
112
+ });
113
+ const sdk = registry?.diag === void 0 ? withDefault("OTEL_LOG_LEVEL", "warn", build) : build();
114
+ sdk.start();
115
+ state.sdk = sdk;
116
+ if (consoleCapture) {
117
+ state.consoleLog = console.log;
118
+ state.repatchConsole = () => {
119
+ new _opentelemetry_instrumentation_console.ConsoleInstrumentation();
120
+ };
121
+ }
122
+ require_lifecycle.installLifecycle();
123
+ const provider = _opentelemetry_api.trace.getTracerProvider();
124
+ const bounded = {
125
+ getTracer: (...args) => provider.getTracer(...args),
126
+ forceFlush: () => require_lifecycle.flush()
127
+ };
128
+ for (const instrumentation of instrumentations) if (instrumentation.instrumentationName === "@opentelemetry/instrumentation-aws-lambda") instrumentation.setTracerProvider(bounded);
129
+ }
130
+ function withDefault(name, value, call) {
131
+ if ((0, _opentelemetry_core.getStringFromEnv)(name) !== void 0) return call();
132
+ process.env[name] = value;
133
+ try {
134
+ return call();
135
+ } finally {
136
+ Reflect.deleteProperty(process.env, name);
137
+ }
138
+ }
139
+ exports.init = init;
@@ -0,0 +1,28 @@
1
+ import { ClickStackNodeOptions } from "./config.cjs";
2
+ /**
3
+ * Starts the OpenTelemetry SDK with ClickStack defaults: traces, metrics and logs over OTLP,
4
+ * `console` capture, and a flush on shutdown, termination signals and crashes.
5
+ *
6
+ * Options with an environment variable are written to `process.env` only when that variable is
7
+ * unset, so a variable set by an operator always wins. A second call, or a call after another
8
+ * OpenTelemetry SDK registered a tracer provider, logs a warning and does nothing. With
9
+ * `OTEL_SDK_DISABLED=true` it does nothing at all. It never throws.
10
+ *
11
+ * Call it before the app loads the modules to instrument. In an ES module entry, static imports
12
+ * run first, so start the process with the register entry instead:
13
+ * `node --import @clickhouse/clickstack-node/register app.mjs` (it also works with `--require` and
14
+ * in `NODE_OPTIONS`); that entry calls `init()` without options.
15
+ *
16
+ * @param options - Code options; each loses to its environment variable.
17
+ * @example
18
+ * At the top of a CommonJS entry, before anything else is required:
19
+ * ```js
20
+ * const { init } = require('@clickhouse/clickstack-node');
21
+ *
22
+ * init({ serviceName: 'checkout', endpoint: 'https://otel.example.com:4318', apiKey: process.env.KEY });
23
+ *
24
+ * const express = require('express');
25
+ * ```
26
+ * @public
27
+ */
28
+ export declare function init(options?: ClickStackNodeOptions): void;
@@ -0,0 +1,28 @@
1
+ import { ClickStackNodeOptions } from "./config.mjs";
2
+ /**
3
+ * Starts the OpenTelemetry SDK with ClickStack defaults: traces, metrics and logs over OTLP,
4
+ * `console` capture, and a flush on shutdown, termination signals and crashes.
5
+ *
6
+ * Options with an environment variable are written to `process.env` only when that variable is
7
+ * unset, so a variable set by an operator always wins. A second call, or a call after another
8
+ * OpenTelemetry SDK registered a tracer provider, logs a warning and does nothing. With
9
+ * `OTEL_SDK_DISABLED=true` it does nothing at all. It never throws.
10
+ *
11
+ * Call it before the app loads the modules to instrument. In an ES module entry, static imports
12
+ * run first, so start the process with the register entry instead:
13
+ * `node --import @clickhouse/clickstack-node/register app.mjs` (it also works with `--require` and
14
+ * in `NODE_OPTIONS`); that entry calls `init()` without options.
15
+ *
16
+ * @param options - Code options; each loses to its environment variable.
17
+ * @example
18
+ * At the top of a CommonJS entry, before anything else is required:
19
+ * ```js
20
+ * const { init } = require('@clickhouse/clickstack-node');
21
+ *
22
+ * init({ serviceName: 'checkout', endpoint: 'https://otel.example.com:4318', apiKey: process.env.KEY });
23
+ *
24
+ * const express = require('express');
25
+ * ```
26
+ * @public
27
+ */
28
+ export declare function init(options?: ClickStackNodeOptions): void;
package/dist/init.mjs ADDED
@@ -0,0 +1,137 @@
1
+ import { getState } from "./state.mjs";
2
+ import { apiRegistry, report, warnOnce, withStartupLogger } from "./env.mjs";
3
+ import { flush, installLifecycle } from "./lifecycle.mjs";
4
+ import { composeInstrumentationConfig } from "./compose.mjs";
5
+ import { applyOptions } from "./config.mjs";
6
+ import { exceptionAttributes } from "./exception.mjs";
7
+ import { installEsmHook } from "./hook.mjs";
8
+ import { VERSION } from "./version.mjs";
9
+ import * as workerThreads from "node:worker_threads";
10
+ import { trace } from "@opentelemetry/api";
11
+ import { getNodeAutoInstrumentations, getResourceDetectors } from "@opentelemetry/auto-instrumentations-node";
12
+ import { getBooleanFromEnv, getStringFromEnv, getStringListFromEnv } from "@opentelemetry/core";
13
+ import { ConsoleInstrumentation } from "@opentelemetry/instrumentation-console";
14
+ import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
15
+ import { NodeSDK } from "@opentelemetry/sdk-node";
16
+ import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME, ATTR_TELEMETRY_DISTRO_NAME, ATTR_TELEMETRY_DISTRO_VERSION } from "@opentelemetry/semantic-conventions";
17
+ // src/init.ts
18
+ /**
19
+ * Starts the OpenTelemetry SDK with ClickStack defaults: traces, metrics and logs over OTLP,
20
+ * `console` capture, and a flush on shutdown, termination signals and crashes.
21
+ *
22
+ * Options with an environment variable are written to `process.env` only when that variable is
23
+ * unset, so a variable set by an operator always wins. A second call, or a call after another
24
+ * OpenTelemetry SDK registered a tracer provider, logs a warning and does nothing. With
25
+ * `OTEL_SDK_DISABLED=true` it does nothing at all. It never throws.
26
+ *
27
+ * Call it before the app loads the modules to instrument. In an ES module entry, static imports
28
+ * run first, so start the process with the register entry instead:
29
+ * `node --import @clickhouse/clickstack-node/register app.mjs` (it also works with `--require` and
30
+ * in `NODE_OPTIONS`); that entry calls `init()` without options.
31
+ *
32
+ * @param options - Code options; each loses to its environment variable.
33
+ * @example
34
+ * At the top of a CommonJS entry, before anything else is required:
35
+ * ```js
36
+ * const { init } = require('@clickhouse/clickstack-node');
37
+ *
38
+ * init({ serviceName: 'checkout', endpoint: 'https://otel.example.com:4318', apiKey: process.env.KEY });
39
+ *
40
+ * const express = require('express');
41
+ * ```
42
+ * @public
43
+ */
44
+ function init(options = {}) {
45
+ try {
46
+ start(options);
47
+ } catch (err) {
48
+ getState().unsubscribeUndici?.();
49
+ report("error", "init() failed; the application is not instrumented", err);
50
+ }
51
+ }
52
+ function start(options) {
53
+ if (workerThreads.isInternalThread) return;
54
+ if (getBooleanFromEnv("OTEL_SDK_DISABLED")) return;
55
+ const state = getState();
56
+ const registry = apiRegistry();
57
+ if (state.initialized === true || registry?.trace !== void 0) {
58
+ report("warn", "an OpenTelemetry SDK is already started; init() does nothing");
59
+ return;
60
+ }
61
+ state.initialized = true;
62
+ applyOptions(options);
63
+ try {
64
+ state.esmHook = installEsmHook();
65
+ } catch (err) {
66
+ state.esmHook = "failed";
67
+ warnOnce("esm-hook", "ESM loader hook not installed; ES modules are not instrumented", err);
68
+ }
69
+ const bodies = getBooleanFromEnv("CLICKSTACK_CAPTURE_HTTP_BODY");
70
+ const { map, unsubscribe } = composeInstrumentationConfig(options.instrumentations, bodies);
71
+ if (unsubscribe !== void 0) state.unsubscribeUndici = unsubscribe;
72
+ const runtimeNode = "@opentelemetry/instrumentation-runtime-node";
73
+ const runtime = map[runtimeNode] ?? {};
74
+ map[runtimeNode] = {
75
+ captureUncaughtException: true,
76
+ ...runtime,
77
+ applyCustomExceptionAttributes: (error, eventType) => ({
78
+ ...exceptionAttributes(error),
79
+ ...runtime.applyCustomExceptionAttributes?.(error, eventType)
80
+ })
81
+ };
82
+ const disabled = getStringListFromEnv("OTEL_NODE_DISABLED_INSTRUMENTATIONS");
83
+ const enabled = getStringListFromEnv("OTEL_NODE_ENABLED_INSTRUMENTATIONS");
84
+ const router = "@opentelemetry/instrumentation-router";
85
+ if (enabled === void 0) map[router] = {
86
+ enabled: false,
87
+ ...map[router]
88
+ };
89
+ const consoleListed = disabled?.includes("console") !== true && (enabled?.includes("console") ?? true);
90
+ const consoleExporter = [
91
+ "OTEL_TRACES_EXPORTER",
92
+ "OTEL_METRICS_EXPORTER",
93
+ "OTEL_LOGS_EXPORTER"
94
+ ].find((name) => getStringListFromEnv(name)?.includes("console"));
95
+ if (consoleListed && consoleExporter !== void 0) warnOnce("console-exporter", `console capture is off because ${consoleExporter} includes console`);
96
+ const consoleCapture = consoleListed && consoleExporter === void 0;
97
+ const instrumentations = [
98
+ ...withStartupLogger(() => getNodeAutoInstrumentations(map)),
99
+ ...consoleCapture ? [new ConsoleInstrumentation()] : [],
100
+ ...options.additionalInstrumentations ?? []
101
+ ];
102
+ const build = () => new NodeSDK({
103
+ resource: defaultResource().merge(resourceFromAttributes({
104
+ ...options.environment ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: options.environment } : {},
105
+ [ATTR_TELEMETRY_DISTRO_NAME]: "clickstack-node",
106
+ [ATTR_TELEMETRY_DISTRO_VERSION]: VERSION
107
+ })),
108
+ resourceDetectors: withDefault("OTEL_NODE_RESOURCE_DETECTORS", "container,host,os,process,serviceinstance,env", () => withStartupLogger(getResourceDetectors)),
109
+ instrumentations
110
+ });
111
+ const sdk = registry?.diag === void 0 ? withDefault("OTEL_LOG_LEVEL", "warn", build) : build();
112
+ sdk.start();
113
+ state.sdk = sdk;
114
+ if (consoleCapture) {
115
+ state.consoleLog = console.log;
116
+ state.repatchConsole = () => {
117
+ new ConsoleInstrumentation();
118
+ };
119
+ }
120
+ installLifecycle();
121
+ const provider = trace.getTracerProvider();
122
+ const bounded = {
123
+ getTracer: (...args) => provider.getTracer(...args),
124
+ forceFlush: () => flush()
125
+ };
126
+ for (const instrumentation of instrumentations) if (instrumentation.instrumentationName === "@opentelemetry/instrumentation-aws-lambda") instrumentation.setTracerProvider(bounded);
127
+ }
128
+ function withDefault(name, value, call) {
129
+ if (getStringFromEnv(name) !== void 0) return call();
130
+ process.env[name] = value;
131
+ try {
132
+ return call();
133
+ } finally {
134
+ Reflect.deleteProperty(process.env, name);
135
+ }
136
+ }
137
+ export { init };