@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,87 @@
1
+ import { warnOnce } from "../env.mjs";
2
+ import { BodyCapture, headerMap, reportFailure, responseHasNoBody } from "./rules.mjs";
3
+ import { subscribe, unsubscribe } from "node:diagnostics_channel";
4
+ // src/body/undici.ts
5
+ const side = (headers, contentLength) => ({
6
+ capture: new BodyCapture(),
7
+ meta: {
8
+ contentType: headers.get("content-type"),
9
+ contentEncoding: headers.get("content-encoding"),
10
+ contentLength
11
+ }
12
+ });
13
+ /**
14
+ * Tells whether the undici that Node bundles publishes the body diagnostics channels.
15
+ *
16
+ * @returns `true` from undici 7.11, which Node bundles from 24.4.
17
+ */
18
+ function hasBodyChannels() {
19
+ const [major = 0, minor = 0] = (process.versions["undici"] ?? "0").split(".").map(Number);
20
+ return major > 7 || major === 7 && minor >= 11;
21
+ }
22
+ /**
23
+ * Creates the fetch body capture from the undici body diagnostics channels, which Node bundles
24
+ * from 24.4 (undici 7.11). Call it before `instrumentation-undici` is constructed: that
25
+ * instrumentation ends the span in its own trailers and error subscribers, and subscribers run
26
+ * in subscription order, so ours must come first to write while the span is open.
27
+ *
28
+ * @returns The capture, or `undefined` (with one warning) when this Node has no body channels.
29
+ */
30
+ function createUndiciBodyCapture() {
31
+ if (!hasBodyChannels()) {
32
+ warnOnce("undici-bodies", `fetch bodies are captured from Node.js 24.4; ${process.version} has no undici body channels`);
33
+ return;
34
+ }
35
+ const exchanges = /* @__PURE__ */ new WeakMap();
36
+ const listeners = [];
37
+ const listen = (name, handle) => {
38
+ const listener = (message) => {
39
+ try {
40
+ const { request } = message;
41
+ handle(request, message);
42
+ } catch (err) {
43
+ reportFailure(err);
44
+ }
45
+ };
46
+ subscribe(name, listener);
47
+ listeners.push([name, listener]);
48
+ };
49
+ const chunk = (direction) => (request, message) => {
50
+ exchanges.get(request)?.[direction]?.capture.add(message.chunk);
51
+ };
52
+ const finish = (cut) => (request) => {
53
+ const ex = exchanges.get(request);
54
+ exchanges.delete(request);
55
+ if (!ex?.span.isRecording()) return;
56
+ for (const direction of ["request", "response"]) {
57
+ const s = ex[direction];
58
+ if (s !== void 0) ex.span.setAttributes(s.capture.attributes(direction, {
59
+ ...s.meta,
60
+ complete: !cut
61
+ }));
62
+ }
63
+ };
64
+ listen("undici:request:bodyChunkSent", chunk("request"));
65
+ listen("undici:request:bodyChunkReceived", chunk("response"));
66
+ listen("undici:request:trailers", finish(false));
67
+ listen("undici:request:error", finish(true));
68
+ return {
69
+ requestHook(span, request) {
70
+ if (!span.isRecording()) return;
71
+ exchanges.set(request, {
72
+ span,
73
+ request: side(headerMap(request.headers), request.contentLength)
74
+ });
75
+ },
76
+ responseHook(_span, { request, response }) {
77
+ const ex = exchanges.get(request);
78
+ if (ex === void 0 || responseHasNoBody(request.method, response.statusCode)) return;
79
+ const headers = headerMap(response.headers);
80
+ ex.response = side(headers, headers.get("content-length"));
81
+ },
82
+ unsubscribe() {
83
+ for (const [name, listener] of listeners) unsubscribe(name, listener);
84
+ }
85
+ };
86
+ }
87
+ export { createUndiciBodyCapture, hasBodyChannels };
@@ -0,0 +1,107 @@
1
+ const require_env = require("./env.cjs");
2
+ const require_rules = require("./body/rules.cjs");
3
+ const require_http = require("./body/http.cjs");
4
+ const require_undici = require("./body/undici.cjs");
5
+ const require_lifecycle = require("./lifecycle.cjs");
6
+ let _opentelemetry_core = require("@opentelemetry/core");
7
+ // src/compose.ts
8
+ const HTTP = "@opentelemetry/instrumentation-http";
9
+ const UNDICI = "@opentelemetry/instrumentation-undici";
10
+ const LAMBDA = "@opentelemetry/instrumentation-aws-lambda";
11
+ const HEADERS_ENV = "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_";
12
+ const SANITIZE_DEFAULT = [
13
+ "authorization",
14
+ "cookie",
15
+ "set-cookie",
16
+ "proxy-authorization",
17
+ "x-api-key"
18
+ ];
19
+ function chain(ours, user) {
20
+ return function(...args) {
21
+ try {
22
+ ours(...args);
23
+ } catch (err) {
24
+ require_rules.reportFailure(err);
25
+ }
26
+ user?.apply(this, args);
27
+ };
28
+ }
29
+ /**
30
+ * Returns a copy of the user's instrumentation config with the header lists from
31
+ * `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_*` and, when body capture is on, our body hooks
32
+ * merged into the http and undici entries, and a hook that records each AWS Lambda invocation's
33
+ * deadline. The bundle passes each entry to the instrumentation
34
+ * constructor as is, so this is the only place to merge.
35
+ *
36
+ * @param user - The `instrumentations` option.
37
+ * @param bodies - Whether body capture is on.
38
+ * @returns The composed map and the fetch body unsubscribe function, if any.
39
+ */
40
+ function composeInstrumentationConfig(user, bodies) {
41
+ const sanitize = new Set(((0, _opentelemetry_core.getStringListFromEnv)(`${HEADERS_ENV}SANITIZE_FIELDS`) ?? SANITIZE_DEFAULT).map((n) => n.toLowerCase()));
42
+ const names = (list, code) => {
43
+ const all = [...(0, _opentelemetry_core.getStringListFromEnv)(HEADERS_ENV + list) ?? [], ...code ?? []];
44
+ const kept = /* @__PURE__ */ new Set();
45
+ for (const name of all.map((n) => n.toLowerCase())) if (!sanitize.has(name)) kept.add(name);
46
+ else require_env.warnOnce(`sanitized:${name}`, `header "${name}" is on the sanitize list and is not captured`);
47
+ return [...kept];
48
+ };
49
+ const http = user?.[HTTP] ?? {};
50
+ const hh = http.headersToSpanAttributes;
51
+ const httpEntry = {
52
+ ...http,
53
+ headersToSpanAttributes: {
54
+ server: {
55
+ requestHeaders: names("SERVER_REQUEST", hh?.server?.requestHeaders),
56
+ responseHeaders: names("SERVER_RESPONSE", hh?.server?.responseHeaders)
57
+ },
58
+ client: {
59
+ requestHeaders: names("CLIENT_REQUEST", hh?.client?.requestHeaders),
60
+ responseHeaders: names("CLIENT_RESPONSE", hh?.client?.responseHeaders)
61
+ }
62
+ }
63
+ };
64
+ const undici = user?.[UNDICI] ?? {};
65
+ const uh = undici.headersToSpanAttributes;
66
+ const requestHeaders = names("CLIENT_REQUEST", uh?.requestHeaders);
67
+ const responseHeaders = names("CLIENT_RESPONSE", uh?.responseHeaders);
68
+ const undiciEntry = {
69
+ ...undici,
70
+ headersToSpanAttributes: {
71
+ ...requestHeaders.length > 0 ? { requestHeaders } : {},
72
+ ...responseHeaders.length > 0 ? { responseHeaders } : {}
73
+ }
74
+ };
75
+ let unsubscribe;
76
+ if (bodies) {
77
+ const hooks = require_http.createHttpBodyHooks();
78
+ httpEntry.requestHook = chain(hooks.requestHook, http.requestHook);
79
+ httpEntry.responseHook = chain(hooks.responseHook, http.responseHook);
80
+ httpEntry.applyCustomAttributesOnSpan = chain((span) => {
81
+ hooks.finalize(span);
82
+ }, http.applyCustomAttributesOnSpan);
83
+ const fetchBodies = require_undici.createUndiciBodyCapture();
84
+ if (fetchBodies !== void 0) {
85
+ undiciEntry.requestHook = chain(fetchBodies.requestHook, undici.requestHook);
86
+ undiciEntry.responseHook = chain(fetchBodies.responseHook, undici.responseHook);
87
+ unsubscribe = fetchBodies.unsubscribe;
88
+ }
89
+ }
90
+ const lambda = user?.[LAMBDA] ?? {};
91
+ const lambdaEntry = {
92
+ ...lambda,
93
+ requestHook: chain((_span, { context }) => {
94
+ require_lifecycle.recordInvocationDeadline(context);
95
+ }, lambda.requestHook)
96
+ };
97
+ return {
98
+ map: {
99
+ ...user,
100
+ [HTTP]: httpEntry,
101
+ [UNDICI]: undiciEntry,
102
+ [LAMBDA]: lambdaEntry
103
+ },
104
+ unsubscribe
105
+ };
106
+ }
107
+ exports.composeInstrumentationConfig = composeInstrumentationConfig;
@@ -0,0 +1,107 @@
1
+ import { warnOnce } from "./env.mjs";
2
+ import { reportFailure } from "./body/rules.mjs";
3
+ import { createHttpBodyHooks } from "./body/http.mjs";
4
+ import { createUndiciBodyCapture } from "./body/undici.mjs";
5
+ import { recordInvocationDeadline } from "./lifecycle.mjs";
6
+ import { getStringListFromEnv } from "@opentelemetry/core";
7
+ // src/compose.ts
8
+ const HTTP = "@opentelemetry/instrumentation-http";
9
+ const UNDICI = "@opentelemetry/instrumentation-undici";
10
+ const LAMBDA = "@opentelemetry/instrumentation-aws-lambda";
11
+ const HEADERS_ENV = "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_";
12
+ const SANITIZE_DEFAULT = [
13
+ "authorization",
14
+ "cookie",
15
+ "set-cookie",
16
+ "proxy-authorization",
17
+ "x-api-key"
18
+ ];
19
+ function chain(ours, user) {
20
+ return function(...args) {
21
+ try {
22
+ ours(...args);
23
+ } catch (err) {
24
+ reportFailure(err);
25
+ }
26
+ user?.apply(this, args);
27
+ };
28
+ }
29
+ /**
30
+ * Returns a copy of the user's instrumentation config with the header lists from
31
+ * `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_*` and, when body capture is on, our body hooks
32
+ * merged into the http and undici entries, and a hook that records each AWS Lambda invocation's
33
+ * deadline. The bundle passes each entry to the instrumentation
34
+ * constructor as is, so this is the only place to merge.
35
+ *
36
+ * @param user - The `instrumentations` option.
37
+ * @param bodies - Whether body capture is on.
38
+ * @returns The composed map and the fetch body unsubscribe function, if any.
39
+ */
40
+ function composeInstrumentationConfig(user, bodies) {
41
+ const sanitize = new Set((getStringListFromEnv(`${HEADERS_ENV}SANITIZE_FIELDS`) ?? SANITIZE_DEFAULT).map((n) => n.toLowerCase()));
42
+ const names = (list, code) => {
43
+ const all = [...getStringListFromEnv(HEADERS_ENV + list) ?? [], ...code ?? []];
44
+ const kept = /* @__PURE__ */ new Set();
45
+ for (const name of all.map((n) => n.toLowerCase())) if (!sanitize.has(name)) kept.add(name);
46
+ else warnOnce(`sanitized:${name}`, `header "${name}" is on the sanitize list and is not captured`);
47
+ return [...kept];
48
+ };
49
+ const http = user?.[HTTP] ?? {};
50
+ const hh = http.headersToSpanAttributes;
51
+ const httpEntry = {
52
+ ...http,
53
+ headersToSpanAttributes: {
54
+ server: {
55
+ requestHeaders: names("SERVER_REQUEST", hh?.server?.requestHeaders),
56
+ responseHeaders: names("SERVER_RESPONSE", hh?.server?.responseHeaders)
57
+ },
58
+ client: {
59
+ requestHeaders: names("CLIENT_REQUEST", hh?.client?.requestHeaders),
60
+ responseHeaders: names("CLIENT_RESPONSE", hh?.client?.responseHeaders)
61
+ }
62
+ }
63
+ };
64
+ const undici = user?.[UNDICI] ?? {};
65
+ const uh = undici.headersToSpanAttributes;
66
+ const requestHeaders = names("CLIENT_REQUEST", uh?.requestHeaders);
67
+ const responseHeaders = names("CLIENT_RESPONSE", uh?.responseHeaders);
68
+ const undiciEntry = {
69
+ ...undici,
70
+ headersToSpanAttributes: {
71
+ ...requestHeaders.length > 0 ? { requestHeaders } : {},
72
+ ...responseHeaders.length > 0 ? { responseHeaders } : {}
73
+ }
74
+ };
75
+ let unsubscribe;
76
+ if (bodies) {
77
+ const hooks = createHttpBodyHooks();
78
+ httpEntry.requestHook = chain(hooks.requestHook, http.requestHook);
79
+ httpEntry.responseHook = chain(hooks.responseHook, http.responseHook);
80
+ httpEntry.applyCustomAttributesOnSpan = chain((span) => {
81
+ hooks.finalize(span);
82
+ }, http.applyCustomAttributesOnSpan);
83
+ const fetchBodies = createUndiciBodyCapture();
84
+ if (fetchBodies !== void 0) {
85
+ undiciEntry.requestHook = chain(fetchBodies.requestHook, undici.requestHook);
86
+ undiciEntry.responseHook = chain(fetchBodies.responseHook, undici.responseHook);
87
+ unsubscribe = fetchBodies.unsubscribe;
88
+ }
89
+ }
90
+ const lambda = user?.[LAMBDA] ?? {};
91
+ const lambdaEntry = {
92
+ ...lambda,
93
+ requestHook: chain((_span, { context }) => {
94
+ recordInvocationDeadline(context);
95
+ }, lambda.requestHook)
96
+ };
97
+ return {
98
+ map: {
99
+ ...user,
100
+ [HTTP]: httpEntry,
101
+ [UNDICI]: undiciEntry,
102
+ [LAMBDA]: lambdaEntry
103
+ },
104
+ unsubscribe
105
+ };
106
+ }
107
+ export { composeInstrumentationConfig };
@@ -0,0 +1,27 @@
1
+ const require_env = require("./env.cjs");
2
+ let _opentelemetry_core = require("@opentelemetry/core");
3
+ // src/config.ts
4
+ const HEADERS = "OTEL_EXPORTER_OTLP_HEADERS";
5
+ /**
6
+ * Writes the options that have an environment variable to `process.env` where it is unset.
7
+ *
8
+ * @param options - The `init` options.
9
+ */
10
+ function applyOptions(options) {
11
+ if (!((0, _opentelemetry_core.getStringFromEnv)("OTEL_RESOURCE_ATTRIBUTES") ?? "").split(",").some((a) => a.split("=")[0]?.trim() === "service.name")) require_env.setDefault("OTEL_SERVICE_NAME", options.serviceName);
12
+ require_env.setDefault("OTEL_EXPORTER_OTLP_ENDPOINT", options.endpoint);
13
+ if (options.httpBodyCapture !== void 0) require_env.setDefault("CLICKSTACK_CAPTURE_HTTP_BODY", String(options.httpBodyCapture));
14
+ const apiKey = options.apiKey?.trim();
15
+ if (!apiKey) return;
16
+ const header = `authorization=${encodeURIComponent(apiKey)}`;
17
+ const current = (0, _opentelemetry_core.getStringFromEnv)(HEADERS);
18
+ if (current !== void 0 && current !== header) require_env.warnOnce("api-key", `${HEADERS} is set, so the apiKey option is ignored`);
19
+ require_env.setDefault(HEADERS, header);
20
+ if ([
21
+ "",
22
+ "_TRACES",
23
+ "_METRICS",
24
+ "_LOGS"
25
+ ].every((s) => (0, _opentelemetry_core.getStringFromEnv)(`OTEL_EXPORTER_OTLP${s}_ENDPOINT`) === void 0)) require_env.warnOnce("no-endpoint", "apiKey is set but OTEL_EXPORTER_OTLP_ENDPOINT is not");
26
+ }
27
+ exports.applyOptions = applyOptions;
@@ -0,0 +1,61 @@
1
+ import { InstrumentationConfigMap } from "@opentelemetry/auto-instrumentations-node";
2
+ import { Instrumentation } from "@opentelemetry/instrumentation";
3
+ /**
4
+ * Options for `init`. Options with an environment variable are written to `process.env` only
5
+ * when that variable is unset, so an operator's setting always wins and child processes inherit
6
+ * the value. Every other setting comes from the standard `OTEL_*` variables.
7
+ *
8
+ * @public
9
+ */
10
+ export interface ClickStackNodeOptions {
11
+ /**
12
+ * The `service.name` resource attribute. Written to `OTEL_SERVICE_NAME`, unless
13
+ * `OTEL_RESOURCE_ATTRIBUTES` sets `service.name`.
14
+ *
15
+ * @defaultValue upstream: `unknown_service:` followed by the Node.js executable name
16
+ */
17
+ serviceName?: string | undefined;
18
+ /**
19
+ * Base URL of the OTLP/HTTP receiver, without `/v1/traces`. Written to
20
+ * `OTEL_EXPORTER_OTLP_ENDPOINT`.
21
+ *
22
+ * @defaultValue upstream: `http://localhost:4318`
23
+ */
24
+ endpoint?: string | undefined;
25
+ /**
26
+ * `deployment.environment.name`, such as `production` or `staging`. Set on the resource, not
27
+ * written to the environment. A value for the same key in `OTEL_RESOURCE_ATTRIBUTES` wins.
28
+ *
29
+ * @defaultValue none
30
+ */
31
+ environment?: string | undefined;
32
+ /**
33
+ * ClickStack ingestion key, sent as the `authorization` header. Trimmed, percent-encoded and
34
+ * written to `OTEL_EXPORTER_OTLP_HEADERS` as `authorization=<key>`. Ignored with one warning
35
+ * when that variable already holds something else.
36
+ *
37
+ * @defaultValue none
38
+ */
39
+ apiKey?: string | undefined;
40
+ /**
41
+ * Records HTTP request and response bodies on `node:http` spans, and on `fetch` spans from
42
+ * Node.js 24.4. Bodies can contain personal data and add storage cost. Written to
43
+ * `CLICKSTACK_CAPTURE_HTTP_BODY`.
44
+ *
45
+ * @defaultValue `false`
46
+ */
47
+ httpBodyCapture?: boolean | undefined;
48
+ /**
49
+ * Configuration for the auto-instrumentations bundle, in the shape of
50
+ * `getNodeAutoInstrumentations()`. Hooks given here run after this package's hooks.
51
+ *
52
+ * @defaultValue `{}`
53
+ */
54
+ instrumentations?: InstrumentationConfigMap | undefined;
55
+ /**
56
+ * Instrumentation instances to add to the bundle's, for libraries the bundle does not cover.
57
+ *
58
+ * @defaultValue `[]`
59
+ */
60
+ additionalInstrumentations?: Instrumentation[] | undefined;
61
+ }
@@ -0,0 +1,61 @@
1
+ import { InstrumentationConfigMap } from "@opentelemetry/auto-instrumentations-node";
2
+ import { Instrumentation } from "@opentelemetry/instrumentation";
3
+ /**
4
+ * Options for `init`. Options with an environment variable are written to `process.env` only
5
+ * when that variable is unset, so an operator's setting always wins and child processes inherit
6
+ * the value. Every other setting comes from the standard `OTEL_*` variables.
7
+ *
8
+ * @public
9
+ */
10
+ export interface ClickStackNodeOptions {
11
+ /**
12
+ * The `service.name` resource attribute. Written to `OTEL_SERVICE_NAME`, unless
13
+ * `OTEL_RESOURCE_ATTRIBUTES` sets `service.name`.
14
+ *
15
+ * @defaultValue upstream: `unknown_service:` followed by the Node.js executable name
16
+ */
17
+ serviceName?: string | undefined;
18
+ /**
19
+ * Base URL of the OTLP/HTTP receiver, without `/v1/traces`. Written to
20
+ * `OTEL_EXPORTER_OTLP_ENDPOINT`.
21
+ *
22
+ * @defaultValue upstream: `http://localhost:4318`
23
+ */
24
+ endpoint?: string | undefined;
25
+ /**
26
+ * `deployment.environment.name`, such as `production` or `staging`. Set on the resource, not
27
+ * written to the environment. A value for the same key in `OTEL_RESOURCE_ATTRIBUTES` wins.
28
+ *
29
+ * @defaultValue none
30
+ */
31
+ environment?: string | undefined;
32
+ /**
33
+ * ClickStack ingestion key, sent as the `authorization` header. Trimmed, percent-encoded and
34
+ * written to `OTEL_EXPORTER_OTLP_HEADERS` as `authorization=<key>`. Ignored with one warning
35
+ * when that variable already holds something else.
36
+ *
37
+ * @defaultValue none
38
+ */
39
+ apiKey?: string | undefined;
40
+ /**
41
+ * Records HTTP request and response bodies on `node:http` spans, and on `fetch` spans from
42
+ * Node.js 24.4. Bodies can contain personal data and add storage cost. Written to
43
+ * `CLICKSTACK_CAPTURE_HTTP_BODY`.
44
+ *
45
+ * @defaultValue `false`
46
+ */
47
+ httpBodyCapture?: boolean | undefined;
48
+ /**
49
+ * Configuration for the auto-instrumentations bundle, in the shape of
50
+ * `getNodeAutoInstrumentations()`. Hooks given here run after this package's hooks.
51
+ *
52
+ * @defaultValue `{}`
53
+ */
54
+ instrumentations?: InstrumentationConfigMap | undefined;
55
+ /**
56
+ * Instrumentation instances to add to the bundle's, for libraries the bundle does not cover.
57
+ *
58
+ * @defaultValue `[]`
59
+ */
60
+ additionalInstrumentations?: Instrumentation[] | undefined;
61
+ }
@@ -0,0 +1,27 @@
1
+ import { setDefault, warnOnce } from "./env.mjs";
2
+ import { getStringFromEnv } from "@opentelemetry/core";
3
+ // src/config.ts
4
+ const HEADERS = "OTEL_EXPORTER_OTLP_HEADERS";
5
+ /**
6
+ * Writes the options that have an environment variable to `process.env` where it is unset.
7
+ *
8
+ * @param options - The `init` options.
9
+ */
10
+ function applyOptions(options) {
11
+ if (!(getStringFromEnv("OTEL_RESOURCE_ATTRIBUTES") ?? "").split(",").some((a) => a.split("=")[0]?.trim() === "service.name")) setDefault("OTEL_SERVICE_NAME", options.serviceName);
12
+ setDefault("OTEL_EXPORTER_OTLP_ENDPOINT", options.endpoint);
13
+ if (options.httpBodyCapture !== void 0) setDefault("CLICKSTACK_CAPTURE_HTTP_BODY", String(options.httpBodyCapture));
14
+ const apiKey = options.apiKey?.trim();
15
+ if (!apiKey) return;
16
+ const header = `authorization=${encodeURIComponent(apiKey)}`;
17
+ const current = getStringFromEnv(HEADERS);
18
+ if (current !== void 0 && current !== header) warnOnce("api-key", `${HEADERS} is set, so the apiKey option is ignored`);
19
+ setDefault(HEADERS, header);
20
+ if ([
21
+ "",
22
+ "_TRACES",
23
+ "_METRICS",
24
+ "_LOGS"
25
+ ].every((s) => getStringFromEnv(`OTEL_EXPORTER_OTLP${s}_ENDPOINT`) === void 0)) warnOnce("no-endpoint", "apiKey is set but OTEL_EXPORTER_OTLP_ENDPOINT is not");
26
+ }
27
+ export { applyOptions };
package/dist/env.cjs ADDED
@@ -0,0 +1,88 @@
1
+ const require_state = require("./state.cjs");
2
+ let _opentelemetry_api = require("@opentelemetry/api");
3
+ let _opentelemetry_core = require("@opentelemetry/core");
4
+ // src/env.ts
5
+ const API_REGISTRY = Symbol.for("opentelemetry.js.api.1");
6
+ /**
7
+ * Returns the global registry of `@opentelemetry/api`, shared by every copy of the api.
8
+ *
9
+ * @returns The registered APIs, such as `diag` and `trace`, or `undefined` when none is.
10
+ */
11
+ function apiRegistry() {
12
+ return globalThis[API_REGISTRY];
13
+ }
14
+ /**
15
+ * Writes `value` to `process.env[name]` only when the variable is unset or blank, so an
16
+ * operator-set value always wins over a code option.
17
+ *
18
+ * @param name - Environment variable name.
19
+ * @param value - Value to write; `undefined` writes nothing.
20
+ */
21
+ function setDefault(name, value) {
22
+ if (value !== void 0 && (0, _opentelemetry_core.getStringFromEnv)(name) === void 0) process.env[name] = value;
23
+ }
24
+ /**
25
+ * Runs `call` so that what it reports through `diag` is printed. Until NodeSDK starts there is no
26
+ * diag logger unless the app installed one, and `diag` would drop everything, so a console logger
27
+ * at `OTEL_LOG_LEVEL` (default warn) is registered for the call and removed, as upstream's own
28
+ * register entry does around the auto-instrumentations bundle.
29
+ *
30
+ * @param call - The work to run.
31
+ * @returns What `call` returns.
32
+ */
33
+ function withStartupLogger(call) {
34
+ if (apiRegistry()?.diag !== void 0) return call();
35
+ const sink = new _opentelemetry_api.DiagConsoleLogger();
36
+ const logger = {
37
+ error: (message, ...args) => {
38
+ if (!message.includes("\"@opentelemetry/instrumentation-console\"")) sink.error(message, ...args);
39
+ },
40
+ warn: sink.warn,
41
+ info: sink.info,
42
+ debug: sink.debug,
43
+ verbose: sink.verbose
44
+ };
45
+ _opentelemetry_api.diag.setLogger(logger, {
46
+ logLevel: (0, _opentelemetry_core.diagLogLevelFromString)((0, _opentelemetry_core.getStringFromEnv)("OTEL_LOG_LEVEL")) ?? _opentelemetry_api.DiagLogLevel.WARN,
47
+ suppressOverrideMessage: true
48
+ });
49
+ try {
50
+ return call();
51
+ } finally {
52
+ _opentelemetry_api.diag.disable();
53
+ }
54
+ }
55
+ /**
56
+ * Writes a message, prefixed with `clickstack-node:`, through `diag`, also before NodeSDK starts.
57
+ * Never throws.
58
+ *
59
+ * @param level - Diag level.
60
+ * @param message - Message without the prefix.
61
+ * @param args - Extra values passed to the logger.
62
+ */
63
+ function report(level, message, ...args) {
64
+ try {
65
+ withStartupLogger(() => {
66
+ _opentelemetry_api.diag[level](`clickstack-node: ${message}`, ...args);
67
+ });
68
+ } catch {}
69
+ }
70
+ /**
71
+ * Reports a warning through {@link report}, once per key per thread.
72
+ *
73
+ * @param key - Deduplication key.
74
+ * @param message - Message without the prefix.
75
+ * @param args - Extra values passed to the logger.
76
+ */
77
+ function warnOnce(key, message, ...args) {
78
+ const state = require_state.getState();
79
+ state.warned ??= /* @__PURE__ */ new Set();
80
+ if (state.warned.has(key)) return;
81
+ state.warned.add(key);
82
+ report("warn", message, ...args);
83
+ }
84
+ exports.apiRegistry = apiRegistry;
85
+ exports.report = report;
86
+ exports.setDefault = setDefault;
87
+ exports.warnOnce = warnOnce;
88
+ exports.withStartupLogger = withStartupLogger;
package/dist/env.mjs ADDED
@@ -0,0 +1,84 @@
1
+ import { getState } from "./state.mjs";
2
+ import { DiagConsoleLogger, DiagLogLevel, diag } from "@opentelemetry/api";
3
+ import { diagLogLevelFromString, getStringFromEnv } from "@opentelemetry/core";
4
+ // src/env.ts
5
+ const API_REGISTRY = Symbol.for("opentelemetry.js.api.1");
6
+ /**
7
+ * Returns the global registry of `@opentelemetry/api`, shared by every copy of the api.
8
+ *
9
+ * @returns The registered APIs, such as `diag` and `trace`, or `undefined` when none is.
10
+ */
11
+ function apiRegistry() {
12
+ return globalThis[API_REGISTRY];
13
+ }
14
+ /**
15
+ * Writes `value` to `process.env[name]` only when the variable is unset or blank, so an
16
+ * operator-set value always wins over a code option.
17
+ *
18
+ * @param name - Environment variable name.
19
+ * @param value - Value to write; `undefined` writes nothing.
20
+ */
21
+ function setDefault(name, value) {
22
+ if (value !== void 0 && getStringFromEnv(name) === void 0) process.env[name] = value;
23
+ }
24
+ /**
25
+ * Runs `call` so that what it reports through `diag` is printed. Until NodeSDK starts there is no
26
+ * diag logger unless the app installed one, and `diag` would drop everything, so a console logger
27
+ * at `OTEL_LOG_LEVEL` (default warn) is registered for the call and removed, as upstream's own
28
+ * register entry does around the auto-instrumentations bundle.
29
+ *
30
+ * @param call - The work to run.
31
+ * @returns What `call` returns.
32
+ */
33
+ function withStartupLogger(call) {
34
+ if (apiRegistry()?.diag !== void 0) return call();
35
+ const sink = new DiagConsoleLogger();
36
+ const logger = {
37
+ error: (message, ...args) => {
38
+ if (!message.includes("\"@opentelemetry/instrumentation-console\"")) sink.error(message, ...args);
39
+ },
40
+ warn: sink.warn,
41
+ info: sink.info,
42
+ debug: sink.debug,
43
+ verbose: sink.verbose
44
+ };
45
+ diag.setLogger(logger, {
46
+ logLevel: diagLogLevelFromString(getStringFromEnv("OTEL_LOG_LEVEL")) ?? DiagLogLevel.WARN,
47
+ suppressOverrideMessage: true
48
+ });
49
+ try {
50
+ return call();
51
+ } finally {
52
+ diag.disable();
53
+ }
54
+ }
55
+ /**
56
+ * Writes a message, prefixed with `clickstack-node:`, through `diag`, also before NodeSDK starts.
57
+ * Never throws.
58
+ *
59
+ * @param level - Diag level.
60
+ * @param message - Message without the prefix.
61
+ * @param args - Extra values passed to the logger.
62
+ */
63
+ function report(level, message, ...args) {
64
+ try {
65
+ withStartupLogger(() => {
66
+ diag[level](`clickstack-node: ${message}`, ...args);
67
+ });
68
+ } catch {}
69
+ }
70
+ /**
71
+ * Reports a warning through {@link report}, once per key per thread.
72
+ *
73
+ * @param key - Deduplication key.
74
+ * @param message - Message without the prefix.
75
+ * @param args - Extra values passed to the logger.
76
+ */
77
+ function warnOnce(key, message, ...args) {
78
+ const state = getState();
79
+ state.warned ??= /* @__PURE__ */ new Set();
80
+ if (state.warned.has(key)) return;
81
+ state.warned.add(key);
82
+ report("warn", message, ...args);
83
+ }
84
+ export { apiRegistry, report, setDefault, warnOnce, withStartupLogger };