@0xchain/telemetry 1.1.0-beta.18 → 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/ignore-BvHZTNsF.js +151 -0
- package/dist/index.js +556 -348
- 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,401 +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
|
-
if (
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
243
|
}
|
|
135
|
-
|
|
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 };
|
|
136
253
|
}
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
return { decision:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
E,
|
|
146
|
-
m.defaultSamplingRatio ?? 0.01
|
|
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
|
|
147
262
|
);
|
|
148
|
-
return
|
|
263
|
+
return getSamplerForRatio(ratio).shouldSample(context2, traceId, spanName, spanKind, attributes, links);
|
|
149
264
|
},
|
|
150
265
|
toString: () => "RuleBasedSampler"
|
|
151
266
|
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 ?? []]);
|
|
182
309
|
return {
|
|
183
|
-
paths:
|
|
184
|
-
extensions:
|
|
185
|
-
methods:
|
|
186
|
-
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
|
|
187
314
|
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
210
345
|
} : void 0
|
|
211
346
|
};
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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 } },
|
|
226
366
|
{ name: "error", ratio: 1, when: { minStatusCode: 500 } }
|
|
227
367
|
];
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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] }
|
|
242
388
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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] }
|
|
249
397
|
});
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
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);
|
|
253
408
|
return {
|
|
254
|
-
...
|
|
255
|
-
serviceName:
|
|
256
|
-
serviceVersion:
|
|
257
|
-
environment
|
|
258
|
-
defaultSamplingRatio
|
|
259
|
-
ignore
|
|
260
|
-
samplingRules
|
|
261
|
-
inspectionHeader:
|
|
262
|
-
userTypeHeader:
|
|
263
|
-
requestStartHeader:
|
|
264
|
-
scheduledDelayMillis:
|
|
265
|
-
maxQueueSize:
|
|
266
|
-
maxExportBatchSize:
|
|
267
|
-
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
|
|
268
423
|
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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);
|
|
274
431
|
return {
|
|
275
|
-
getConfig: () =>
|
|
276
|
-
updateConfig: (
|
|
277
|
-
|
|
278
|
-
...
|
|
279
|
-
...
|
|
280
|
-
ignore:
|
|
432
|
+
getConfig: () => current,
|
|
433
|
+
updateConfig: (partial) => {
|
|
434
|
+
current = normalizeResolvedConfig({
|
|
435
|
+
...current,
|
|
436
|
+
...partial,
|
|
437
|
+
ignore: mergeIgnoreConfig(current.ignore, partial.ignore)
|
|
281
438
|
});
|
|
282
439
|
}
|
|
283
440
|
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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") {
|
|
301
464
|
try {
|
|
302
|
-
return new URL(
|
|
465
|
+
return new URL(url).pathname;
|
|
303
466
|
} catch {
|
|
304
|
-
return
|
|
467
|
+
return url.split("?")[0];
|
|
305
468
|
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"http.status_code",
|
|
332
|
-
"telemetry.response.status_code"
|
|
333
|
-
]);
|
|
334
|
-
if (typeof r == "number" && (t.attributes["telemetry.response.status_code"] = r), t.status.code === Q.ERROR || typeof r == "number" && r >= 500) {
|
|
335
|
-
t.attributes["telemetry.request.error"] = !0;
|
|
336
|
-
const s = t.status.message || _t(t);
|
|
337
|
-
s && (t.attributes["telemetry.request.error_message"] = s);
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
shutdown: () => Promise.resolve(),
|
|
341
|
-
forceFlush: () => Promise.resolve()
|
|
342
|
-
}), xt = async (t) => {
|
|
343
|
-
const e = await Y(t);
|
|
344
|
-
h = await Tt(e);
|
|
345
|
-
const r = process.env, n = {
|
|
346
|
-
[k]: e.serviceName,
|
|
347
|
-
[B]: e.serviceVersion || "1.0.0",
|
|
348
|
-
[rt]: e.environment || r.VERCEL_ENV || r.NODE_ENV || "development"
|
|
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()
|
|
349
494
|
};
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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()
|
|
524
|
+
};
|
|
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
|
|
359
554
|
})
|
|
360
555
|
];
|
|
361
|
-
|
|
362
|
-
serviceName:
|
|
363
|
-
resource
|
|
364
|
-
spanProcessors
|
|
365
|
-
sampler:
|
|
366
|
-
instrumentations:
|
|
556
|
+
sdk = new NodeSDK({
|
|
557
|
+
serviceName: config.serviceName,
|
|
558
|
+
resource,
|
|
559
|
+
spanProcessors,
|
|
560
|
+
sampler: createRuleBasedSampler(runtime),
|
|
561
|
+
instrumentations: getInstrumentations(config)
|
|
367
562
|
});
|
|
368
563
|
try {
|
|
369
|
-
await
|
|
370
|
-
} catch (
|
|
371
|
-
console.warn("Telemetry start failed:",
|
|
372
|
-
}
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
}
|
|
385
593
|
};
|
|
386
594
|
export {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
401
609
|
};
|