@chidchanun/bcp 0.2.16 → 0.2.18
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/README.md +150 -408
- package/docs/README.md +36 -38
- package/docs/api-manifest.json +27 -19
- package/docs/api-reference.md +257 -305
- package/docs/deployment-platform-v2.md +449 -0
- package/docs/docs-web-manifest.json +7 -3
- package/docs/observability-v3.md +402 -0
- package/docs/platform-manifest.json +30 -4
- package/docs/releases/0.2.17.md +166 -0
- package/docs/releases/0.2.18.md +136 -0
- package/package.json +11 -6
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/auth.mjs +1391 -0
- package/packages/client/src/config.mjs +1132 -0
- package/packages/client/src/deployment.mjs +609 -0
- package/packages/client/src/deployment.ts +20 -0
- package/packages/client/src/observability.mjs +1251 -0
- package/packages/client/src/observability.ts +37 -0
- package/packages/client/src/server.mjs +5615 -0
- package/packages/server/src/deployment.ts +936 -0
- package/packages/server/src/middleware.mjs +631 -0
- package/packages/server/src/observability-v3.ts +878 -0
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
// packages/server/src/observability.ts
|
|
2
|
+
var DEFAULT_HISTOGRAM_BUCKETS = [
|
|
3
|
+
5e-3,
|
|
4
|
+
0.01,
|
|
5
|
+
0.025,
|
|
6
|
+
0.05,
|
|
7
|
+
0.1,
|
|
8
|
+
0.25,
|
|
9
|
+
0.5,
|
|
10
|
+
1,
|
|
11
|
+
2.5,
|
|
12
|
+
5,
|
|
13
|
+
10
|
|
14
|
+
];
|
|
15
|
+
function createMetricsRegistry() {
|
|
16
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
17
|
+
return {
|
|
18
|
+
counter(name, options = {}) {
|
|
19
|
+
const definition = getOrCreateSimpleDefinition(
|
|
20
|
+
definitions,
|
|
21
|
+
"counter",
|
|
22
|
+
name,
|
|
23
|
+
options
|
|
24
|
+
);
|
|
25
|
+
return {
|
|
26
|
+
name: definition.name,
|
|
27
|
+
inc(value = 1, labels = {}) {
|
|
28
|
+
assertFiniteNumber(
|
|
29
|
+
value,
|
|
30
|
+
"counter increment"
|
|
31
|
+
);
|
|
32
|
+
if (value < 0) {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
"BCP Observability: counters cannot be decreased."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const series = getSimpleSeries(
|
|
38
|
+
definition,
|
|
39
|
+
labels
|
|
40
|
+
);
|
|
41
|
+
series.value += value;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
gauge(name, options = {}) {
|
|
46
|
+
const definition = getOrCreateSimpleDefinition(
|
|
47
|
+
definitions,
|
|
48
|
+
"gauge",
|
|
49
|
+
name,
|
|
50
|
+
options
|
|
51
|
+
);
|
|
52
|
+
const read = (labels) => getSimpleSeries(
|
|
53
|
+
definition,
|
|
54
|
+
labels
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
name: definition.name,
|
|
58
|
+
set(value, labels = {}) {
|
|
59
|
+
assertFiniteNumber(
|
|
60
|
+
value,
|
|
61
|
+
"gauge value"
|
|
62
|
+
);
|
|
63
|
+
read(labels).value = value;
|
|
64
|
+
},
|
|
65
|
+
inc(value = 1, labels = {}) {
|
|
66
|
+
assertFiniteNumber(
|
|
67
|
+
value,
|
|
68
|
+
"gauge increment"
|
|
69
|
+
);
|
|
70
|
+
read(labels).value += value;
|
|
71
|
+
},
|
|
72
|
+
dec(value = 1, labels = {}) {
|
|
73
|
+
assertFiniteNumber(
|
|
74
|
+
value,
|
|
75
|
+
"gauge decrement"
|
|
76
|
+
);
|
|
77
|
+
read(labels).value -= value;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
histogram(name, options = {}) {
|
|
82
|
+
const metricName = normalizeMetricName(
|
|
83
|
+
name
|
|
84
|
+
);
|
|
85
|
+
const labelNames = normalizeLabelNames(
|
|
86
|
+
options.labelNames
|
|
87
|
+
);
|
|
88
|
+
const buckets = normalizeBuckets(
|
|
89
|
+
options.buckets
|
|
90
|
+
);
|
|
91
|
+
const existing = definitions.get(
|
|
92
|
+
metricName
|
|
93
|
+
);
|
|
94
|
+
let definition;
|
|
95
|
+
if (existing) {
|
|
96
|
+
assertCompatibleDefinition(
|
|
97
|
+
existing,
|
|
98
|
+
"histogram",
|
|
99
|
+
labelNames
|
|
100
|
+
);
|
|
101
|
+
if (existing.type !== "histogram" || !sameNumbers(
|
|
102
|
+
existing.buckets,
|
|
103
|
+
buckets
|
|
104
|
+
)) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`BCP Observability: metric ${metricName} was already registered with different histogram buckets.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
definition = existing;
|
|
110
|
+
} else {
|
|
111
|
+
definition = {
|
|
112
|
+
type: "histogram",
|
|
113
|
+
name: metricName,
|
|
114
|
+
help: normalizeHelp(
|
|
115
|
+
options.help,
|
|
116
|
+
metricName
|
|
117
|
+
),
|
|
118
|
+
labelNames,
|
|
119
|
+
buckets,
|
|
120
|
+
series: /* @__PURE__ */ new Map()
|
|
121
|
+
};
|
|
122
|
+
definitions.set(
|
|
123
|
+
metricName,
|
|
124
|
+
definition
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
name: definition.name,
|
|
129
|
+
observe(value, labels = {}) {
|
|
130
|
+
assertFiniteNumber(
|
|
131
|
+
value,
|
|
132
|
+
"histogram observation"
|
|
133
|
+
);
|
|
134
|
+
const normalizedLabels = normalizeLabels(
|
|
135
|
+
definition.labelNames,
|
|
136
|
+
labels
|
|
137
|
+
);
|
|
138
|
+
const key = serializeLabels(
|
|
139
|
+
definition.labelNames,
|
|
140
|
+
normalizedLabels
|
|
141
|
+
);
|
|
142
|
+
let series = definition.series.get(
|
|
143
|
+
key
|
|
144
|
+
);
|
|
145
|
+
if (!series) {
|
|
146
|
+
series = {
|
|
147
|
+
labels: normalizedLabels,
|
|
148
|
+
count: 0,
|
|
149
|
+
sum: 0,
|
|
150
|
+
buckets: definition.buckets.map(
|
|
151
|
+
() => 0
|
|
152
|
+
)
|
|
153
|
+
};
|
|
154
|
+
definition.series.set(
|
|
155
|
+
key,
|
|
156
|
+
series
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
series.count += 1;
|
|
160
|
+
series.sum += value;
|
|
161
|
+
for (let index = 0; index < definition.buckets.length; index++) {
|
|
162
|
+
if (value <= definition.buckets[index]) {
|
|
163
|
+
series.buckets[index] += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
metrics() {
|
|
170
|
+
return renderMetrics(
|
|
171
|
+
definitions
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
reset() {
|
|
175
|
+
for (const definition of definitions.values()) {
|
|
176
|
+
definition.series.clear();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function createRequestMetricsMiddleware(registry, options = {}) {
|
|
182
|
+
const prefix = normalizePrefix(
|
|
183
|
+
options.prefix
|
|
184
|
+
);
|
|
185
|
+
const labelNames = [];
|
|
186
|
+
if (options.includeMethod !== false) {
|
|
187
|
+
labelNames.push(
|
|
188
|
+
"method"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (options.includeStatus !== false) {
|
|
192
|
+
labelNames.push(
|
|
193
|
+
"status"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const requests = registry.counter(
|
|
197
|
+
`${prefix}http_requests_total`,
|
|
198
|
+
{
|
|
199
|
+
help: "Total HTTP requests observed by BCP middleware.",
|
|
200
|
+
labelNames
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
const duration = registry.histogram(
|
|
204
|
+
`${prefix}http_request_duration_seconds`,
|
|
205
|
+
{
|
|
206
|
+
help: "HTTP request duration observed by BCP middleware.",
|
|
207
|
+
labelNames
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
return async (request, next) => {
|
|
211
|
+
const startedAt = process.hrtime.bigint();
|
|
212
|
+
let status = 500;
|
|
213
|
+
try {
|
|
214
|
+
const response = await next();
|
|
215
|
+
status = response.status;
|
|
216
|
+
return response;
|
|
217
|
+
} finally {
|
|
218
|
+
const elapsed = Number(
|
|
219
|
+
process.hrtime.bigint() - startedAt
|
|
220
|
+
) / 1e9;
|
|
221
|
+
const labels = {};
|
|
222
|
+
if (options.includeMethod !== false) {
|
|
223
|
+
labels.method = request.method.toUpperCase();
|
|
224
|
+
}
|
|
225
|
+
if (options.includeStatus !== false) {
|
|
226
|
+
labels.status = String(status);
|
|
227
|
+
}
|
|
228
|
+
requests.inc(
|
|
229
|
+
1,
|
|
230
|
+
labels
|
|
231
|
+
);
|
|
232
|
+
duration.observe(
|
|
233
|
+
elapsed,
|
|
234
|
+
labels
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function createMetricsResponse(registry) {
|
|
240
|
+
return new Response(
|
|
241
|
+
registry.metrics(),
|
|
242
|
+
{
|
|
243
|
+
status: 200,
|
|
244
|
+
headers: {
|
|
245
|
+
"content-type": "text/plain; version=0.0.4; charset=utf-8",
|
|
246
|
+
"cache-control": "no-store"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function createHealthRegistry() {
|
|
252
|
+
const checks = /* @__PURE__ */ new Map();
|
|
253
|
+
return {
|
|
254
|
+
register(name, check, options = {}) {
|
|
255
|
+
const normalizedName = normalizeHealthCheckName(
|
|
256
|
+
name
|
|
257
|
+
);
|
|
258
|
+
const timeoutMs = normalizeHealthTimeout(
|
|
259
|
+
options.timeoutMs
|
|
260
|
+
);
|
|
261
|
+
if (checks.has(
|
|
262
|
+
normalizedName
|
|
263
|
+
)) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`BCP Observability: health check ${normalizedName} is already registered.`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
checks.set(
|
|
269
|
+
normalizedName,
|
|
270
|
+
{
|
|
271
|
+
check,
|
|
272
|
+
timeoutMs
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
return () => {
|
|
276
|
+
checks.delete(
|
|
277
|
+
normalizedName
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
async run() {
|
|
282
|
+
const results = await Promise.all(
|
|
283
|
+
Array.from(
|
|
284
|
+
checks.entries()
|
|
285
|
+
).map(
|
|
286
|
+
async ([
|
|
287
|
+
name,
|
|
288
|
+
definition
|
|
289
|
+
]) => runHealthCheck(
|
|
290
|
+
name,
|
|
291
|
+
definition.check,
|
|
292
|
+
definition.timeoutMs
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
const ok = results.every(
|
|
297
|
+
(item) => item.ok
|
|
298
|
+
);
|
|
299
|
+
return {
|
|
300
|
+
ok,
|
|
301
|
+
status: ok ? "healthy" : "unhealthy",
|
|
302
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
303
|
+
checks: results
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
async response() {
|
|
307
|
+
const report = await this.run();
|
|
308
|
+
return Response.json(
|
|
309
|
+
report,
|
|
310
|
+
{
|
|
311
|
+
status: report.ok ? 200 : 503,
|
|
312
|
+
headers: {
|
|
313
|
+
"cache-control": "no-store"
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
},
|
|
318
|
+
clear() {
|
|
319
|
+
checks.clear();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function getOrCreateSimpleDefinition(definitions, type, name, options) {
|
|
324
|
+
const metricName = normalizeMetricName(
|
|
325
|
+
name
|
|
326
|
+
);
|
|
327
|
+
const labelNames = normalizeLabelNames(
|
|
328
|
+
options.labelNames
|
|
329
|
+
);
|
|
330
|
+
const existing = definitions.get(
|
|
331
|
+
metricName
|
|
332
|
+
);
|
|
333
|
+
if (existing) {
|
|
334
|
+
assertCompatibleDefinition(
|
|
335
|
+
existing,
|
|
336
|
+
type,
|
|
337
|
+
labelNames
|
|
338
|
+
);
|
|
339
|
+
return existing;
|
|
340
|
+
}
|
|
341
|
+
const definition = {
|
|
342
|
+
type,
|
|
343
|
+
name: metricName,
|
|
344
|
+
help: normalizeHelp(
|
|
345
|
+
options.help,
|
|
346
|
+
metricName
|
|
347
|
+
),
|
|
348
|
+
labelNames,
|
|
349
|
+
series: /* @__PURE__ */ new Map()
|
|
350
|
+
};
|
|
351
|
+
definitions.set(
|
|
352
|
+
metricName,
|
|
353
|
+
definition
|
|
354
|
+
);
|
|
355
|
+
return definition;
|
|
356
|
+
}
|
|
357
|
+
function getSimpleSeries(definition, labels) {
|
|
358
|
+
const normalizedLabels = normalizeLabels(
|
|
359
|
+
definition.labelNames,
|
|
360
|
+
labels
|
|
361
|
+
);
|
|
362
|
+
const key = serializeLabels(
|
|
363
|
+
definition.labelNames,
|
|
364
|
+
normalizedLabels
|
|
365
|
+
);
|
|
366
|
+
let series = definition.series.get(
|
|
367
|
+
key
|
|
368
|
+
);
|
|
369
|
+
if (!series) {
|
|
370
|
+
series = {
|
|
371
|
+
labels: normalizedLabels,
|
|
372
|
+
value: 0
|
|
373
|
+
};
|
|
374
|
+
definition.series.set(
|
|
375
|
+
key,
|
|
376
|
+
series
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return series;
|
|
380
|
+
}
|
|
381
|
+
function renderMetrics(definitions) {
|
|
382
|
+
const lines = [];
|
|
383
|
+
for (const definition of Array.from(
|
|
384
|
+
definitions.values()
|
|
385
|
+
).sort(
|
|
386
|
+
(left, right) => left.name.localeCompare(
|
|
387
|
+
right.name
|
|
388
|
+
)
|
|
389
|
+
)) {
|
|
390
|
+
lines.push(
|
|
391
|
+
`# HELP ${definition.name} ${escapeHelp(definition.help)}`,
|
|
392
|
+
`# TYPE ${definition.name} ${definition.type}`
|
|
393
|
+
);
|
|
394
|
+
if (definition.type === "histogram") {
|
|
395
|
+
for (const series of definition.series.values()) {
|
|
396
|
+
for (let index = 0; index < definition.buckets.length; index++) {
|
|
397
|
+
lines.push(
|
|
398
|
+
`${definition.name}_bucket${renderLabels({
|
|
399
|
+
...series.labels,
|
|
400
|
+
le: String(
|
|
401
|
+
definition.buckets[index]
|
|
402
|
+
)
|
|
403
|
+
})} ${series.buckets[index]}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
lines.push(
|
|
407
|
+
`${definition.name}_bucket${renderLabels({
|
|
408
|
+
...series.labels,
|
|
409
|
+
le: "+Inf"
|
|
410
|
+
})} ${series.count}`,
|
|
411
|
+
`${definition.name}_sum${renderLabels(series.labels)} ${series.sum}`,
|
|
412
|
+
`${definition.name}_count${renderLabels(series.labels)} ${series.count}`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
for (const series of definition.series.values()) {
|
|
417
|
+
lines.push(
|
|
418
|
+
`${definition.name}${renderLabels(series.labels)} ${series.value}`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return lines.length > 0 ? `${lines.join("\n")}
|
|
424
|
+
` : "";
|
|
425
|
+
}
|
|
426
|
+
async function runHealthCheck(name, check, timeoutMs) {
|
|
427
|
+
const startedAt = performance.now();
|
|
428
|
+
try {
|
|
429
|
+
const result = await withTimeout(
|
|
430
|
+
Promise.resolve().then(
|
|
431
|
+
check
|
|
432
|
+
),
|
|
433
|
+
timeoutMs
|
|
434
|
+
);
|
|
435
|
+
const normalized = typeof result === "boolean" ? {
|
|
436
|
+
ok: result
|
|
437
|
+
} : result;
|
|
438
|
+
return {
|
|
439
|
+
name,
|
|
440
|
+
ok: normalized.ok === true,
|
|
441
|
+
durationMs: elapsedMilliseconds(
|
|
442
|
+
startedAt
|
|
443
|
+
),
|
|
444
|
+
...normalized.detail ? {
|
|
445
|
+
detail: normalized.detail
|
|
446
|
+
} : {}
|
|
447
|
+
};
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return {
|
|
450
|
+
name,
|
|
451
|
+
ok: false,
|
|
452
|
+
durationMs: elapsedMilliseconds(
|
|
453
|
+
startedAt
|
|
454
|
+
),
|
|
455
|
+
detail: error instanceof Error ? error.message : "Health check failed."
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function withTimeout(promise, timeoutMs) {
|
|
460
|
+
return new Promise(
|
|
461
|
+
(resolve, reject) => {
|
|
462
|
+
const timeout = setTimeout(
|
|
463
|
+
() => reject(
|
|
464
|
+
new Error(
|
|
465
|
+
`Health check timed out after ${timeoutMs}ms.`
|
|
466
|
+
)
|
|
467
|
+
),
|
|
468
|
+
timeoutMs
|
|
469
|
+
);
|
|
470
|
+
timeout.unref?.();
|
|
471
|
+
promise.then(
|
|
472
|
+
(value) => {
|
|
473
|
+
clearTimeout(
|
|
474
|
+
timeout
|
|
475
|
+
);
|
|
476
|
+
resolve(value);
|
|
477
|
+
},
|
|
478
|
+
(error) => {
|
|
479
|
+
clearTimeout(
|
|
480
|
+
timeout
|
|
481
|
+
);
|
|
482
|
+
reject(error);
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
function normalizeMetricName(value) {
|
|
489
|
+
const name = value.trim();
|
|
490
|
+
if (!/^[a-zA-Z_:][a-zA-Z0-9_:]*$/.test(
|
|
491
|
+
name
|
|
492
|
+
)) {
|
|
493
|
+
throw new TypeError(
|
|
494
|
+
`BCP Observability: invalid metric name "${value}".`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return name;
|
|
498
|
+
}
|
|
499
|
+
function normalizeLabelNames(values) {
|
|
500
|
+
const names = Array.from(
|
|
501
|
+
new Set(
|
|
502
|
+
(values ?? []).map(
|
|
503
|
+
(value) => value.trim()
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
);
|
|
507
|
+
for (const name of names) {
|
|
508
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(
|
|
509
|
+
name
|
|
510
|
+
) || name === "le") {
|
|
511
|
+
throw new TypeError(
|
|
512
|
+
`BCP Observability: invalid metric label name "${name}".`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return names;
|
|
517
|
+
}
|
|
518
|
+
function normalizeLabels(labelNames, labels) {
|
|
519
|
+
const received = Object.keys(
|
|
520
|
+
labels
|
|
521
|
+
).sort();
|
|
522
|
+
const expected = [
|
|
523
|
+
...labelNames
|
|
524
|
+
].sort();
|
|
525
|
+
if (received.length !== expected.length || received.some(
|
|
526
|
+
(name, index) => name !== expected[index]
|
|
527
|
+
)) {
|
|
528
|
+
throw new TypeError(
|
|
529
|
+
`BCP Observability: metric labels must match [${labelNames.join(", ")}].`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return Object.fromEntries(
|
|
533
|
+
labelNames.map(
|
|
534
|
+
(name) => [
|
|
535
|
+
name,
|
|
536
|
+
String(
|
|
537
|
+
labels[name]
|
|
538
|
+
)
|
|
539
|
+
]
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
function serializeLabels(labelNames, labels) {
|
|
544
|
+
return labelNames.map(
|
|
545
|
+
(name) => `${name}=${labels[name]}`
|
|
546
|
+
).join("\0");
|
|
547
|
+
}
|
|
548
|
+
function renderLabels(labels) {
|
|
549
|
+
const entries = Object.entries(
|
|
550
|
+
labels
|
|
551
|
+
);
|
|
552
|
+
if (entries.length === 0) {
|
|
553
|
+
return "";
|
|
554
|
+
}
|
|
555
|
+
return `{${entries.map(
|
|
556
|
+
([
|
|
557
|
+
name,
|
|
558
|
+
value
|
|
559
|
+
]) => `${name}="${escapeLabelValue(value)}"`
|
|
560
|
+
).join(",")}}`;
|
|
561
|
+
}
|
|
562
|
+
function normalizeBuckets(values) {
|
|
563
|
+
const buckets = [
|
|
564
|
+
...values ?? DEFAULT_HISTOGRAM_BUCKETS
|
|
565
|
+
].sort(
|
|
566
|
+
(left, right) => left - right
|
|
567
|
+
);
|
|
568
|
+
if (buckets.length === 0 || buckets.some(
|
|
569
|
+
(value) => !Number.isFinite(
|
|
570
|
+
value
|
|
571
|
+
)
|
|
572
|
+
)) {
|
|
573
|
+
throw new TypeError(
|
|
574
|
+
"BCP Observability: histogram buckets must contain finite numbers."
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
return Array.from(
|
|
578
|
+
new Set(
|
|
579
|
+
buckets
|
|
580
|
+
)
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
function assertCompatibleDefinition(existing, expectedType, labelNames) {
|
|
584
|
+
if (existing.type !== expectedType || !sameStrings(
|
|
585
|
+
existing.labelNames,
|
|
586
|
+
labelNames
|
|
587
|
+
)) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`BCP Observability: metric ${existing.name} was already registered with a different definition.`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function normalizeHelp(value, fallback) {
|
|
594
|
+
return value?.trim() || fallback;
|
|
595
|
+
}
|
|
596
|
+
function normalizePrefix(value) {
|
|
597
|
+
if (!value) {
|
|
598
|
+
return "bcp_";
|
|
599
|
+
}
|
|
600
|
+
const prefix = value.trim();
|
|
601
|
+
if (!/^[a-zA-Z_:][a-zA-Z0-9_:]*$/.test(
|
|
602
|
+
prefix
|
|
603
|
+
)) {
|
|
604
|
+
throw new TypeError(
|
|
605
|
+
"BCP Observability: metrics prefix is invalid."
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return prefix.endsWith("_") ? prefix : `${prefix}_`;
|
|
609
|
+
}
|
|
610
|
+
function normalizeHealthCheckName(value) {
|
|
611
|
+
const name = value.trim();
|
|
612
|
+
if (!name) {
|
|
613
|
+
throw new TypeError(
|
|
614
|
+
"BCP Observability: health check name must not be empty."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return name;
|
|
618
|
+
}
|
|
619
|
+
function normalizeHealthTimeout(value) {
|
|
620
|
+
const timeoutMs = value ?? 5e3;
|
|
621
|
+
if (!Number.isFinite(
|
|
622
|
+
timeoutMs
|
|
623
|
+
) || timeoutMs <= 0) {
|
|
624
|
+
throw new TypeError(
|
|
625
|
+
"BCP Observability: health check timeout must be a positive finite number of milliseconds."
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
return Math.floor(
|
|
629
|
+
timeoutMs
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
function assertFiniteNumber(value, label) {
|
|
633
|
+
if (!Number.isFinite(
|
|
634
|
+
value
|
|
635
|
+
)) {
|
|
636
|
+
throw new TypeError(
|
|
637
|
+
`BCP Observability: ${label} must be a finite number.`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function sameStrings(left, right) {
|
|
642
|
+
return left.length === right.length && left.every(
|
|
643
|
+
(value, index) => value === right[index]
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
function sameNumbers(left, right) {
|
|
647
|
+
return left.length === right.length && left.every(
|
|
648
|
+
(value, index) => value === right[index]
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
function escapeHelp(value) {
|
|
652
|
+
return value.replaceAll(
|
|
653
|
+
"\\",
|
|
654
|
+
"\\\\"
|
|
655
|
+
).replaceAll(
|
|
656
|
+
"\n",
|
|
657
|
+
"\\n"
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
function escapeLabelValue(value) {
|
|
661
|
+
return value.replaceAll(
|
|
662
|
+
"\\",
|
|
663
|
+
"\\\\"
|
|
664
|
+
).replaceAll(
|
|
665
|
+
'"',
|
|
666
|
+
'\\"'
|
|
667
|
+
).replaceAll(
|
|
668
|
+
"\n",
|
|
669
|
+
"\\n"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
function elapsedMilliseconds(startedAt) {
|
|
673
|
+
return Math.round(
|
|
674
|
+
(performance.now() - startedAt) * 1e3
|
|
675
|
+
) / 1e3;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// packages/server/src/observability-v3.ts
|
|
679
|
+
import {
|
|
680
|
+
AsyncLocalStorage
|
|
681
|
+
} from "node:async_hooks";
|
|
682
|
+
import {
|
|
683
|
+
randomBytes,
|
|
684
|
+
randomUUID
|
|
685
|
+
} from "node:crypto";
|
|
686
|
+
var TRACEPARENT_PATTERN = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
687
|
+
var traceStorage = new AsyncLocalStorage();
|
|
688
|
+
function createTracer(options = {}) {
|
|
689
|
+
const exporter = options.exporter;
|
|
690
|
+
const now = options.now ?? Date.now;
|
|
691
|
+
const idFactory = options.idFactory ?? createDefaultTraceIdFactory();
|
|
692
|
+
const serviceName = normalizeOptionalText(options.serviceName);
|
|
693
|
+
const defaultAttributes = normalizeAttributes(options.defaultAttributes);
|
|
694
|
+
function startSpan(rawName, spanOptions = {}) {
|
|
695
|
+
const name = normalizeName(rawName, "span name");
|
|
696
|
+
const parent = spanOptions.parent === void 0 ? currentTraceContext() : spanOptions.parent ?? void 0;
|
|
697
|
+
const traceId = parent?.traceId ?? normalizeTraceId(idFactory.traceId());
|
|
698
|
+
const spanId = normalizeSpanId(idFactory.spanId());
|
|
699
|
+
const traceFlags = parent?.traceFlags ?? "01";
|
|
700
|
+
const correlationId = parent?.correlationId ?? normalizeCorrelationId(idFactory.correlationId());
|
|
701
|
+
const tracestate = parent?.tracestate;
|
|
702
|
+
const context = {
|
|
703
|
+
traceId,
|
|
704
|
+
spanId,
|
|
705
|
+
traceFlags,
|
|
706
|
+
correlationId,
|
|
707
|
+
...tracestate ? { tracestate } : {}
|
|
708
|
+
};
|
|
709
|
+
const attributes = {
|
|
710
|
+
...defaultAttributes,
|
|
711
|
+
...normalizeAttributes(spanOptions.attributes)
|
|
712
|
+
};
|
|
713
|
+
const events = [];
|
|
714
|
+
const startedAt = spanOptions.startTime ?? now();
|
|
715
|
+
assertTimestamp(startedAt, "span startTime");
|
|
716
|
+
const kind = spanOptions.kind ?? "internal";
|
|
717
|
+
let status = "unset";
|
|
718
|
+
let statusMessage;
|
|
719
|
+
let ended = false;
|
|
720
|
+
let completed;
|
|
721
|
+
const span = {
|
|
722
|
+
name,
|
|
723
|
+
context,
|
|
724
|
+
parentSpanId: parent?.spanId,
|
|
725
|
+
kind,
|
|
726
|
+
get status() {
|
|
727
|
+
return status;
|
|
728
|
+
},
|
|
729
|
+
get ended() {
|
|
730
|
+
return ended;
|
|
731
|
+
},
|
|
732
|
+
setAttribute(rawAttributeName, value) {
|
|
733
|
+
assertNotEnded();
|
|
734
|
+
attributes[normalizeName(rawAttributeName, "attribute name")] = normalizeAttributeValue(value);
|
|
735
|
+
},
|
|
736
|
+
addEvent(rawEventName, eventAttributes = {}) {
|
|
737
|
+
assertNotEnded();
|
|
738
|
+
const timestamp = now();
|
|
739
|
+
assertTimestamp(timestamp, "event timestamp");
|
|
740
|
+
events.push({
|
|
741
|
+
name: normalizeName(rawEventName, "event name"),
|
|
742
|
+
timestamp,
|
|
743
|
+
attributes: normalizeAttributes(eventAttributes)
|
|
744
|
+
});
|
|
745
|
+
},
|
|
746
|
+
setStatus(nextStatus, message) {
|
|
747
|
+
assertNotEnded();
|
|
748
|
+
status = normalizeStatus(nextStatus);
|
|
749
|
+
statusMessage = normalizeOptionalText(message);
|
|
750
|
+
},
|
|
751
|
+
async end(endTime) {
|
|
752
|
+
if (completed) {
|
|
753
|
+
return cloneTraceSpanRecord(completed);
|
|
754
|
+
}
|
|
755
|
+
const endedAt = endTime ?? now();
|
|
756
|
+
assertTimestamp(endedAt, "span endTime");
|
|
757
|
+
if (endedAt < startedAt) {
|
|
758
|
+
throw new RangeError(
|
|
759
|
+
"BCP Observability: span endTime cannot be earlier than startTime."
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
ended = true;
|
|
763
|
+
completed = {
|
|
764
|
+
name,
|
|
765
|
+
kind,
|
|
766
|
+
traceId,
|
|
767
|
+
spanId,
|
|
768
|
+
...parent?.spanId ? { parentSpanId: parent.spanId } : {},
|
|
769
|
+
traceFlags,
|
|
770
|
+
correlationId,
|
|
771
|
+
...tracestate ? { tracestate } : {},
|
|
772
|
+
...serviceName ? { serviceName } : {},
|
|
773
|
+
startedAt,
|
|
774
|
+
endedAt,
|
|
775
|
+
durationMs: endedAt - startedAt,
|
|
776
|
+
status,
|
|
777
|
+
...statusMessage ? { statusMessage } : {},
|
|
778
|
+
attributes: { ...attributes },
|
|
779
|
+
events: events.map(cloneTraceEvent)
|
|
780
|
+
};
|
|
781
|
+
try {
|
|
782
|
+
await exporter?.export(
|
|
783
|
+
cloneTraceSpanRecord(completed)
|
|
784
|
+
);
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
return cloneTraceSpanRecord(completed);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
return span;
|
|
791
|
+
function assertNotEnded() {
|
|
792
|
+
if (ended) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`BCP Observability: span "${name}" has already ended.`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function withSpan(name, callback, spanOptions = {}) {
|
|
800
|
+
if (typeof callback !== "function") {
|
|
801
|
+
throw new TypeError(
|
|
802
|
+
"BCP Observability: span callback must be a function."
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const span = startSpan(name, spanOptions);
|
|
806
|
+
return await traceStorage.run(
|
|
807
|
+
span.context,
|
|
808
|
+
async () => {
|
|
809
|
+
try {
|
|
810
|
+
const value = await callback(span);
|
|
811
|
+
if (!span.ended && span.status === "unset") {
|
|
812
|
+
span.setStatus("ok");
|
|
813
|
+
}
|
|
814
|
+
return value;
|
|
815
|
+
} catch (error) {
|
|
816
|
+
if (!span.ended) {
|
|
817
|
+
span.setStatus(
|
|
818
|
+
"error",
|
|
819
|
+
error instanceof Error ? error.message : "Span callback failed."
|
|
820
|
+
);
|
|
821
|
+
span.addEvent("exception", {
|
|
822
|
+
"exception.type": error instanceof Error ? error.name : typeof error,
|
|
823
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
throw error;
|
|
827
|
+
} finally {
|
|
828
|
+
if (!span.ended) {
|
|
829
|
+
await span.end();
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
const tracer = {
|
|
836
|
+
startSpan,
|
|
837
|
+
withSpan,
|
|
838
|
+
currentContext() {
|
|
839
|
+
return currentTraceContext();
|
|
840
|
+
},
|
|
841
|
+
runWithContext(context, callback) {
|
|
842
|
+
return runWithTraceContext(context, callback);
|
|
843
|
+
},
|
|
844
|
+
async shutdown() {
|
|
845
|
+
await exporter?.shutdown?.();
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
return tracer;
|
|
849
|
+
}
|
|
850
|
+
function currentTraceContext() {
|
|
851
|
+
const current = traceStorage.getStore();
|
|
852
|
+
return current ? cloneTraceContext(current) : void 0;
|
|
853
|
+
}
|
|
854
|
+
function runWithTraceContext(context, callback) {
|
|
855
|
+
if (typeof callback !== "function") {
|
|
856
|
+
throw new TypeError(
|
|
857
|
+
"BCP Observability: trace context callback must be a function."
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
return traceStorage.run(
|
|
861
|
+
normalizeTraceContext(context),
|
|
862
|
+
callback
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
function createTraceCarrier(context = currentTraceContext()) {
|
|
866
|
+
if (!context) {
|
|
867
|
+
return void 0;
|
|
868
|
+
}
|
|
869
|
+
const normalized = normalizeTraceContext(context);
|
|
870
|
+
return {
|
|
871
|
+
traceparent: formatTraceparent(normalized),
|
|
872
|
+
correlationId: normalized.correlationId,
|
|
873
|
+
...normalized.tracestate ? { tracestate: normalized.tracestate } : {}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function runWithTraceCarrier(carrier, callback) {
|
|
877
|
+
if (!carrier) {
|
|
878
|
+
return callback();
|
|
879
|
+
}
|
|
880
|
+
const context = extractTraceCarrier(carrier);
|
|
881
|
+
return context ? runWithTraceContext(context, callback) : callback();
|
|
882
|
+
}
|
|
883
|
+
function extractTraceCarrier(carrier) {
|
|
884
|
+
const traceparent = carrier.traceparent;
|
|
885
|
+
if (!traceparent) {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
const parsed = parseTraceparent(traceparent);
|
|
889
|
+
if (!parsed) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
const correlationId = typeof carrier.correlationId === "string" ? carrier.correlationId : parsed.traceId;
|
|
893
|
+
const tracestate = typeof carrier.tracestate === "string" ? normalizeOptionalText(carrier.tracestate) : void 0;
|
|
894
|
+
return {
|
|
895
|
+
...parsed,
|
|
896
|
+
correlationId: normalizeCorrelationId(correlationId),
|
|
897
|
+
...tracestate ? { tracestate } : {}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function injectTraceHeaders(headers, context = currentTraceContext()) {
|
|
901
|
+
if (!(headers instanceof Headers)) {
|
|
902
|
+
throw new TypeError(
|
|
903
|
+
"BCP Observability: injectTraceHeaders() requires Headers."
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (!context) {
|
|
907
|
+
return headers;
|
|
908
|
+
}
|
|
909
|
+
const normalized = normalizeTraceContext(context);
|
|
910
|
+
headers.set("traceparent", formatTraceparent(normalized));
|
|
911
|
+
headers.set("x-correlation-id", normalized.correlationId);
|
|
912
|
+
if (normalized.tracestate) {
|
|
913
|
+
headers.set("tracestate", normalized.tracestate);
|
|
914
|
+
} else {
|
|
915
|
+
headers.delete("tracestate");
|
|
916
|
+
}
|
|
917
|
+
return headers;
|
|
918
|
+
}
|
|
919
|
+
function extractTraceHeaders(input) {
|
|
920
|
+
const headers = input instanceof Headers ? input : input.headers;
|
|
921
|
+
if (!(headers instanceof Headers)) {
|
|
922
|
+
throw new TypeError(
|
|
923
|
+
"BCP Observability: extractTraceHeaders() requires Headers or a request-like object."
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
const parsed = parseTraceparent(headers.get("traceparent"));
|
|
927
|
+
if (!parsed) {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
const correlationId = headers.get("x-correlation-id") ?? parsed.traceId;
|
|
931
|
+
const tracestate = normalizeOptionalText(
|
|
932
|
+
headers.get("tracestate") ?? void 0
|
|
933
|
+
);
|
|
934
|
+
return {
|
|
935
|
+
...parsed,
|
|
936
|
+
correlationId: normalizeCorrelationId(correlationId),
|
|
937
|
+
...tracestate ? { tracestate } : {}
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function formatTraceparent(context) {
|
|
941
|
+
return [
|
|
942
|
+
"00",
|
|
943
|
+
normalizeTraceId(context.traceId),
|
|
944
|
+
normalizeSpanId(context.spanId),
|
|
945
|
+
normalizeTraceFlags(context.traceFlags)
|
|
946
|
+
].join("-");
|
|
947
|
+
}
|
|
948
|
+
function parseTraceparent(value) {
|
|
949
|
+
if (!value) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
const match = TRACEPARENT_PATTERN.exec(
|
|
953
|
+
value.trim().toLowerCase()
|
|
954
|
+
);
|
|
955
|
+
if (!match || /^0+$/.test(match[1]) || /^0+$/.test(match[2])) {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
traceId: match[1],
|
|
960
|
+
spanId: match[2],
|
|
961
|
+
traceFlags: match[3]
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function createRequestTracingMiddleware(tracer, options = {}) {
|
|
965
|
+
if (!tracer) {
|
|
966
|
+
throw new TypeError(
|
|
967
|
+
"BCP Observability: request tracing requires a tracer."
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
const middleware = async (request, next, _context) => {
|
|
971
|
+
const incoming = extractTraceHeaders(request.headers);
|
|
972
|
+
const spanName = typeof options.spanName === "function" ? options.spanName(request) : options.spanName ?? `HTTP ${request.method.toUpperCase()}`;
|
|
973
|
+
const optionAttributes = typeof options.attributes === "function" ? options.attributes(request) : options.attributes ?? {};
|
|
974
|
+
return tracer.withSpan(
|
|
975
|
+
spanName,
|
|
976
|
+
async (span) => {
|
|
977
|
+
span.setAttribute(
|
|
978
|
+
"http.request.method",
|
|
979
|
+
request.method.toUpperCase()
|
|
980
|
+
);
|
|
981
|
+
span.setAttribute(
|
|
982
|
+
"url.path",
|
|
983
|
+
request.nextUrl.pathname
|
|
984
|
+
);
|
|
985
|
+
let response;
|
|
986
|
+
try {
|
|
987
|
+
response = await next();
|
|
988
|
+
span.setAttribute(
|
|
989
|
+
"http.response.status_code",
|
|
990
|
+
response.status
|
|
991
|
+
);
|
|
992
|
+
if (response.status >= 500) {
|
|
993
|
+
span.setStatus("error", `HTTP ${response.status}`);
|
|
994
|
+
}
|
|
995
|
+
} catch (error) {
|
|
996
|
+
span.setStatus(
|
|
997
|
+
"error",
|
|
998
|
+
error instanceof Error ? error.message : "HTTP request failed."
|
|
999
|
+
);
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
return options.includeResponseHeaders === false ? response : cloneResponseWithTraceHeaders(
|
|
1003
|
+
response,
|
|
1004
|
+
span.context
|
|
1005
|
+
);
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
parent: incoming,
|
|
1009
|
+
kind: "server",
|
|
1010
|
+
attributes: optionAttributes
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
};
|
|
1014
|
+
return middleware;
|
|
1015
|
+
}
|
|
1016
|
+
function createMemoryTraceSpanExporter() {
|
|
1017
|
+
const records = [];
|
|
1018
|
+
return {
|
|
1019
|
+
export(span) {
|
|
1020
|
+
records.push(cloneTraceSpanRecord(span));
|
|
1021
|
+
},
|
|
1022
|
+
spans() {
|
|
1023
|
+
return records.map(cloneTraceSpanRecord);
|
|
1024
|
+
},
|
|
1025
|
+
clear() {
|
|
1026
|
+
records.length = 0;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function createTraceMetricsExporter(registry, options = {}) {
|
|
1031
|
+
const prefix = normalizeMetricPrefix(
|
|
1032
|
+
options.prefix ?? "bcp_trace"
|
|
1033
|
+
);
|
|
1034
|
+
const labelNames = [
|
|
1035
|
+
"kind",
|
|
1036
|
+
"status",
|
|
1037
|
+
...options.includeSpanName ? ["span"] : []
|
|
1038
|
+
];
|
|
1039
|
+
const spans = registry.counter(
|
|
1040
|
+
`${prefix}_spans_total`,
|
|
1041
|
+
{
|
|
1042
|
+
help: "Completed BCP trace spans.",
|
|
1043
|
+
labelNames
|
|
1044
|
+
}
|
|
1045
|
+
);
|
|
1046
|
+
const duration = registry.histogram(
|
|
1047
|
+
`${prefix}_span_duration_seconds`,
|
|
1048
|
+
{
|
|
1049
|
+
help: "BCP trace span duration in seconds.",
|
|
1050
|
+
labelNames
|
|
1051
|
+
}
|
|
1052
|
+
);
|
|
1053
|
+
return {
|
|
1054
|
+
export(span) {
|
|
1055
|
+
const labels = {
|
|
1056
|
+
kind: span.kind,
|
|
1057
|
+
status: span.status
|
|
1058
|
+
};
|
|
1059
|
+
if (options.includeSpanName) {
|
|
1060
|
+
labels.span = span.name;
|
|
1061
|
+
}
|
|
1062
|
+
spans.inc(1, labels);
|
|
1063
|
+
duration.observe(span.durationMs / 1e3, labels);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function createCompositeTraceSpanExporter(exporters) {
|
|
1068
|
+
const list = [...exporters];
|
|
1069
|
+
return {
|
|
1070
|
+
async export(span) {
|
|
1071
|
+
for (const exporter of list) {
|
|
1072
|
+
await exporter.export(cloneTraceSpanRecord(span));
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
1075
|
+
async shutdown() {
|
|
1076
|
+
for (const exporter of [...list].reverse()) {
|
|
1077
|
+
await exporter.shutdown?.();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function getTraceLogFields(context = currentTraceContext()) {
|
|
1083
|
+
if (!context) {
|
|
1084
|
+
return {};
|
|
1085
|
+
}
|
|
1086
|
+
const normalized = normalizeTraceContext(context);
|
|
1087
|
+
return {
|
|
1088
|
+
traceId: normalized.traceId,
|
|
1089
|
+
spanId: normalized.spanId,
|
|
1090
|
+
correlationId: normalized.correlationId
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function createDefaultTraceIdFactory() {
|
|
1094
|
+
return {
|
|
1095
|
+
traceId() {
|
|
1096
|
+
return randomBytes(16).toString("hex");
|
|
1097
|
+
},
|
|
1098
|
+
spanId() {
|
|
1099
|
+
return randomBytes(8).toString("hex");
|
|
1100
|
+
},
|
|
1101
|
+
correlationId() {
|
|
1102
|
+
return randomUUID();
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function cloneResponseWithTraceHeaders(response, context) {
|
|
1107
|
+
const headers = new Headers(response.headers);
|
|
1108
|
+
injectTraceHeaders(headers, context);
|
|
1109
|
+
return new Response(response.body, {
|
|
1110
|
+
status: response.status,
|
|
1111
|
+
statusText: response.statusText,
|
|
1112
|
+
headers
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
function normalizeTraceContext(context) {
|
|
1116
|
+
const tracestate = normalizeOptionalText(context.tracestate);
|
|
1117
|
+
return {
|
|
1118
|
+
traceId: normalizeTraceId(context.traceId),
|
|
1119
|
+
spanId: normalizeSpanId(context.spanId),
|
|
1120
|
+
traceFlags: normalizeTraceFlags(context.traceFlags),
|
|
1121
|
+
correlationId: normalizeCorrelationId(context.correlationId),
|
|
1122
|
+
...tracestate ? { tracestate } : {}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function cloneTraceContext(context) {
|
|
1126
|
+
return { ...context };
|
|
1127
|
+
}
|
|
1128
|
+
function cloneTraceSpanRecord(span) {
|
|
1129
|
+
return {
|
|
1130
|
+
...span,
|
|
1131
|
+
attributes: { ...span.attributes },
|
|
1132
|
+
events: span.events.map(cloneTraceEvent)
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function cloneTraceEvent(event) {
|
|
1136
|
+
return {
|
|
1137
|
+
...event,
|
|
1138
|
+
attributes: { ...event.attributes }
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function normalizeAttributes(attributes) {
|
|
1142
|
+
const normalized = {};
|
|
1143
|
+
for (const [rawName, value] of Object.entries(attributes ?? {})) {
|
|
1144
|
+
normalized[normalizeName(rawName, "attribute name")] = normalizeAttributeValue(value);
|
|
1145
|
+
}
|
|
1146
|
+
return normalized;
|
|
1147
|
+
}
|
|
1148
|
+
function normalizeAttributeValue(value) {
|
|
1149
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
1150
|
+
throw new TypeError(
|
|
1151
|
+
"BCP Observability: trace attributes must be string, number or boolean values."
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
1155
|
+
throw new TypeError(
|
|
1156
|
+
"BCP Observability: numeric trace attributes must be finite."
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
return value;
|
|
1160
|
+
}
|
|
1161
|
+
function normalizeStatus(status) {
|
|
1162
|
+
if (status !== "unset" && status !== "ok" && status !== "error") {
|
|
1163
|
+
throw new TypeError(
|
|
1164
|
+
"BCP Observability: invalid span status."
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
return status;
|
|
1168
|
+
}
|
|
1169
|
+
function normalizeTraceId(value) {
|
|
1170
|
+
const normalized = value.trim().toLowerCase();
|
|
1171
|
+
if (!/^[0-9a-f]{32}$/.test(normalized) || /^0+$/.test(normalized)) {
|
|
1172
|
+
throw new TypeError(
|
|
1173
|
+
"BCP Observability: traceId must be 32 non-zero hexadecimal characters."
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
return normalized;
|
|
1177
|
+
}
|
|
1178
|
+
function normalizeSpanId(value) {
|
|
1179
|
+
const normalized = value.trim().toLowerCase();
|
|
1180
|
+
if (!/^[0-9a-f]{16}$/.test(normalized) || /^0+$/.test(normalized)) {
|
|
1181
|
+
throw new TypeError(
|
|
1182
|
+
"BCP Observability: spanId must be 16 non-zero hexadecimal characters."
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
return normalized;
|
|
1186
|
+
}
|
|
1187
|
+
function normalizeTraceFlags(value) {
|
|
1188
|
+
const normalized = value.trim().toLowerCase();
|
|
1189
|
+
if (!/^[0-9a-f]{2}$/.test(normalized)) {
|
|
1190
|
+
throw new TypeError(
|
|
1191
|
+
"BCP Observability: traceFlags must be two hexadecimal characters."
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
return normalized;
|
|
1195
|
+
}
|
|
1196
|
+
function normalizeCorrelationId(value) {
|
|
1197
|
+
return normalizeName(value, "correlationId");
|
|
1198
|
+
}
|
|
1199
|
+
function normalizeName(value, field) {
|
|
1200
|
+
const normalized = String(value ?? "").trim();
|
|
1201
|
+
if (!normalized) {
|
|
1202
|
+
throw new TypeError(
|
|
1203
|
+
`BCP Observability: ${field} must be a non-empty string.`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
return normalized;
|
|
1207
|
+
}
|
|
1208
|
+
function normalizeOptionalText(value) {
|
|
1209
|
+
if (value === void 0) {
|
|
1210
|
+
return void 0;
|
|
1211
|
+
}
|
|
1212
|
+
const normalized = value.trim();
|
|
1213
|
+
return normalized || void 0;
|
|
1214
|
+
}
|
|
1215
|
+
function assertTimestamp(value, field) {
|
|
1216
|
+
if (!Number.isFinite(value)) {
|
|
1217
|
+
throw new TypeError(
|
|
1218
|
+
`BCP Observability: ${field} must be a finite number.`
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function normalizeMetricPrefix(value) {
|
|
1223
|
+
const normalized = value.trim();
|
|
1224
|
+
if (!/^[a-zA-Z_:][a-zA-Z0-9_:]*$/.test(normalized)) {
|
|
1225
|
+
throw new TypeError(
|
|
1226
|
+
"BCP Observability: trace metrics prefix contains unsupported characters."
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
return normalized;
|
|
1230
|
+
}
|
|
1231
|
+
export {
|
|
1232
|
+
createCompositeTraceSpanExporter,
|
|
1233
|
+
createHealthRegistry,
|
|
1234
|
+
createMemoryTraceSpanExporter,
|
|
1235
|
+
createMetricsRegistry,
|
|
1236
|
+
createMetricsResponse,
|
|
1237
|
+
createRequestMetricsMiddleware,
|
|
1238
|
+
createRequestTracingMiddleware,
|
|
1239
|
+
createTraceCarrier,
|
|
1240
|
+
createTraceMetricsExporter,
|
|
1241
|
+
createTracer,
|
|
1242
|
+
currentTraceContext,
|
|
1243
|
+
extractTraceCarrier,
|
|
1244
|
+
extractTraceHeaders,
|
|
1245
|
+
formatTraceparent,
|
|
1246
|
+
getTraceLogFields,
|
|
1247
|
+
injectTraceHeaders,
|
|
1248
|
+
parseTraceparent,
|
|
1249
|
+
runWithTraceCarrier,
|
|
1250
|
+
runWithTraceContext
|
|
1251
|
+
};
|