@0xchain/telemetry 1.1.0-beta.17 → 1.1.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/Sampler.d.ts.map +1 -1
- package/dist/ignore-BvHZTNsF.js +151 -0
- package/dist/index.js +558 -344
- package/dist/instrumentation.js +5 -5
- package/dist/middleware.js +36 -22
- package/dist/next.js +36 -22
- package/package.json +27 -10
- package/dist/ignore-CMi2r3b2.js +0 -114
package/dist/index.js
CHANGED
|
@@ -1,395 +1,609 @@
|
|
|
1
|
-
import { NodeSDK
|
|
2
|
-
import { OTLPTraceExporter
|
|
3
|
-
import { resourceFromAttributes
|
|
4
|
-
import { ATTR_SERVICE_VERSION
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
3
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
|
+
import { ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
5
|
+
import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME } from "@opentelemetry/semantic-conventions/incubating";
|
|
6
|
+
import { SamplingDecision, TraceIdRatioBasedSampler, AlwaysOnSampler, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
7
|
+
import { trace, TraceFlags, propagation, context, SpanStatusCode } from "@opentelemetry/api";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
|
10
|
+
import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
|
|
11
|
+
import { c as createIgnoreMatcher, m as matchesPathPattern, a as createSpanNameMatcher, d as defaultIgnoreConfig } from "./ignore-BvHZTNsF.js";
|
|
12
|
+
const getHeaderValue = (headers, name) => {
|
|
13
|
+
if (!headers) return void 0;
|
|
14
|
+
const value = headers[name.toLowerCase()];
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
const first = value[0];
|
|
17
|
+
return typeof first === "string" ? first : void 0;
|
|
18
|
+
}
|
|
19
|
+
return typeof value === "string" ? value : void 0;
|
|
20
|
+
};
|
|
21
|
+
const getPathname = (url) => {
|
|
22
|
+
if (!url) return "";
|
|
21
23
|
try {
|
|
22
|
-
|
|
24
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
25
|
+
return new URL(url).pathname;
|
|
26
|
+
}
|
|
27
|
+
return new URL(url, "http://localhost").pathname;
|
|
23
28
|
} catch {
|
|
24
|
-
return
|
|
29
|
+
return url.split("?")[0] || url;
|
|
25
30
|
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
};
|
|
32
|
+
const spanStartTime = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
function getInstrumentations(config) {
|
|
34
|
+
const shouldIgnore = createIgnoreMatcher(config.ignore);
|
|
35
|
+
const inspectionHeader = (config.inspectionHeader ?? "x-inspection").toLowerCase();
|
|
36
|
+
const userTypeHeader = (config.userTypeHeader ?? "x-telemetry-user-type").toLowerCase();
|
|
37
|
+
const requestStartHeader = (config.requestStartHeader ?? "x-telemetry-start").toLowerCase();
|
|
29
38
|
return [
|
|
30
|
-
new
|
|
31
|
-
startIncomingSpanHook: (
|
|
32
|
-
const
|
|
33
|
-
|
|
39
|
+
new HttpInstrumentation({
|
|
40
|
+
startIncomingSpanHook: (request) => {
|
|
41
|
+
const headers = "headers" in request ? request.headers : void 0;
|
|
42
|
+
const inspectionValue = getHeaderValue(headers, inspectionHeader);
|
|
43
|
+
const userTypeValue = getHeaderValue(headers, userTypeHeader);
|
|
44
|
+
const startTimeValue = getHeaderValue(headers, requestStartHeader);
|
|
45
|
+
const method = "method" in request ? request.method : void 0;
|
|
46
|
+
const pathname = getPathname("url" in request ? request.url : void 0);
|
|
47
|
+
const startTime = Number(startTimeValue || Date.now());
|
|
48
|
+
const attributes = {
|
|
49
|
+
"telemetry.request.start_time_ms": Number.isFinite(startTime) ? startTime : Date.now()
|
|
34
50
|
};
|
|
35
|
-
|
|
51
|
+
if (pathname) {
|
|
52
|
+
attributes["url.path"] = pathname;
|
|
53
|
+
}
|
|
54
|
+
if (method) {
|
|
55
|
+
attributes["http.request.method"] = method;
|
|
56
|
+
}
|
|
57
|
+
if (inspectionValue) {
|
|
58
|
+
attributes["telemetry.request.inspection"] = true;
|
|
59
|
+
attributes["http.request.header.x-inspection"] = inspectionValue;
|
|
60
|
+
}
|
|
61
|
+
if (userTypeValue) {
|
|
62
|
+
attributes["enduser.type"] = userTypeValue;
|
|
63
|
+
attributes["telemetry.request.user_type"] = userTypeValue;
|
|
64
|
+
}
|
|
65
|
+
return attributes;
|
|
36
66
|
},
|
|
37
|
-
requestHook: (
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
67
|
+
requestHook: (span, request) => {
|
|
68
|
+
const headers = "headers" in request ? request.headers : void 0;
|
|
69
|
+
const startTimeValue = getHeaderValue(headers, requestStartHeader);
|
|
70
|
+
const startTime = Number(startTimeValue || Date.now());
|
|
71
|
+
spanStartTime.set(span, Number.isFinite(startTime) ? startTime : Date.now());
|
|
72
|
+
const method = "method" in request ? request.method || "GET" : "GET";
|
|
73
|
+
const pathname = getPathname("url" in request ? request.url : void 0);
|
|
74
|
+
if (pathname) {
|
|
75
|
+
span.updateName(`${method} ${pathname}`);
|
|
76
|
+
}
|
|
42
77
|
},
|
|
43
|
-
responseHook: (
|
|
44
|
-
const
|
|
45
|
-
typeof
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
responseHook: (span, response) => {
|
|
79
|
+
const statusCode = "statusCode" in response ? response.statusCode : void 0;
|
|
80
|
+
if (typeof statusCode === "number") {
|
|
81
|
+
span.setAttribute("http.response.status_code", statusCode);
|
|
82
|
+
span.setAttribute("telemetry.response.status_code", statusCode);
|
|
83
|
+
if (statusCode >= 500) {
|
|
84
|
+
span.setAttribute("telemetry.request.error", true);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const startTime = spanStartTime.get(span);
|
|
88
|
+
if (typeof startTime === "number") {
|
|
89
|
+
span.setAttribute("telemetry.request.duration_ms", Date.now() - startTime);
|
|
90
|
+
}
|
|
48
91
|
},
|
|
49
|
-
ignoreIncomingRequestHook: (
|
|
50
|
-
const
|
|
51
|
-
|
|
92
|
+
ignoreIncomingRequestHook: (request) => {
|
|
93
|
+
const url = "url" in request ? request.url : void 0;
|
|
94
|
+
const pathname = getPathname(url);
|
|
95
|
+
const method = "method" in request ? request.method : void 0;
|
|
96
|
+
return shouldIgnore(pathname, method);
|
|
52
97
|
},
|
|
53
|
-
ignoreOutgoingRequestHook: (
|
|
54
|
-
const
|
|
55
|
-
return
|
|
98
|
+
ignoreOutgoingRequestHook: (request) => {
|
|
99
|
+
const url = typeof request === "string" ? request : request.path || "";
|
|
100
|
+
return shouldIgnore(getPathname(url));
|
|
56
101
|
}
|
|
57
102
|
}),
|
|
58
|
-
new
|
|
103
|
+
new UndiciInstrumentation()
|
|
59
104
|
];
|
|
60
105
|
}
|
|
61
|
-
const
|
|
62
|
-
if (typeof
|
|
63
|
-
if (Array.isArray(
|
|
64
|
-
const
|
|
65
|
-
return typeof
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
const toStringValue = (value) => {
|
|
107
|
+
if (typeof value === "string") return value;
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
const first = value[0];
|
|
110
|
+
return typeof first === "string" ? first : void 0;
|
|
111
|
+
}
|
|
112
|
+
return void 0;
|
|
113
|
+
};
|
|
114
|
+
const toNumberValue = (value) => {
|
|
115
|
+
if (typeof value === "number") return value;
|
|
116
|
+
if (typeof value === "string") {
|
|
117
|
+
const parsed = Number(value);
|
|
118
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
const first = value[0];
|
|
122
|
+
return toNumberValue(first);
|
|
123
|
+
}
|
|
124
|
+
return void 0;
|
|
125
|
+
};
|
|
126
|
+
const pickAttribute = (attributes, keys) => {
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
if (key in attributes) {
|
|
129
|
+
return attributes[key];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return void 0;
|
|
133
|
+
};
|
|
134
|
+
const buildSamplingContextFromAttributes = (attributes) => {
|
|
135
|
+
const pathValue = pickAttribute(attributes, ["url.path", "http.target", "http.route"]);
|
|
136
|
+
const methodValue = pickAttribute(attributes, ["http.request.method", "http.method"]);
|
|
137
|
+
const userTypeValue = pickAttribute(attributes, ["enduser.type", "telemetry.request.user_type"]);
|
|
138
|
+
const statusValue = pickAttribute(attributes, [
|
|
83
139
|
"http.response.status_code",
|
|
84
140
|
"http.status_code",
|
|
85
141
|
"telemetry.response.status_code"
|
|
86
|
-
])
|
|
142
|
+
]);
|
|
143
|
+
const inspectionValue = pickAttribute(attributes, [
|
|
87
144
|
"telemetry.request.inspection",
|
|
88
145
|
"http.request.header.x-inspection",
|
|
89
146
|
"x-inspection"
|
|
90
|
-
])
|
|
147
|
+
]);
|
|
148
|
+
const inspection = inspectionValue === true || inspectionValue === "true" || inspectionValue === "1" || inspectionValue === 1;
|
|
91
149
|
return {
|
|
92
|
-
path:
|
|
93
|
-
method:
|
|
94
|
-
userType:
|
|
95
|
-
statusCode:
|
|
96
|
-
inspection
|
|
150
|
+
path: toStringValue(pathValue),
|
|
151
|
+
method: toStringValue(methodValue),
|
|
152
|
+
userType: toStringValue(userTypeValue),
|
|
153
|
+
statusCode: toNumberValue(statusValue),
|
|
154
|
+
inspection
|
|
97
155
|
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
if (
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
156
|
+
};
|
|
157
|
+
const matchStringList = (value, list) => {
|
|
158
|
+
if (!value || !list?.length) return false;
|
|
159
|
+
return list.some((item) => item.toLowerCase() === value.toLowerCase());
|
|
160
|
+
};
|
|
161
|
+
const matchPathList = (path, list) => {
|
|
162
|
+
if (!path || !list?.length) return false;
|
|
163
|
+
return list.some((pattern) => matchesPathPattern(pattern, path));
|
|
164
|
+
};
|
|
165
|
+
const matchSamplingRule = (rule, context2) => {
|
|
166
|
+
const condition = rule.when;
|
|
167
|
+
if (!condition) return true;
|
|
168
|
+
if (condition.inspection !== void 0 && condition.inspection !== context2.inspection) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (condition.path && !matchPathList(context2.path, condition.path)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (condition.method && !matchStringList(context2.method, condition.method)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (condition.userType && !matchStringList(context2.userType, condition.userType)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (condition.statusCode && typeof context2.statusCode === "number") {
|
|
181
|
+
if (!condition.statusCode.includes(context2.statusCode)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
} else if (condition.statusCode && condition.statusCode.length) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (typeof condition.minStatusCode === "number") {
|
|
188
|
+
if (typeof context2.statusCode !== "number" || context2.statusCode < condition.minStatusCode) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (typeof condition.maxStatusCode === "number") {
|
|
193
|
+
if (typeof context2.statusCode !== "number" || context2.statusCode > condition.maxStatusCode) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
199
|
+
const clampRatio = (ratio) => Math.max(0, Math.min(1, ratio));
|
|
200
|
+
const resolveSamplingRatio = (rules, context2, defaultRatio) => {
|
|
201
|
+
if (!rules?.length) return clampRatio(defaultRatio);
|
|
202
|
+
for (const rule of rules) {
|
|
203
|
+
if (matchSamplingRule(rule, context2)) {
|
|
204
|
+
return clampRatio(rule.ratio);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return clampRatio(defaultRatio);
|
|
208
|
+
};
|
|
209
|
+
const samplerCache = /* @__PURE__ */ new Map();
|
|
210
|
+
const getSamplerForRatio = (ratio) => {
|
|
211
|
+
if (ratio <= 0) {
|
|
212
|
+
return new TraceIdRatioBasedSampler(0);
|
|
213
|
+
}
|
|
214
|
+
if (ratio >= 1) {
|
|
215
|
+
return new AlwaysOnSampler();
|
|
216
|
+
}
|
|
217
|
+
const normalized = Number(ratio.toFixed(6));
|
|
218
|
+
const cached = samplerCache.get(normalized);
|
|
219
|
+
if (cached) return cached;
|
|
220
|
+
const sampler = new TraceIdRatioBasedSampler(normalized);
|
|
221
|
+
samplerCache.set(normalized, sampler);
|
|
222
|
+
return sampler;
|
|
223
|
+
};
|
|
224
|
+
const getSpanNameMatcher = (config, cache) => {
|
|
225
|
+
if (config !== cache.ignore) {
|
|
226
|
+
cache.ignore = config;
|
|
227
|
+
cache.matcher = createSpanNameMatcher(config);
|
|
228
|
+
}
|
|
229
|
+
return cache.matcher;
|
|
230
|
+
};
|
|
231
|
+
const createRuleBasedSampler = (runtime2) => {
|
|
232
|
+
const matcherCache = {};
|
|
126
233
|
return {
|
|
127
|
-
shouldSample(
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
234
|
+
shouldSample(context2, traceId, spanName, spanKind, attributes, links) {
|
|
235
|
+
const parentContext = trace.getSpanContext(context2);
|
|
236
|
+
if (parentContext) {
|
|
237
|
+
if (parentContext.isRemote) {
|
|
238
|
+
const config2 = runtime2.getConfig();
|
|
239
|
+
const matcher2 = getSpanNameMatcher(config2.ignore, matcherCache);
|
|
240
|
+
if (matcher2?.(spanName)) {
|
|
241
|
+
return { decision: SamplingDecision.NOT_RECORD };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if ((parentContext.traceFlags & TraceFlags.SAMPLED) === TraceFlags.SAMPLED) {
|
|
245
|
+
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
|
|
246
|
+
}
|
|
247
|
+
return { decision: SamplingDecision.NOT_RECORD };
|
|
248
|
+
}
|
|
249
|
+
const config = runtime2.getConfig();
|
|
250
|
+
const matcher = getSpanNameMatcher(config.ignore, matcherCache);
|
|
251
|
+
if (matcher?.(spanName)) {
|
|
252
|
+
return { decision: SamplingDecision.NOT_RECORD };
|
|
253
|
+
}
|
|
254
|
+
const samplingContext = buildSamplingContextFromAttributes(attributes);
|
|
255
|
+
if (samplingContext.inspection) {
|
|
256
|
+
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
|
|
257
|
+
}
|
|
258
|
+
const ratio = resolveSamplingRatio(
|
|
259
|
+
config.samplingRules,
|
|
260
|
+
samplingContext,
|
|
261
|
+
config.defaultSamplingRatio ?? 0.01
|
|
141
262
|
);
|
|
142
|
-
return
|
|
263
|
+
return getSamplerForRatio(ratio).shouldSample(context2, traceId, spanName, spanKind, attributes, links);
|
|
143
264
|
},
|
|
144
265
|
toString: () => "RuleBasedSampler"
|
|
145
266
|
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
267
|
+
};
|
|
268
|
+
const createFilteringTraceExporter = (exporter) => exporter;
|
|
269
|
+
let sdk;
|
|
270
|
+
let runtime;
|
|
271
|
+
const uniqueList = (items) => Array.from(new Set(items));
|
|
272
|
+
const parseList = (value) => value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
|
|
273
|
+
const parseHeaders = (value) => {
|
|
274
|
+
if (!value) return void 0;
|
|
275
|
+
const headers = {};
|
|
276
|
+
for (const pair of value.split(",")) {
|
|
277
|
+
const [key, ...rest] = pair.split("=");
|
|
278
|
+
const normalizedKey = key?.trim();
|
|
279
|
+
if (!normalizedKey) continue;
|
|
280
|
+
headers[normalizedKey] = rest.join("=").trim();
|
|
281
|
+
}
|
|
282
|
+
return headers;
|
|
283
|
+
};
|
|
284
|
+
const parseJson = (value) => {
|
|
285
|
+
if (!value) return void 0;
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(value);
|
|
288
|
+
} catch {
|
|
289
|
+
return void 0;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const readConfigFile = async (path) => {
|
|
293
|
+
if (!path) return void 0;
|
|
294
|
+
try {
|
|
295
|
+
const raw = await readFile(path, "utf8");
|
|
296
|
+
return parseJson(raw);
|
|
297
|
+
} catch {
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const mergeIgnoreConfig = (base, overrides) => {
|
|
302
|
+
const mergedPaths = uniqueList([...base?.paths ?? [], ...overrides?.paths ?? []]);
|
|
303
|
+
const mergedExtensions = uniqueList([
|
|
304
|
+
...base?.extensions ?? [],
|
|
305
|
+
...overrides?.extensions ?? []
|
|
306
|
+
]);
|
|
307
|
+
const mergedMethods = uniqueList([...base?.methods ?? [], ...overrides?.methods ?? []]);
|
|
308
|
+
const mergedSpanNames = uniqueList([...base?.spanNames ?? [], ...overrides?.spanNames ?? []]);
|
|
176
309
|
return {
|
|
177
|
-
paths:
|
|
178
|
-
extensions:
|
|
179
|
-
methods:
|
|
180
|
-
spanNames:
|
|
310
|
+
paths: mergedPaths.length ? mergedPaths : void 0,
|
|
311
|
+
extensions: mergedExtensions.length ? mergedExtensions : void 0,
|
|
312
|
+
methods: mergedMethods.length ? mergedMethods : void 0,
|
|
313
|
+
spanNames: mergedSpanNames.length ? mergedSpanNames : void 0
|
|
181
314
|
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
315
|
+
};
|
|
316
|
+
const normalizeSamplingRules = (rules) => {
|
|
317
|
+
if (!rules?.length) return void 0;
|
|
318
|
+
return rules.map((rule) => ({
|
|
319
|
+
...rule,
|
|
320
|
+
ratio: Number.isFinite(rule.ratio) ? Math.max(0, Math.min(1, rule.ratio)) : 0
|
|
321
|
+
})).filter((rule) => rule.ratio >= 0);
|
|
322
|
+
};
|
|
323
|
+
const loadTelemetryConfigFromEnv = async (env = process.env) => {
|
|
324
|
+
const fileConfig = await readConfigFile(env.TELEMETRY_CONFIG_FILE);
|
|
325
|
+
const ignorePaths = parseList(env.TELEMETRY_IGNORE_PATHS);
|
|
326
|
+
const ignoreExtensions = parseList(env.TELEMETRY_IGNORE_EXTENSIONS);
|
|
327
|
+
const ignoreMethods = parseList(env.TELEMETRY_IGNORE_METHODS);
|
|
328
|
+
const samplingRules = parseJson(env.TELEMETRY_SAMPLING_RULES);
|
|
329
|
+
const fileSamplingRules = typeof fileConfig?.samplingRules === "string" ? parseJson(fileConfig.samplingRules) : fileConfig?.samplingRules;
|
|
330
|
+
const envConfig = {
|
|
331
|
+
serviceName: env.OTEL_SERVICE_NAME || env.TELEMETRY_SERVICE_NAME,
|
|
332
|
+
serviceVersion: env.OTEL_SERVICE_VERSION || env.TELEMETRY_SERVICE_VERSION,
|
|
333
|
+
environment: env.OTEL_RESOURCE_ATTRIBUTES || env.TELEMETRY_ENVIRONMENT || (env.VERCEL ? env.VERCEL_ENV : void 0),
|
|
334
|
+
defaultSamplingRatio: env.TELEMETRY_SAMPLING_RATIO ? Number(env.TELEMETRY_SAMPLING_RATIO) : void 0,
|
|
335
|
+
url: env.OTEL_EXPORTER_OTLP_ENDPOINT || env.TELEMETRY_OTLP_ENDPOINT,
|
|
336
|
+
headers: parseHeaders(env.OTEL_EXPORTER_OTLP_HEADERS || env.TELEMETRY_OTLP_HEADERS),
|
|
337
|
+
inspectionHeader: env.TELEMETRY_INSPECTION_HEADER,
|
|
338
|
+
userTypeHeader: env.TELEMETRY_USER_TYPE_HEADER,
|
|
339
|
+
requestStartHeader: env.TELEMETRY_REQUEST_START_HEADER,
|
|
340
|
+
samplingRules: samplingRules || fileSamplingRules,
|
|
341
|
+
ignore: ignorePaths.length || ignoreExtensions.length || ignoreMethods.length ? {
|
|
342
|
+
paths: ignorePaths.length ? ignorePaths : void 0,
|
|
343
|
+
extensions: ignoreExtensions.length ? ignoreExtensions : void 0,
|
|
344
|
+
methods: ignoreMethods.length ? ignoreMethods : void 0
|
|
204
345
|
} : void 0
|
|
205
346
|
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
347
|
+
if (fileConfig && typeof fileConfig === "object") {
|
|
348
|
+
return { ...fileConfig, ...envConfig };
|
|
349
|
+
}
|
|
350
|
+
return envConfig;
|
|
351
|
+
};
|
|
352
|
+
const resolveEnvironmentName = (environment) => {
|
|
353
|
+
if (!environment) return process.env.NODE_ENV || "development";
|
|
354
|
+
if (environment.includes("deployment.environment")) {
|
|
355
|
+
const entries = environment.split(",");
|
|
356
|
+
const match = entries.find((entry) => entry.includes("deployment.environment"));
|
|
357
|
+
if (!match) return environment;
|
|
358
|
+
const [, value] = match.split("=");
|
|
359
|
+
return value?.trim() || environment;
|
|
360
|
+
}
|
|
361
|
+
return environment;
|
|
362
|
+
};
|
|
363
|
+
const buildLayeredSamplingRules = (layered, defaultRatio) => {
|
|
364
|
+
const rules = [
|
|
365
|
+
{ name: "inspection", ratio: 1, when: { inspection: true } },
|
|
220
366
|
{ name: "error", ratio: 1, when: { minStatusCode: 500 } }
|
|
221
367
|
];
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
368
|
+
if (layered?.criticalPaths?.length) {
|
|
369
|
+
rules.push({
|
|
370
|
+
name: "critical-path",
|
|
371
|
+
ratio: 1,
|
|
372
|
+
when: { path: layered.criticalPaths }
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (layered?.criticalUserTypes?.length) {
|
|
376
|
+
rules.push({
|
|
377
|
+
name: "critical-user",
|
|
378
|
+
ratio: 1,
|
|
379
|
+
when: { userType: layered.criticalUserTypes }
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (layered?.pathSampling) {
|
|
383
|
+
for (const [pattern, ratio] of Object.entries(layered.pathSampling)) {
|
|
384
|
+
rules.push({
|
|
385
|
+
name: `path:${pattern}`,
|
|
386
|
+
ratio,
|
|
387
|
+
when: { path: [pattern] }
|
|
236
388
|
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (layered?.userTypeSampling) {
|
|
392
|
+
for (const [userType, ratio] of Object.entries(layered.userTypeSampling)) {
|
|
393
|
+
rules.push({
|
|
394
|
+
name: `user:${userType}`,
|
|
395
|
+
ratio,
|
|
396
|
+
when: { userType: [userType] }
|
|
243
397
|
});
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
rules.push({ name: "default", ratio: defaultRatio });
|
|
401
|
+
return rules;
|
|
402
|
+
};
|
|
403
|
+
const normalizeResolvedConfig = (merged) => {
|
|
404
|
+
const environment = resolveEnvironmentName(merged.environment);
|
|
405
|
+
const ignore = mergeIgnoreConfig(defaultIgnoreConfig, merged.ignore);
|
|
406
|
+
const defaultSamplingRatio = typeof merged.defaultSamplingRatio === "number" ? Math.max(0, Math.min(1, merged.defaultSamplingRatio)) : 0.01;
|
|
407
|
+
const samplingRules = normalizeSamplingRules(merged.samplingRules) ?? buildLayeredSamplingRules(merged.layeredSampling, defaultSamplingRatio);
|
|
247
408
|
return {
|
|
248
|
-
...
|
|
249
|
-
serviceName:
|
|
250
|
-
serviceVersion:
|
|
251
|
-
environment
|
|
252
|
-
defaultSamplingRatio
|
|
253
|
-
ignore
|
|
254
|
-
samplingRules
|
|
255
|
-
inspectionHeader:
|
|
256
|
-
userTypeHeader:
|
|
257
|
-
requestStartHeader:
|
|
258
|
-
scheduledDelayMillis:
|
|
259
|
-
maxQueueSize:
|
|
260
|
-
maxExportBatchSize:
|
|
261
|
-
exporterTimeoutMillis:
|
|
409
|
+
...merged,
|
|
410
|
+
serviceName: merged.serviceName || "unknown-service",
|
|
411
|
+
serviceVersion: merged.serviceVersion,
|
|
412
|
+
environment,
|
|
413
|
+
defaultSamplingRatio,
|
|
414
|
+
ignore,
|
|
415
|
+
samplingRules,
|
|
416
|
+
inspectionHeader: merged.inspectionHeader ?? "x-inspection",
|
|
417
|
+
userTypeHeader: merged.userTypeHeader ?? "x-telemetry-user-type",
|
|
418
|
+
requestStartHeader: merged.requestStartHeader ?? "x-telemetry-start",
|
|
419
|
+
scheduledDelayMillis: merged.scheduledDelayMillis ?? 1e3,
|
|
420
|
+
maxQueueSize: merged.maxQueueSize ?? 2048,
|
|
421
|
+
maxExportBatchSize: merged.maxExportBatchSize ?? 512,
|
|
422
|
+
exporterTimeoutMillis: merged.exporterTimeoutMillis ?? 3e4
|
|
262
423
|
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
424
|
+
};
|
|
425
|
+
const resolveTelemetryConfig = async (options = {}, env = process.env) => {
|
|
426
|
+
const envConfig = await loadTelemetryConfigFromEnv(env);
|
|
427
|
+
return normalizeResolvedConfig({ ...envConfig, ...options });
|
|
428
|
+
};
|
|
429
|
+
const createTelemetryRuntime = async (initialConfig) => {
|
|
430
|
+
let current = await resolveTelemetryConfig(initialConfig);
|
|
268
431
|
return {
|
|
269
|
-
getConfig: () =>
|
|
270
|
-
updateConfig: (
|
|
271
|
-
|
|
272
|
-
...
|
|
273
|
-
...
|
|
274
|
-
ignore:
|
|
432
|
+
getConfig: () => current,
|
|
433
|
+
updateConfig: (partial) => {
|
|
434
|
+
current = normalizeResolvedConfig({
|
|
435
|
+
...current,
|
|
436
|
+
...partial,
|
|
437
|
+
ignore: mergeIgnoreConfig(current.ignore, partial.ignore)
|
|
275
438
|
});
|
|
276
439
|
}
|
|
277
440
|
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
441
|
+
};
|
|
442
|
+
const hrTimeToMilliseconds = (time) => time[0] * 1e3 + time[1] / 1e6;
|
|
443
|
+
const getSpanAttributeNumber = (span, keys) => {
|
|
444
|
+
for (const key of keys) {
|
|
445
|
+
const value = span.attributes[key];
|
|
446
|
+
if (typeof value === "number") return value;
|
|
447
|
+
}
|
|
448
|
+
return void 0;
|
|
449
|
+
};
|
|
450
|
+
const extractExceptionMessage = (span) => {
|
|
451
|
+
const exceptionEvent = span.events.find((event) => event.name === "exception");
|
|
452
|
+
if (!exceptionEvent?.attributes) return void 0;
|
|
453
|
+
const message = exceptionEvent.attributes["exception.message"];
|
|
454
|
+
return typeof message === "string" ? message : void 0;
|
|
455
|
+
};
|
|
456
|
+
const BARE_HTTP_METHOD = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$/;
|
|
457
|
+
const extractPathFromSpanAttributes = (attrs) => {
|
|
458
|
+
for (const key of ["url.path", "http.target", "http.route", "next.route"]) {
|
|
459
|
+
const val = attrs[key];
|
|
460
|
+
if (val && typeof val === "string") return val;
|
|
461
|
+
}
|
|
462
|
+
const url = attrs["http.url"] ?? attrs["url.full"];
|
|
463
|
+
if (url && typeof url === "string") {
|
|
295
464
|
try {
|
|
296
|
-
return new URL(
|
|
465
|
+
return new URL(url).pathname;
|
|
297
466
|
} catch {
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
}, gt = () => ({
|
|
301
|
-
onStart: (t) => {
|
|
302
|
-
const e = t, r = e.name;
|
|
303
|
-
if (!r || !I.test(r)) return;
|
|
304
|
-
const n = P(e.attributes);
|
|
305
|
-
n && t.updateName(`${r} ${n}`);
|
|
306
|
-
},
|
|
307
|
-
onEnd: (t) => {
|
|
308
|
-
const e = t.name;
|
|
309
|
-
if (!e || !I.test(e)) return;
|
|
310
|
-
const r = P(t.attributes);
|
|
311
|
-
if (!r) return;
|
|
312
|
-
const n = t.attributes["http.request.method"] ?? t.attributes["http.method"], s = typeof n == "string" ? n : e;
|
|
313
|
-
t.name = `${s} ${r}`;
|
|
314
|
-
},
|
|
315
|
-
shutdown: () => Promise.resolve(),
|
|
316
|
-
forceFlush: () => Promise.resolve()
|
|
317
|
-
}), _t = () => ({
|
|
318
|
-
onStart: () => {
|
|
319
|
-
},
|
|
320
|
-
onEnd: (t) => {
|
|
321
|
-
const e = dt(t.duration);
|
|
322
|
-
t.attributes["telemetry.request.duration_ms"] = Math.round(e), typeof t.attributes["telemetry.request.start_time_ms"] != "number" && (t.attributes["telemetry.request.start_time_ms"] = Date.now() - Math.round(e));
|
|
323
|
-
const r = Tt(t, [
|
|
324
|
-
"http.response.status_code",
|
|
325
|
-
"http.status_code",
|
|
326
|
-
"telemetry.response.status_code"
|
|
327
|
-
]);
|
|
328
|
-
if (typeof r == "number" && (t.attributes["telemetry.response.status_code"] = r), t.status.code === $.ERROR || typeof r == "number" && r >= 500) {
|
|
329
|
-
t.attributes["telemetry.request.error"] = !0;
|
|
330
|
-
const s = t.status.message || Rt(t);
|
|
331
|
-
s && (t.attributes["telemetry.request.error_message"] = s);
|
|
467
|
+
return url.split("?")[0];
|
|
332
468
|
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
469
|
+
}
|
|
470
|
+
return void 0;
|
|
471
|
+
};
|
|
472
|
+
const createSpanNamingProcessor = () => {
|
|
473
|
+
return {
|
|
474
|
+
onStart: (span) => {
|
|
475
|
+
const sdkSpan = span;
|
|
476
|
+
const name = sdkSpan.name;
|
|
477
|
+
if (!name || !BARE_HTTP_METHOD.test(name)) return;
|
|
478
|
+
const path = extractPathFromSpanAttributes(sdkSpan.attributes);
|
|
479
|
+
if (path) {
|
|
480
|
+
span.updateName(`${name} ${path}`);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
onEnd: (span) => {
|
|
484
|
+
const name = span.name;
|
|
485
|
+
if (!name || !BARE_HTTP_METHOD.test(name)) return;
|
|
486
|
+
const path = extractPathFromSpanAttributes(span.attributes);
|
|
487
|
+
if (!path) return;
|
|
488
|
+
const method = span.attributes["http.request.method"] ?? span.attributes["http.method"];
|
|
489
|
+
const resolvedMethod = typeof method === "string" ? method : name;
|
|
490
|
+
span.name = `${resolvedMethod} ${path}`;
|
|
491
|
+
},
|
|
492
|
+
shutdown: () => Promise.resolve(),
|
|
493
|
+
forceFlush: () => Promise.resolve()
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
const createRequestSpanProcessor = () => {
|
|
497
|
+
return {
|
|
498
|
+
onStart: () => void 0,
|
|
499
|
+
onEnd: (span) => {
|
|
500
|
+
const durationMs = hrTimeToMilliseconds(span.duration);
|
|
501
|
+
span.attributes["telemetry.request.duration_ms"] = Math.round(durationMs);
|
|
502
|
+
if (typeof span.attributes["telemetry.request.start_time_ms"] !== "number") {
|
|
503
|
+
span.attributes["telemetry.request.start_time_ms"] = Date.now() - Math.round(durationMs);
|
|
504
|
+
}
|
|
505
|
+
const statusCode = getSpanAttributeNumber(span, [
|
|
506
|
+
"http.response.status_code",
|
|
507
|
+
"http.status_code",
|
|
508
|
+
"telemetry.response.status_code"
|
|
509
|
+
]);
|
|
510
|
+
if (typeof statusCode === "number") {
|
|
511
|
+
span.attributes["telemetry.response.status_code"] = statusCode;
|
|
512
|
+
}
|
|
513
|
+
const isError = span.status.code === SpanStatusCode.ERROR || typeof statusCode === "number" && statusCode >= 500;
|
|
514
|
+
if (isError) {
|
|
515
|
+
span.attributes["telemetry.request.error"] = true;
|
|
516
|
+
const message = span.status.message || extractExceptionMessage(span);
|
|
517
|
+
if (message) {
|
|
518
|
+
span.attributes["telemetry.request.error_message"] = message;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
shutdown: () => Promise.resolve(),
|
|
523
|
+
forceFlush: () => Promise.resolve()
|
|
343
524
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
525
|
+
};
|
|
526
|
+
const startTelemetry = async (options) => {
|
|
527
|
+
const config = await resolveTelemetryConfig(options);
|
|
528
|
+
runtime = await createTelemetryRuntime(config);
|
|
529
|
+
const env = process.env;
|
|
530
|
+
const resourceAttrs = {
|
|
531
|
+
[ATTR_SERVICE_NAME]: config.serviceName,
|
|
532
|
+
[ATTR_SERVICE_VERSION]: config.serviceVersion || "1.0.0",
|
|
533
|
+
[ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: config.environment || env.VERCEL_ENV || env.NODE_ENV || "development"
|
|
534
|
+
};
|
|
535
|
+
if (env.VERCEL) {
|
|
536
|
+
if (env.VERCEL_REGION) resourceAttrs["vercel.region"] = env.VERCEL_REGION;
|
|
537
|
+
if (env.NEXT_RUNTIME) resourceAttrs["vercel.runtime"] = env.NEXT_RUNTIME;
|
|
538
|
+
if (env.VERCEL_GIT_COMMIT_SHA) resourceAttrs["vercel.sha"] = env.VERCEL_GIT_COMMIT_SHA;
|
|
539
|
+
if (env.VERCEL_URL) resourceAttrs["vercel.host"] = env.VERCEL_URL;
|
|
540
|
+
if (env.VERCEL_BRANCH_URL) resourceAttrs["vercel.branch_host"] = env.VERCEL_BRANCH_URL;
|
|
541
|
+
if (env.VERCEL_DEPLOYMENT_ID) resourceAttrs["vercel.deployment_id"] = env.VERCEL_DEPLOYMENT_ID;
|
|
542
|
+
}
|
|
543
|
+
const resource = resourceFromAttributes(resourceAttrs);
|
|
544
|
+
const traceExporter = new OTLPTraceExporter(config);
|
|
545
|
+
const filteringExporter = createFilteringTraceExporter(traceExporter);
|
|
546
|
+
const spanProcessors = [
|
|
547
|
+
createSpanNamingProcessor(),
|
|
548
|
+
createRequestSpanProcessor(),
|
|
549
|
+
new BatchSpanProcessor(filteringExporter, {
|
|
550
|
+
scheduledDelayMillis: config.scheduledDelayMillis,
|
|
551
|
+
maxQueueSize: config.maxQueueSize,
|
|
552
|
+
maxExportBatchSize: config.maxExportBatchSize,
|
|
553
|
+
exportTimeoutMillis: config.exporterTimeoutMillis
|
|
353
554
|
})
|
|
354
555
|
];
|
|
355
|
-
|
|
356
|
-
serviceName:
|
|
357
|
-
resource
|
|
358
|
-
spanProcessors
|
|
359
|
-
sampler:
|
|
360
|
-
instrumentations:
|
|
556
|
+
sdk = new NodeSDK({
|
|
557
|
+
serviceName: config.serviceName,
|
|
558
|
+
resource,
|
|
559
|
+
spanProcessors,
|
|
560
|
+
sampler: createRuleBasedSampler(runtime),
|
|
561
|
+
instrumentations: getInstrumentations(config)
|
|
361
562
|
});
|
|
362
563
|
try {
|
|
363
|
-
await
|
|
364
|
-
} catch (
|
|
365
|
-
console.warn("Telemetry start failed:",
|
|
366
|
-
}
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
564
|
+
await sdk.start();
|
|
565
|
+
} catch (error) {
|
|
566
|
+
console.warn("Telemetry start failed:", error);
|
|
567
|
+
}
|
|
568
|
+
return runtime;
|
|
569
|
+
};
|
|
570
|
+
const updateTelemetryConfig = (partial) => {
|
|
571
|
+
runtime?.updateConfig(partial);
|
|
572
|
+
};
|
|
573
|
+
const injectTraceHeaders = (headers) => {
|
|
574
|
+
propagation.inject(context.active(), headers);
|
|
575
|
+
return headers;
|
|
576
|
+
};
|
|
577
|
+
const createPropagatingFetch = (fetchImpl = fetch) => {
|
|
578
|
+
return async (input, init) => {
|
|
579
|
+
const baseHeaders = init?.headers ?? (input instanceof Request ? input.headers : void 0);
|
|
580
|
+
const headers = new Headers(baseHeaders);
|
|
581
|
+
propagation.inject(context.active(), headers, {
|
|
582
|
+
set: (carrier, key, value) => {
|
|
583
|
+
carrier.set(key, value);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
return fetchImpl(input, { ...init, headers });
|
|
587
|
+
};
|
|
588
|
+
};
|
|
589
|
+
const shutdownTelemetry = async () => {
|
|
590
|
+
if (sdk) {
|
|
591
|
+
await sdk.shutdown();
|
|
592
|
+
}
|
|
379
593
|
};
|
|
380
594
|
export {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
595
|
+
buildSamplingContextFromAttributes,
|
|
596
|
+
createIgnoreMatcher,
|
|
597
|
+
createPropagatingFetch,
|
|
598
|
+
createSpanNameMatcher,
|
|
599
|
+
createTelemetryRuntime,
|
|
600
|
+
defaultIgnoreConfig,
|
|
601
|
+
injectTraceHeaders,
|
|
602
|
+
loadTelemetryConfigFromEnv,
|
|
603
|
+
matchSamplingRule,
|
|
604
|
+
resolveSamplingRatio,
|
|
605
|
+
resolveTelemetryConfig,
|
|
606
|
+
shutdownTelemetry,
|
|
607
|
+
startTelemetry,
|
|
608
|
+
updateTelemetryConfig
|
|
395
609
|
};
|