@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,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 };
|
package/dist/compose.cjs
ADDED
|
@@ -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;
|
package/dist/compose.mjs
ADDED
|
@@ -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 };
|
package/dist/config.cjs
ADDED
|
@@ -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
|
+
}
|
package/dist/config.mjs
ADDED
|
@@ -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 };
|