@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.
@@ -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
+ };