@chidchanun/bcp 0.2.16 → 0.2.17
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 +152 -271
- package/docs/README.md +40 -34
- package/docs/api-manifest.json +13 -13
- package/docs/api-reference.md +225 -283
- package/docs/docs-web-manifest.json +4 -2
- package/docs/observability-v3.md +402 -0
- package/docs/platform-manifest.json +16 -4
- package/docs/releases/0.2.17.md +166 -0
- package/package.json +2 -2
- package/packages/client/src/observability.mjs +1251 -0
- package/packages/client/src/observability.ts +37 -0
- package/packages/server/src/observability-v3.ts +878 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AsyncLocalStorage,
|
|
3
|
+
} from "node:async_hooks";
|
|
4
|
+
import {
|
|
5
|
+
randomBytes,
|
|
6
|
+
randomUUID,
|
|
7
|
+
} from "node:crypto";
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
MiddlewareContext,
|
|
11
|
+
MiddlewareNext,
|
|
12
|
+
MiddlewarePipelineHandler,
|
|
13
|
+
MiddlewareRequest,
|
|
14
|
+
} from "./middleware.js";
|
|
15
|
+
|
|
16
|
+
export type TraceAttributeValue =
|
|
17
|
+
| string
|
|
18
|
+
| number
|
|
19
|
+
| boolean;
|
|
20
|
+
|
|
21
|
+
export type TraceAttributes =
|
|
22
|
+
Readonly<Record<string, TraceAttributeValue>>;
|
|
23
|
+
|
|
24
|
+
export type TraceSpanKind =
|
|
25
|
+
| "internal"
|
|
26
|
+
| "server"
|
|
27
|
+
| "client"
|
|
28
|
+
| "producer"
|
|
29
|
+
| "consumer";
|
|
30
|
+
|
|
31
|
+
export type TraceSpanStatus =
|
|
32
|
+
| "unset"
|
|
33
|
+
| "ok"
|
|
34
|
+
| "error";
|
|
35
|
+
|
|
36
|
+
export interface TraceContext {
|
|
37
|
+
traceId: string;
|
|
38
|
+
spanId: string;
|
|
39
|
+
traceFlags: string;
|
|
40
|
+
correlationId: string;
|
|
41
|
+
tracestate?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TraceCarrier {
|
|
45
|
+
traceparent: string;
|
|
46
|
+
correlationId: string;
|
|
47
|
+
tracestate?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TraceSpanEvent {
|
|
51
|
+
name: string;
|
|
52
|
+
timestamp: number;
|
|
53
|
+
attributes: Record<string, TraceAttributeValue>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TraceSpanRecord {
|
|
57
|
+
name: string;
|
|
58
|
+
kind: TraceSpanKind;
|
|
59
|
+
traceId: string;
|
|
60
|
+
spanId: string;
|
|
61
|
+
parentSpanId?: string;
|
|
62
|
+
traceFlags: string;
|
|
63
|
+
correlationId: string;
|
|
64
|
+
tracestate?: string;
|
|
65
|
+
serviceName?: string;
|
|
66
|
+
startedAt: number;
|
|
67
|
+
endedAt: number;
|
|
68
|
+
durationMs: number;
|
|
69
|
+
status: TraceSpanStatus;
|
|
70
|
+
statusMessage?: string;
|
|
71
|
+
attributes: Record<string, TraceAttributeValue>;
|
|
72
|
+
events: TraceSpanEvent[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TraceSpanExporter {
|
|
76
|
+
export(span: TraceSpanRecord): void | Promise<void>;
|
|
77
|
+
shutdown?(): void | Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface MemoryTraceSpanExporter
|
|
81
|
+
extends TraceSpanExporter {
|
|
82
|
+
spans(): TraceSpanRecord[];
|
|
83
|
+
clear(): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TraceIdFactory {
|
|
87
|
+
traceId(): string;
|
|
88
|
+
spanId(): string;
|
|
89
|
+
correlationId(): string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface StartTraceSpanOptions {
|
|
93
|
+
parent?: TraceContext | null;
|
|
94
|
+
kind?: TraceSpanKind;
|
|
95
|
+
attributes?: TraceAttributes;
|
|
96
|
+
startTime?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface TraceSpan {
|
|
100
|
+
readonly name: string;
|
|
101
|
+
readonly context: TraceContext;
|
|
102
|
+
readonly parentSpanId: string | undefined;
|
|
103
|
+
readonly kind: TraceSpanKind;
|
|
104
|
+
readonly status: TraceSpanStatus;
|
|
105
|
+
readonly ended: boolean;
|
|
106
|
+
setAttribute(name: string, value: TraceAttributeValue): void;
|
|
107
|
+
addEvent(name: string, attributes?: TraceAttributes): void;
|
|
108
|
+
setStatus(status: TraceSpanStatus, message?: string): void;
|
|
109
|
+
end(endTime?: number): Promise<TraceSpanRecord>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface TracerOptions {
|
|
113
|
+
exporter?: TraceSpanExporter;
|
|
114
|
+
serviceName?: string;
|
|
115
|
+
now?: () => number;
|
|
116
|
+
idFactory?: TraceIdFactory;
|
|
117
|
+
defaultAttributes?: TraceAttributes;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface Tracer {
|
|
121
|
+
startSpan(name: string, options?: StartTraceSpanOptions): TraceSpan;
|
|
122
|
+
withSpan<T>(
|
|
123
|
+
name: string,
|
|
124
|
+
callback: (span: TraceSpan) => T | Promise<T>,
|
|
125
|
+
options?: StartTraceSpanOptions
|
|
126
|
+
): Promise<T>;
|
|
127
|
+
currentContext(): TraceContext | undefined;
|
|
128
|
+
runWithContext<T>(context: TraceContext, callback: () => T): T;
|
|
129
|
+
shutdown(): Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface RequestTracingOptions {
|
|
133
|
+
spanName?: string | ((request: MiddlewareRequest) => string);
|
|
134
|
+
includeResponseHeaders?: boolean;
|
|
135
|
+
attributes?:
|
|
136
|
+
| TraceAttributes
|
|
137
|
+
| ((request: MiddlewareRequest) => TraceAttributes);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface TraceMetricsRegistryLike {
|
|
141
|
+
counter(
|
|
142
|
+
name: string,
|
|
143
|
+
options?: {
|
|
144
|
+
help?: string;
|
|
145
|
+
labelNames?: readonly string[];
|
|
146
|
+
}
|
|
147
|
+
): {
|
|
148
|
+
inc(value?: number, labels?: TraceAttributes): void;
|
|
149
|
+
};
|
|
150
|
+
histogram(
|
|
151
|
+
name: string,
|
|
152
|
+
options?: {
|
|
153
|
+
help?: string;
|
|
154
|
+
labelNames?: readonly string[];
|
|
155
|
+
buckets?: readonly number[];
|
|
156
|
+
}
|
|
157
|
+
): {
|
|
158
|
+
observe(value: number, labels?: TraceAttributes): void;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface TraceMetricsOptions {
|
|
163
|
+
prefix?: string;
|
|
164
|
+
includeSpanName?: boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const TRACEPARENT_PATTERN =
|
|
168
|
+
/^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
169
|
+
|
|
170
|
+
const traceStorage =
|
|
171
|
+
new AsyncLocalStorage<TraceContext>();
|
|
172
|
+
|
|
173
|
+
export function createTracer(
|
|
174
|
+
options: TracerOptions = {}
|
|
175
|
+
): Tracer {
|
|
176
|
+
const exporter = options.exporter;
|
|
177
|
+
const now = options.now ?? Date.now;
|
|
178
|
+
const idFactory =
|
|
179
|
+
options.idFactory ?? createDefaultTraceIdFactory();
|
|
180
|
+
const serviceName =
|
|
181
|
+
normalizeOptionalText(options.serviceName);
|
|
182
|
+
const defaultAttributes =
|
|
183
|
+
normalizeAttributes(options.defaultAttributes);
|
|
184
|
+
|
|
185
|
+
function startSpan(
|
|
186
|
+
rawName: string,
|
|
187
|
+
spanOptions: StartTraceSpanOptions = {}
|
|
188
|
+
): TraceSpan {
|
|
189
|
+
const name = normalizeName(rawName, "span name");
|
|
190
|
+
const parent =
|
|
191
|
+
spanOptions.parent === undefined
|
|
192
|
+
? currentTraceContext()
|
|
193
|
+
: spanOptions.parent ?? undefined;
|
|
194
|
+
const traceId = parent?.traceId ??
|
|
195
|
+
normalizeTraceId(idFactory.traceId());
|
|
196
|
+
const spanId =
|
|
197
|
+
normalizeSpanId(idFactory.spanId());
|
|
198
|
+
const traceFlags = parent?.traceFlags ?? "01";
|
|
199
|
+
const correlationId = parent?.correlationId ??
|
|
200
|
+
normalizeCorrelationId(idFactory.correlationId());
|
|
201
|
+
const tracestate = parent?.tracestate;
|
|
202
|
+
const context: TraceContext = {
|
|
203
|
+
traceId,
|
|
204
|
+
spanId,
|
|
205
|
+
traceFlags,
|
|
206
|
+
correlationId,
|
|
207
|
+
...(tracestate ? { tracestate } : {}),
|
|
208
|
+
};
|
|
209
|
+
const attributes = {
|
|
210
|
+
...defaultAttributes,
|
|
211
|
+
...normalizeAttributes(spanOptions.attributes),
|
|
212
|
+
};
|
|
213
|
+
const events: TraceSpanEvent[] = [];
|
|
214
|
+
const startedAt = spanOptions.startTime ?? now();
|
|
215
|
+
assertTimestamp(startedAt, "span startTime");
|
|
216
|
+
const kind = spanOptions.kind ?? "internal";
|
|
217
|
+
let status: TraceSpanStatus = "unset";
|
|
218
|
+
let statusMessage: string | undefined;
|
|
219
|
+
let ended = false;
|
|
220
|
+
let completed: TraceSpanRecord | undefined;
|
|
221
|
+
|
|
222
|
+
const span: TraceSpan = {
|
|
223
|
+
name,
|
|
224
|
+
context,
|
|
225
|
+
parentSpanId: parent?.spanId,
|
|
226
|
+
kind,
|
|
227
|
+
get status(): TraceSpanStatus {
|
|
228
|
+
return status;
|
|
229
|
+
},
|
|
230
|
+
get ended(): boolean {
|
|
231
|
+
return ended;
|
|
232
|
+
},
|
|
233
|
+
setAttribute(
|
|
234
|
+
rawAttributeName: string,
|
|
235
|
+
value: TraceAttributeValue
|
|
236
|
+
): void {
|
|
237
|
+
assertNotEnded();
|
|
238
|
+
attributes[
|
|
239
|
+
normalizeName(rawAttributeName, "attribute name")
|
|
240
|
+
] = normalizeAttributeValue(value);
|
|
241
|
+
},
|
|
242
|
+
addEvent(
|
|
243
|
+
rawEventName: string,
|
|
244
|
+
eventAttributes: TraceAttributes = {}
|
|
245
|
+
): void {
|
|
246
|
+
assertNotEnded();
|
|
247
|
+
const timestamp = now();
|
|
248
|
+
assertTimestamp(timestamp, "event timestamp");
|
|
249
|
+
events.push({
|
|
250
|
+
name: normalizeName(rawEventName, "event name"),
|
|
251
|
+
timestamp,
|
|
252
|
+
attributes: normalizeAttributes(eventAttributes),
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
setStatus(
|
|
256
|
+
nextStatus: TraceSpanStatus,
|
|
257
|
+
message?: string
|
|
258
|
+
): void {
|
|
259
|
+
assertNotEnded();
|
|
260
|
+
status = normalizeStatus(nextStatus);
|
|
261
|
+
statusMessage = normalizeOptionalText(message);
|
|
262
|
+
},
|
|
263
|
+
async end(endTime?: number): Promise<TraceSpanRecord> {
|
|
264
|
+
if (completed) {
|
|
265
|
+
return cloneTraceSpanRecord(completed);
|
|
266
|
+
}
|
|
267
|
+
const endedAt = endTime ?? now();
|
|
268
|
+
assertTimestamp(endedAt, "span endTime");
|
|
269
|
+
if (endedAt < startedAt) {
|
|
270
|
+
throw new RangeError(
|
|
271
|
+
"BCP Observability: span endTime cannot be earlier than startTime."
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
ended = true;
|
|
275
|
+
completed = {
|
|
276
|
+
name,
|
|
277
|
+
kind,
|
|
278
|
+
traceId,
|
|
279
|
+
spanId,
|
|
280
|
+
...(parent?.spanId
|
|
281
|
+
? { parentSpanId: parent.spanId }
|
|
282
|
+
: {}),
|
|
283
|
+
traceFlags,
|
|
284
|
+
correlationId,
|
|
285
|
+
...(tracestate ? { tracestate } : {}),
|
|
286
|
+
...(serviceName ? { serviceName } : {}),
|
|
287
|
+
startedAt,
|
|
288
|
+
endedAt,
|
|
289
|
+
durationMs: endedAt - startedAt,
|
|
290
|
+
status,
|
|
291
|
+
...(statusMessage ? { statusMessage } : {}),
|
|
292
|
+
attributes: { ...attributes },
|
|
293
|
+
events: events.map(cloneTraceEvent),
|
|
294
|
+
};
|
|
295
|
+
try {
|
|
296
|
+
await exporter?.export(
|
|
297
|
+
cloneTraceSpanRecord(completed)
|
|
298
|
+
);
|
|
299
|
+
} catch {
|
|
300
|
+
// Exporters must not break application execution.
|
|
301
|
+
}
|
|
302
|
+
return cloneTraceSpanRecord(completed);
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return span;
|
|
307
|
+
|
|
308
|
+
function assertNotEnded(): void {
|
|
309
|
+
if (ended) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`BCP Observability: span "${name}" has already ended.`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function withSpan<T>(
|
|
318
|
+
name: string,
|
|
319
|
+
callback: (span: TraceSpan) => T | Promise<T>,
|
|
320
|
+
spanOptions: StartTraceSpanOptions = {}
|
|
321
|
+
): Promise<T> {
|
|
322
|
+
if (typeof callback !== "function") {
|
|
323
|
+
throw new TypeError(
|
|
324
|
+
"BCP Observability: span callback must be a function."
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const span = startSpan(name, spanOptions);
|
|
328
|
+
return await traceStorage.run(
|
|
329
|
+
span.context,
|
|
330
|
+
async (): Promise<T> => {
|
|
331
|
+
try {
|
|
332
|
+
const value = await callback(span);
|
|
333
|
+
if (
|
|
334
|
+
!span.ended &&
|
|
335
|
+
span.status === "unset"
|
|
336
|
+
) {
|
|
337
|
+
span.setStatus("ok");
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (!span.ended) {
|
|
342
|
+
span.setStatus(
|
|
343
|
+
"error",
|
|
344
|
+
error instanceof Error
|
|
345
|
+
? error.message
|
|
346
|
+
: "Span callback failed."
|
|
347
|
+
);
|
|
348
|
+
span.addEvent("exception", {
|
|
349
|
+
"exception.type":
|
|
350
|
+
error instanceof Error
|
|
351
|
+
? error.name
|
|
352
|
+
: typeof error,
|
|
353
|
+
"exception.message":
|
|
354
|
+
error instanceof Error
|
|
355
|
+
? error.message
|
|
356
|
+
: String(error),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
360
|
+
} finally {
|
|
361
|
+
if (!span.ended) {
|
|
362
|
+
await span.end();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const tracer: Tracer = {
|
|
370
|
+
startSpan,
|
|
371
|
+
withSpan,
|
|
372
|
+
currentContext(): TraceContext | undefined {
|
|
373
|
+
return currentTraceContext();
|
|
374
|
+
},
|
|
375
|
+
runWithContext<T>(
|
|
376
|
+
context: TraceContext,
|
|
377
|
+
callback: () => T
|
|
378
|
+
): T {
|
|
379
|
+
return runWithTraceContext(context, callback);
|
|
380
|
+
},
|
|
381
|
+
async shutdown(): Promise<void> {
|
|
382
|
+
await exporter?.shutdown?.();
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return tracer;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function currentTraceContext(): TraceContext | undefined {
|
|
390
|
+
const current = traceStorage.getStore();
|
|
391
|
+
return current ? cloneTraceContext(current) : undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function runWithTraceContext<T>(
|
|
395
|
+
context: TraceContext,
|
|
396
|
+
callback: () => T
|
|
397
|
+
): T {
|
|
398
|
+
if (typeof callback !== "function") {
|
|
399
|
+
throw new TypeError(
|
|
400
|
+
"BCP Observability: trace context callback must be a function."
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return traceStorage.run(
|
|
404
|
+
normalizeTraceContext(context),
|
|
405
|
+
callback
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function createTraceCarrier(
|
|
410
|
+
context: TraceContext | undefined = currentTraceContext()
|
|
411
|
+
): TraceCarrier | undefined {
|
|
412
|
+
if (!context) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
const normalized = normalizeTraceContext(context);
|
|
416
|
+
return {
|
|
417
|
+
traceparent: formatTraceparent(normalized),
|
|
418
|
+
correlationId: normalized.correlationId,
|
|
419
|
+
...(normalized.tracestate
|
|
420
|
+
? { tracestate: normalized.tracestate }
|
|
421
|
+
: {}),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function runWithTraceCarrier<T>(
|
|
426
|
+
carrier: TraceCarrier | undefined | null,
|
|
427
|
+
callback: () => T
|
|
428
|
+
): T {
|
|
429
|
+
if (!carrier) {
|
|
430
|
+
return callback();
|
|
431
|
+
}
|
|
432
|
+
const context = extractTraceCarrier(carrier);
|
|
433
|
+
return context
|
|
434
|
+
? runWithTraceContext(context, callback)
|
|
435
|
+
: callback();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function extractTraceCarrier(
|
|
439
|
+
carrier:
|
|
440
|
+
| TraceCarrier
|
|
441
|
+
| Readonly<Record<string, string | undefined>>
|
|
442
|
+
): TraceContext | null {
|
|
443
|
+
const traceparent = carrier.traceparent;
|
|
444
|
+
if (!traceparent) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const parsed = parseTraceparent(traceparent);
|
|
448
|
+
if (!parsed) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const correlationId =
|
|
452
|
+
typeof carrier.correlationId === "string"
|
|
453
|
+
? carrier.correlationId
|
|
454
|
+
: parsed.traceId;
|
|
455
|
+
const tracestate =
|
|
456
|
+
typeof carrier.tracestate === "string"
|
|
457
|
+
? normalizeOptionalText(carrier.tracestate)
|
|
458
|
+
: undefined;
|
|
459
|
+
return {
|
|
460
|
+
...parsed,
|
|
461
|
+
correlationId: normalizeCorrelationId(correlationId),
|
|
462
|
+
...(tracestate ? { tracestate } : {}),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function injectTraceHeaders(
|
|
467
|
+
headers: Headers,
|
|
468
|
+
context: TraceContext | undefined = currentTraceContext()
|
|
469
|
+
): Headers {
|
|
470
|
+
if (!(headers instanceof Headers)) {
|
|
471
|
+
throw new TypeError(
|
|
472
|
+
"BCP Observability: injectTraceHeaders() requires Headers."
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (!context) {
|
|
476
|
+
return headers;
|
|
477
|
+
}
|
|
478
|
+
const normalized = normalizeTraceContext(context);
|
|
479
|
+
headers.set("traceparent", formatTraceparent(normalized));
|
|
480
|
+
headers.set("x-correlation-id", normalized.correlationId);
|
|
481
|
+
if (normalized.tracestate) {
|
|
482
|
+
headers.set("tracestate", normalized.tracestate);
|
|
483
|
+
} else {
|
|
484
|
+
headers.delete("tracestate");
|
|
485
|
+
}
|
|
486
|
+
return headers;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function extractTraceHeaders(
|
|
490
|
+
input: Headers | { readonly headers: Headers }
|
|
491
|
+
): TraceContext | null {
|
|
492
|
+
const headers =
|
|
493
|
+
input instanceof Headers
|
|
494
|
+
? input
|
|
495
|
+
: input.headers;
|
|
496
|
+
if (!(headers instanceof Headers)) {
|
|
497
|
+
throw new TypeError(
|
|
498
|
+
"BCP Observability: extractTraceHeaders() requires Headers or a request-like object."
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const parsed = parseTraceparent(headers.get("traceparent"));
|
|
502
|
+
if (!parsed) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
const correlationId =
|
|
506
|
+
headers.get("x-correlation-id") ?? parsed.traceId;
|
|
507
|
+
const tracestate = normalizeOptionalText(
|
|
508
|
+
headers.get("tracestate") ?? undefined
|
|
509
|
+
);
|
|
510
|
+
return {
|
|
511
|
+
...parsed,
|
|
512
|
+
correlationId: normalizeCorrelationId(correlationId),
|
|
513
|
+
...(tracestate ? { tracestate } : {}),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function formatTraceparent(
|
|
518
|
+
context: Pick<TraceContext, "traceId" | "spanId" | "traceFlags">
|
|
519
|
+
): string {
|
|
520
|
+
return [
|
|
521
|
+
"00",
|
|
522
|
+
normalizeTraceId(context.traceId),
|
|
523
|
+
normalizeSpanId(context.spanId),
|
|
524
|
+
normalizeTraceFlags(context.traceFlags),
|
|
525
|
+
].join("-");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function parseTraceparent(
|
|
529
|
+
value: string | null | undefined
|
|
530
|
+
): Pick<TraceContext, "traceId" | "spanId" | "traceFlags"> | null {
|
|
531
|
+
if (!value) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const match = TRACEPARENT_PATTERN.exec(
|
|
535
|
+
value.trim().toLowerCase()
|
|
536
|
+
);
|
|
537
|
+
if (!match || /^0+$/.test(match[1]) || /^0+$/.test(match[2])) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
traceId: match[1],
|
|
542
|
+
spanId: match[2],
|
|
543
|
+
traceFlags: match[3],
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function createRequestTracingMiddleware(
|
|
548
|
+
tracer: Tracer,
|
|
549
|
+
options: RequestTracingOptions = {}
|
|
550
|
+
): MiddlewarePipelineHandler {
|
|
551
|
+
if (!tracer) {
|
|
552
|
+
throw new TypeError(
|
|
553
|
+
"BCP Observability: request tracing requires a tracer."
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const middleware: MiddlewarePipelineHandler = async (
|
|
558
|
+
request: MiddlewareRequest,
|
|
559
|
+
next: MiddlewareNext,
|
|
560
|
+
_context: MiddlewareContext
|
|
561
|
+
): Promise<Response> => {
|
|
562
|
+
const incoming = extractTraceHeaders(request.headers);
|
|
563
|
+
const spanName =
|
|
564
|
+
typeof options.spanName === "function"
|
|
565
|
+
? options.spanName(request)
|
|
566
|
+
: options.spanName ??
|
|
567
|
+
`HTTP ${request.method.toUpperCase()}`;
|
|
568
|
+
const optionAttributes =
|
|
569
|
+
typeof options.attributes === "function"
|
|
570
|
+
? options.attributes(request)
|
|
571
|
+
: options.attributes ?? {};
|
|
572
|
+
|
|
573
|
+
return tracer.withSpan(
|
|
574
|
+
spanName,
|
|
575
|
+
async (span: TraceSpan): Promise<Response> => {
|
|
576
|
+
span.setAttribute(
|
|
577
|
+
"http.request.method",
|
|
578
|
+
request.method.toUpperCase()
|
|
579
|
+
);
|
|
580
|
+
span.setAttribute(
|
|
581
|
+
"url.path",
|
|
582
|
+
request.nextUrl.pathname
|
|
583
|
+
);
|
|
584
|
+
let response: Response;
|
|
585
|
+
try {
|
|
586
|
+
response = await next();
|
|
587
|
+
span.setAttribute(
|
|
588
|
+
"http.response.status_code",
|
|
589
|
+
response.status
|
|
590
|
+
);
|
|
591
|
+
if (response.status >= 500) {
|
|
592
|
+
span.setStatus("error", `HTTP ${response.status}`);
|
|
593
|
+
}
|
|
594
|
+
} catch (error) {
|
|
595
|
+
span.setStatus(
|
|
596
|
+
"error",
|
|
597
|
+
error instanceof Error
|
|
598
|
+
? error.message
|
|
599
|
+
: "HTTP request failed."
|
|
600
|
+
);
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
return options.includeResponseHeaders === false
|
|
604
|
+
? response
|
|
605
|
+
: cloneResponseWithTraceHeaders(
|
|
606
|
+
response,
|
|
607
|
+
span.context
|
|
608
|
+
);
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
parent: incoming,
|
|
612
|
+
kind: "server",
|
|
613
|
+
attributes: optionAttributes,
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
return middleware;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function createMemoryTraceSpanExporter(): MemoryTraceSpanExporter {
|
|
622
|
+
const records: TraceSpanRecord[] = [];
|
|
623
|
+
return {
|
|
624
|
+
export(span: TraceSpanRecord): void {
|
|
625
|
+
records.push(cloneTraceSpanRecord(span));
|
|
626
|
+
},
|
|
627
|
+
spans(): TraceSpanRecord[] {
|
|
628
|
+
return records.map(cloneTraceSpanRecord);
|
|
629
|
+
},
|
|
630
|
+
clear(): void {
|
|
631
|
+
records.length = 0;
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function createTraceMetricsExporter(
|
|
637
|
+
registry: TraceMetricsRegistryLike,
|
|
638
|
+
options: TraceMetricsOptions = {}
|
|
639
|
+
): TraceSpanExporter {
|
|
640
|
+
const prefix = normalizeMetricPrefix(
|
|
641
|
+
options.prefix ?? "bcp_trace"
|
|
642
|
+
);
|
|
643
|
+
const labelNames = [
|
|
644
|
+
"kind",
|
|
645
|
+
"status",
|
|
646
|
+
...(options.includeSpanName ? ["span"] : []),
|
|
647
|
+
];
|
|
648
|
+
const spans = registry.counter(
|
|
649
|
+
`${prefix}_spans_total`,
|
|
650
|
+
{
|
|
651
|
+
help: "Completed BCP trace spans.",
|
|
652
|
+
labelNames,
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
const duration = registry.histogram(
|
|
656
|
+
`${prefix}_span_duration_seconds`,
|
|
657
|
+
{
|
|
658
|
+
help: "BCP trace span duration in seconds.",
|
|
659
|
+
labelNames,
|
|
660
|
+
}
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
export(span: TraceSpanRecord): void {
|
|
665
|
+
const labels: Record<string, TraceAttributeValue> = {
|
|
666
|
+
kind: span.kind,
|
|
667
|
+
status: span.status,
|
|
668
|
+
};
|
|
669
|
+
if (options.includeSpanName) {
|
|
670
|
+
labels.span = span.name;
|
|
671
|
+
}
|
|
672
|
+
spans.inc(1, labels);
|
|
673
|
+
duration.observe(span.durationMs / 1000, labels);
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function createCompositeTraceSpanExporter(
|
|
679
|
+
exporters: readonly TraceSpanExporter[]
|
|
680
|
+
): TraceSpanExporter {
|
|
681
|
+
const list = [...exporters];
|
|
682
|
+
return {
|
|
683
|
+
async export(span: TraceSpanRecord): Promise<void> {
|
|
684
|
+
for (const exporter of list) {
|
|
685
|
+
await exporter.export(cloneTraceSpanRecord(span));
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
async shutdown(): Promise<void> {
|
|
689
|
+
for (const exporter of [...list].reverse()) {
|
|
690
|
+
await exporter.shutdown?.();
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function getTraceLogFields(
|
|
697
|
+
context: TraceContext | undefined = currentTraceContext()
|
|
698
|
+
): Record<string, string> {
|
|
699
|
+
if (!context) {
|
|
700
|
+
return {};
|
|
701
|
+
}
|
|
702
|
+
const normalized = normalizeTraceContext(context);
|
|
703
|
+
return {
|
|
704
|
+
traceId: normalized.traceId,
|
|
705
|
+
spanId: normalized.spanId,
|
|
706
|
+
correlationId: normalized.correlationId,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function createDefaultTraceIdFactory(): TraceIdFactory {
|
|
711
|
+
return {
|
|
712
|
+
traceId(): string {
|
|
713
|
+
return randomBytes(16).toString("hex");
|
|
714
|
+
},
|
|
715
|
+
spanId(): string {
|
|
716
|
+
return randomBytes(8).toString("hex");
|
|
717
|
+
},
|
|
718
|
+
correlationId(): string {
|
|
719
|
+
return randomUUID();
|
|
720
|
+
},
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function cloneResponseWithTraceHeaders(
|
|
725
|
+
response: Response,
|
|
726
|
+
context: TraceContext
|
|
727
|
+
): Response {
|
|
728
|
+
const headers = new Headers(response.headers);
|
|
729
|
+
injectTraceHeaders(headers, context);
|
|
730
|
+
return new Response(response.body, {
|
|
731
|
+
status: response.status,
|
|
732
|
+
statusText: response.statusText,
|
|
733
|
+
headers,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function normalizeTraceContext(context: TraceContext): TraceContext {
|
|
738
|
+
const tracestate = normalizeOptionalText(context.tracestate);
|
|
739
|
+
return {
|
|
740
|
+
traceId: normalizeTraceId(context.traceId),
|
|
741
|
+
spanId: normalizeSpanId(context.spanId),
|
|
742
|
+
traceFlags: normalizeTraceFlags(context.traceFlags),
|
|
743
|
+
correlationId: normalizeCorrelationId(context.correlationId),
|
|
744
|
+
...(tracestate ? { tracestate } : {}),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function cloneTraceContext(context: TraceContext): TraceContext {
|
|
749
|
+
return { ...context };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function cloneTraceSpanRecord(span: TraceSpanRecord): TraceSpanRecord {
|
|
753
|
+
return {
|
|
754
|
+
...span,
|
|
755
|
+
attributes: { ...span.attributes },
|
|
756
|
+
events: span.events.map(cloneTraceEvent),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function cloneTraceEvent(event: TraceSpanEvent): TraceSpanEvent {
|
|
761
|
+
return {
|
|
762
|
+
...event,
|
|
763
|
+
attributes: { ...event.attributes },
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function normalizeAttributes(
|
|
768
|
+
attributes: TraceAttributes | undefined
|
|
769
|
+
): Record<string, TraceAttributeValue> {
|
|
770
|
+
const normalized: Record<string, TraceAttributeValue> = {};
|
|
771
|
+
for (const [rawName, value] of Object.entries(attributes ?? {})) {
|
|
772
|
+
normalized[
|
|
773
|
+
normalizeName(rawName, "attribute name")
|
|
774
|
+
] = normalizeAttributeValue(value);
|
|
775
|
+
}
|
|
776
|
+
return normalized;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function normalizeAttributeValue(
|
|
780
|
+
value: TraceAttributeValue
|
|
781
|
+
): TraceAttributeValue {
|
|
782
|
+
if (
|
|
783
|
+
typeof value !== "string" &&
|
|
784
|
+
typeof value !== "number" &&
|
|
785
|
+
typeof value !== "boolean"
|
|
786
|
+
) {
|
|
787
|
+
throw new TypeError(
|
|
788
|
+
"BCP Observability: trace attributes must be string, number or boolean values."
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
792
|
+
throw new TypeError(
|
|
793
|
+
"BCP Observability: numeric trace attributes must be finite."
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function normalizeStatus(status: TraceSpanStatus): TraceSpanStatus {
|
|
800
|
+
if (status !== "unset" && status !== "ok" && status !== "error") {
|
|
801
|
+
throw new TypeError(
|
|
802
|
+
"BCP Observability: invalid span status."
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
return status;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function normalizeTraceId(value: string): string {
|
|
809
|
+
const normalized = value.trim().toLowerCase();
|
|
810
|
+
if (!/^[0-9a-f]{32}$/.test(normalized) || /^0+$/.test(normalized)) {
|
|
811
|
+
throw new TypeError(
|
|
812
|
+
"BCP Observability: traceId must be 32 non-zero hexadecimal characters."
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return normalized;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function normalizeSpanId(value: string): string {
|
|
819
|
+
const normalized = value.trim().toLowerCase();
|
|
820
|
+
if (!/^[0-9a-f]{16}$/.test(normalized) || /^0+$/.test(normalized)) {
|
|
821
|
+
throw new TypeError(
|
|
822
|
+
"BCP Observability: spanId must be 16 non-zero hexadecimal characters."
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
return normalized;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function normalizeTraceFlags(value: string): string {
|
|
829
|
+
const normalized = value.trim().toLowerCase();
|
|
830
|
+
if (!/^[0-9a-f]{2}$/.test(normalized)) {
|
|
831
|
+
throw new TypeError(
|
|
832
|
+
"BCP Observability: traceFlags must be two hexadecimal characters."
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
return normalized;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function normalizeCorrelationId(value: string): string {
|
|
839
|
+
return normalizeName(value, "correlationId");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function normalizeName(value: string, field: string): string {
|
|
843
|
+
const normalized = String(value ?? "").trim();
|
|
844
|
+
if (!normalized) {
|
|
845
|
+
throw new TypeError(
|
|
846
|
+
`BCP Observability: ${field} must be a non-empty string.`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return normalized;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function normalizeOptionalText(
|
|
853
|
+
value: string | undefined
|
|
854
|
+
): string | undefined {
|
|
855
|
+
if (value === undefined) {
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
const normalized = value.trim();
|
|
859
|
+
return normalized || undefined;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function assertTimestamp(value: number, field: string): void {
|
|
863
|
+
if (!Number.isFinite(value)) {
|
|
864
|
+
throw new TypeError(
|
|
865
|
+
`BCP Observability: ${field} must be a finite number.`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function normalizeMetricPrefix(value: string): string {
|
|
871
|
+
const normalized = value.trim();
|
|
872
|
+
if (!/^[a-zA-Z_:][a-zA-Z0-9_:]*$/.test(normalized)) {
|
|
873
|
+
throw new TypeError(
|
|
874
|
+
"BCP Observability: trace metrics prefix contains unsupported characters."
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
return normalized;
|
|
878
|
+
}
|