@buenojs/bueno 0.8.0

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.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,1097 @@
1
+ /**
2
+ * OpenTelemetry OTLP Trace Exporter
3
+ *
4
+ * Provides distributed tracing with OpenTelemetry Protocol (OTLP) export.
5
+ * Part of Layer 7 (Testing & Observability) implementation.
6
+ */
7
+
8
+ // ============= Types =============
9
+
10
+ /**
11
+ * Span kind enumeration
12
+ */
13
+ export type SpanKind = "server" | "client" | "producer" | "consumer" | "internal";
14
+
15
+ /**
16
+ * Span status code
17
+ */
18
+ export type StatusCode = "ok" | "error" | "unset";
19
+
20
+ /**
21
+ * Event attached to a span
22
+ */
23
+ export interface SpanEvent {
24
+ /** Event name */
25
+ name: string;
26
+ /** Event timestamp in nanoseconds */
27
+ timestamp: number;
28
+ /** Event attributes */
29
+ attributes?: Record<string, string | number | boolean>;
30
+ }
31
+
32
+ /**
33
+ * Span status
34
+ */
35
+ export interface SpanStatus {
36
+ /** Status code */
37
+ code: StatusCode;
38
+ /** Optional status message */
39
+ message?: string;
40
+ }
41
+
42
+ /**
43
+ * Span represents a unit of work in distributed tracing
44
+ */
45
+ export interface Span {
46
+ /** Unique trace identifier (32 hex characters) */
47
+ traceId: string;
48
+ /** Unique span identifier (16 hex characters) */
49
+ spanId: string;
50
+ /** Parent span identifier if this is a child span */
51
+ parentSpanId?: string;
52
+ /** Span name (operation name) */
53
+ name: string;
54
+ /** Span kind */
55
+ kind: SpanKind;
56
+ /** Start time in nanoseconds */
57
+ startTime: number;
58
+ /** End time in nanoseconds */
59
+ endTime?: number;
60
+ /** Duration in nanoseconds */
61
+ duration?: number;
62
+ /** Span attributes */
63
+ attributes: Record<string, string | number | boolean>;
64
+ /** Events recorded during the span */
65
+ events: SpanEvent[];
66
+ /** Span status */
67
+ status: SpanStatus;
68
+ /** Whether the span has ended */
69
+ ended: boolean;
70
+ }
71
+
72
+ /**
73
+ * Options for starting a new span
74
+ */
75
+ export interface SpanOptions {
76
+ /** Span kind */
77
+ kind?: SpanKind;
78
+ /** Parent span (if any) */
79
+ parent?: Span;
80
+ /** Initial attributes */
81
+ attributes?: Record<string, string | number | boolean>;
82
+ /** Links to other spans */
83
+ links?: Array<{ traceId: string; spanId: string; attributes?: Record<string, string | number | boolean> }>;
84
+ /** Start time in nanoseconds (defaults to current time) */
85
+ startTime?: number;
86
+ }
87
+
88
+ /**
89
+ * OTLP Exporter options
90
+ */
91
+ export interface OTLPExporterOptions {
92
+ /** OTLP endpoint URL (e.g., http://localhost:4318/v1/traces) */
93
+ endpoint: string;
94
+ /** Additional headers to send with requests */
95
+ headers?: Record<string, string>;
96
+ /** Export interval in milliseconds (default: 5000) */
97
+ exportInterval?: number;
98
+ /** Maximum batch size before forcing export (default: 100) */
99
+ maxBatchSize?: number;
100
+ /** Maximum retry attempts on failure (default: 3) */
101
+ maxRetries?: number;
102
+ /** Initial retry delay in milliseconds (default: 1000) */
103
+ retryDelay?: number;
104
+ /** Timeout for export requests in milliseconds (default: 30000) */
105
+ timeout?: number;
106
+ }
107
+
108
+ /**
109
+ * Sampler type
110
+ */
111
+ export type SamplerType = "always" | "never" | "probabilistic";
112
+
113
+ /**
114
+ * Tracer options
115
+ */
116
+ export interface TracerOptions {
117
+ /** OTLP exporter instance */
118
+ exporter?: OTLPExporter;
119
+ /** Sampling strategy (default: "always") */
120
+ sampler?: SamplerType;
121
+ /** Probability for probabilistic sampling (0.0 to 1.0) */
122
+ probability?: number;
123
+ /** Service name for resource attributes */
124
+ serviceName?: string;
125
+ /** Additional resource attributes */
126
+ resourceAttributes?: Record<string, string | number | boolean>;
127
+ }
128
+
129
+ /**
130
+ * Trace context for propagation
131
+ */
132
+ export interface TraceContext {
133
+ traceId: string;
134
+ spanId: string;
135
+ traceFlags: number;
136
+ traceState?: string;
137
+ }
138
+
139
+ /**
140
+ * OTLP JSON format types
141
+ */
142
+ interface OTLPAttributeValue {
143
+ stringValue?: string;
144
+ intValue?: number;
145
+ doubleValue?: number;
146
+ boolValue?: boolean;
147
+ }
148
+
149
+ interface OTLPAttribute {
150
+ key: string;
151
+ value: OTLPAttributeValue;
152
+ }
153
+
154
+ interface OTLPSpan {
155
+ traceId: string;
156
+ spanId: string;
157
+ parentSpanId?: string;
158
+ name: string;
159
+ kind: number;
160
+ startTimeUnixNano: number;
161
+ endTimeUnixNano: number;
162
+ attributes: OTLPAttribute[];
163
+ events: Array<{
164
+ timeUnixNano: number;
165
+ name: string;
166
+ attributes: OTLPAttribute[];
167
+ }>;
168
+ status: {
169
+ code: number;
170
+ message?: string;
171
+ };
172
+ }
173
+
174
+ interface OTLPResourceSpan {
175
+ resource: {
176
+ attributes: OTLPAttribute[];
177
+ };
178
+ scopeSpans: Array<{
179
+ scope: { name: string };
180
+ spans: OTLPSpan[];
181
+ }>;
182
+ }
183
+
184
+ interface OTLPExportRequest {
185
+ resourceSpans: OTLPResourceSpan[];
186
+ }
187
+
188
+ // ============= Helper Functions =============
189
+
190
+ /**
191
+ * Generate a random trace ID (32 hex characters / 16 bytes)
192
+ */
193
+ export function generateTraceId(): string {
194
+ const bytes = new Uint8Array(16);
195
+ crypto.getRandomValues(bytes);
196
+ return Array.from(bytes)
197
+ .map((b) => b.toString(16).padStart(2, "0"))
198
+ .join("");
199
+ }
200
+
201
+ /**
202
+ * Generate a random span ID (16 hex characters / 8 bytes)
203
+ */
204
+ export function generateSpanId(): string {
205
+ const bytes = new Uint8Array(8);
206
+ crypto.getRandomValues(bytes);
207
+ return Array.from(bytes)
208
+ .map((b) => b.toString(16).padStart(2, "0"))
209
+ .join("");
210
+ }
211
+
212
+ /**
213
+ * Convert hex string to base64
214
+ */
215
+ function hexToBase64(hex: string): string {
216
+ const bytes = new Uint8Array(hex.length / 2);
217
+ for (let i = 0; i < hex.length; i += 2) {
218
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
219
+ }
220
+ return btoa(String.fromCharCode(...bytes));
221
+ }
222
+
223
+ /**
224
+ * Get current time in nanoseconds using Bun.nanoseconds()
225
+ */
226
+ export function nowNanoseconds(): number {
227
+ return Bun.nanoseconds();
228
+ }
229
+
230
+ /**
231
+ * Convert an attribute value to OTLP format
232
+ */
233
+ function toOTLPAttribute(key: string, value: string | number | boolean): OTLPAttribute {
234
+ if (typeof value === "string") {
235
+ return { key, value: { stringValue: value } };
236
+ } else if (typeof value === "number") {
237
+ // Check if it's an integer or float
238
+ if (Number.isInteger(value)) {
239
+ return { key, value: { intValue: value } };
240
+ }
241
+ return { key, value: { doubleValue: value } };
242
+ } else if (typeof value === "boolean") {
243
+ return { key, value: { boolValue: value } };
244
+ }
245
+ return { key, value: { stringValue: String(value) } };
246
+ }
247
+
248
+ /**
249
+ * Map span kind to OTLP kind number
250
+ */
251
+ function spanKindToOTLP(kind: SpanKind): number {
252
+ const kindMap: Record<SpanKind, number> = {
253
+ internal: 1,
254
+ server: 2,
255
+ client: 3,
256
+ producer: 4,
257
+ consumer: 5,
258
+ };
259
+ return kindMap[kind] ?? 1;
260
+ }
261
+
262
+ /**
263
+ * Map status code to OTLP status number
264
+ */
265
+ function statusCodeToOTLP(code: StatusCode): number {
266
+ const codeMap: Record<StatusCode, number> = {
267
+ unset: 0,
268
+ ok: 1,
269
+ error: 2,
270
+ };
271
+ return codeMap[code];
272
+ }
273
+
274
+ // ============= OTLPExporter Class =============
275
+
276
+ /**
277
+ * OTLP HTTP Trace Exporter
278
+ *
279
+ * Exports traces to an OTLP-compatible endpoint using HTTP/JSON.
280
+ */
281
+ export class OTLPExporter {
282
+ private endpoint: string;
283
+ private headers: Record<string, string>;
284
+ private exportInterval: number;
285
+ private maxBatchSize: number;
286
+ private maxRetries: number;
287
+ private retryDelay: number;
288
+ private timeout: number;
289
+ private pendingSpans: Span[] = [];
290
+ private exportTimer: Timer | null = null;
291
+ private isShuttingDown = false;
292
+ private serviceName: string = "unknown-service";
293
+ private resourceAttributes: Record<string, string | number | boolean> = {};
294
+
295
+ constructor(options: OTLPExporterOptions) {
296
+ this.endpoint = options.endpoint;
297
+ this.headers = {
298
+ "Content-Type": "application/json",
299
+ ...options.headers,
300
+ };
301
+ this.exportInterval = options.exportInterval ?? 5000;
302
+ this.maxBatchSize = options.maxBatchSize ?? 100;
303
+ this.maxRetries = options.maxRetries ?? 3;
304
+ this.retryDelay = options.retryDelay ?? 1000;
305
+ this.timeout = options.timeout ?? 30000;
306
+ }
307
+
308
+ /**
309
+ * Set service name for resource attributes
310
+ */
311
+ setServiceName(name: string): void {
312
+ this.serviceName = name;
313
+ }
314
+
315
+ /**
316
+ * Set resource attributes
317
+ */
318
+ setResourceAttributes(attributes: Record<string, string | number | boolean>): void {
319
+ this.resourceAttributes = { ...attributes };
320
+ }
321
+
322
+ /**
323
+ * Start periodic export
324
+ */
325
+ start(): void {
326
+ if (this.exportTimer !== null) return;
327
+
328
+ this.exportTimer = setInterval(() => {
329
+ this.flush().catch(() => {
330
+ // Ignore errors in periodic flush
331
+ });
332
+ }, this.exportInterval);
333
+ }
334
+
335
+ /**
336
+ * Stop periodic export
337
+ */
338
+ stop(): void {
339
+ if (this.exportTimer !== null) {
340
+ clearInterval(this.exportTimer);
341
+ this.exportTimer = null;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Add a span to the pending batch
347
+ */
348
+ addSpan(span: Span): void {
349
+ if (this.isShuttingDown) return;
350
+
351
+ this.pendingSpans.push(span);
352
+
353
+ // Force export if batch is full
354
+ if (this.pendingSpans.length >= this.maxBatchSize) {
355
+ this.flush().catch(() => {
356
+ // Ignore errors
357
+ });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Export spans to OTLP endpoint
363
+ */
364
+ async export(spans: Span[]): Promise<boolean> {
365
+ if (spans.length === 0) return true;
366
+
367
+ const exportRequest = this.buildExportRequest(spans);
368
+
369
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
370
+ try {
371
+ const controller = new AbortController();
372
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
373
+
374
+ const response = await fetch(this.endpoint, {
375
+ method: "POST",
376
+ headers: this.headers,
377
+ body: JSON.stringify(exportRequest),
378
+ signal: controller.signal,
379
+ });
380
+
381
+ clearTimeout(timeoutId);
382
+
383
+ if (response.ok) {
384
+ return true;
385
+ }
386
+
387
+ // Don't retry on client errors (4xx)
388
+ if (response.status >= 400 && response.status < 500) {
389
+ console.error(`OTLP export failed with status ${response.status}`);
390
+ return false;
391
+ }
392
+
393
+ // Retry on server errors (5xx)
394
+ if (attempt < this.maxRetries - 1) {
395
+ await this.delay(this.retryDelay * Math.pow(2, attempt));
396
+ }
397
+ } catch (error) {
398
+ if (attempt < this.maxRetries - 1) {
399
+ await this.delay(this.retryDelay * Math.pow(2, attempt));
400
+ } else {
401
+ console.error("OTLP export failed:", error);
402
+ return false;
403
+ }
404
+ }
405
+ }
406
+
407
+ return false;
408
+ }
409
+
410
+ /**
411
+ * Flush all pending spans
412
+ */
413
+ async flush(): Promise<void> {
414
+ if (this.pendingSpans.length === 0) return;
415
+
416
+ const spansToExport = [...this.pendingSpans];
417
+ this.pendingSpans = [];
418
+
419
+ await this.export(spansToExport);
420
+ }
421
+
422
+ /**
423
+ * Close the exporter and flush remaining spans
424
+ */
425
+ async close(): Promise<void> {
426
+ this.isShuttingDown = true;
427
+ this.stop();
428
+ await this.flush();
429
+ }
430
+
431
+ /**
432
+ * Build OTLP export request from spans
433
+ */
434
+ private buildExportRequest(spans: Span[]): OTLPExportRequest {
435
+ const resourceAttributes: OTLPAttribute[] = [
436
+ toOTLPAttribute("service.name", this.serviceName),
437
+ ];
438
+
439
+ // Add custom resource attributes
440
+ for (const [key, value] of Object.entries(this.resourceAttributes)) {
441
+ resourceAttributes.push(toOTLPAttribute(key, value));
442
+ }
443
+
444
+ const otlpSpans: OTLPSpan[] = spans.map((span) => ({
445
+ traceId: hexToBase64(span.traceId),
446
+ spanId: hexToBase64(span.spanId),
447
+ parentSpanId: span.parentSpanId ? hexToBase64(span.parentSpanId) : undefined,
448
+ name: span.name,
449
+ kind: spanKindToOTLP(span.kind),
450
+ startTimeUnixNano: span.startTime,
451
+ endTimeUnixNano: span.endTime ?? span.startTime,
452
+ attributes: Object.entries(span.attributes).map(([k, v]) => toOTLPAttribute(k, v)),
453
+ events: span.events.map((event) => ({
454
+ timeUnixNano: event.timestamp,
455
+ name: event.name,
456
+ attributes: event.attributes
457
+ ? Object.entries(event.attributes).map(([k, v]) => toOTLPAttribute(k, v))
458
+ : [],
459
+ })),
460
+ status: {
461
+ code: statusCodeToOTLP(span.status.code),
462
+ message: span.status.message,
463
+ },
464
+ }));
465
+
466
+ return {
467
+ resourceSpans: [
468
+ {
469
+ resource: {
470
+ attributes: resourceAttributes,
471
+ },
472
+ scopeSpans: [
473
+ {
474
+ scope: { name: "bueno-tracer" },
475
+ spans: otlpSpans,
476
+ },
477
+ ],
478
+ },
479
+ ],
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Delay helper
485
+ */
486
+ private delay(ms: number): Promise<void> {
487
+ return new Promise((resolve) => setTimeout(resolve, ms));
488
+ }
489
+ }
490
+
491
+ // ============= Tracer Class =============
492
+
493
+ /**
494
+ * Tracer creates and manages spans for distributed tracing
495
+ */
496
+ export class Tracer {
497
+ private serviceName: string;
498
+ private exporter?: OTLPExporter;
499
+ private sampler: SamplerType;
500
+ private probability: number;
501
+ private resourceAttributes: Record<string, string | number | boolean>;
502
+ private currentSpan: Span | null = null;
503
+ private spanStack: Span[] = [];
504
+
505
+ constructor(options: TracerOptions = {}) {
506
+ this.serviceName = options.serviceName ?? "unknown-service";
507
+ this.exporter = options.exporter;
508
+ this.sampler = options.sampler ?? "always";
509
+ this.probability = options.probability ?? 1.0;
510
+ this.resourceAttributes = options.resourceAttributes ?? {};
511
+
512
+ // Configure exporter with service name
513
+ if (this.exporter) {
514
+ this.exporter.setServiceName(this.serviceName);
515
+ this.exporter.setResourceAttributes(this.resourceAttributes);
516
+ this.exporter.start();
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Check if a span should be sampled
522
+ */
523
+ private shouldSample(): boolean {
524
+ switch (this.sampler) {
525
+ case "always":
526
+ return true;
527
+ case "never":
528
+ return false;
529
+ case "probabilistic":
530
+ return Math.random() < this.probability;
531
+ default:
532
+ return true;
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Start a new span
538
+ */
539
+ startSpan(name: string, options: SpanOptions = {}): Span {
540
+ // Check sampling
541
+ if (!this.shouldSample()) {
542
+ // Return a no-op span that won't be exported
543
+ return this.createNoopSpan(name, options);
544
+ }
545
+
546
+ // Determine parent: explicit parent > current span > none
547
+ const parent = options.parent ?? this.currentSpan;
548
+
549
+ const span: Span = {
550
+ traceId: parent?.traceId ?? generateTraceId(),
551
+ spanId: generateSpanId(),
552
+ parentSpanId: parent?.spanId,
553
+ name,
554
+ kind: options.kind ?? "internal",
555
+ startTime: options.startTime ?? nowNanoseconds(),
556
+ attributes: { ...options.attributes },
557
+ events: [],
558
+ status: { code: "unset" },
559
+ ended: false,
560
+ };
561
+
562
+ return span;
563
+ }
564
+
565
+ /**
566
+ * End a span
567
+ */
568
+ endSpan(span: Span, endTime?: number): void {
569
+ if (span.ended) return;
570
+
571
+ span.ended = true;
572
+ span.endTime = endTime ?? nowNanoseconds();
573
+ span.duration = span.endTime - span.startTime;
574
+
575
+ // Export the span
576
+ if (this.exporter) {
577
+ this.exporter.addSpan(span);
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Get the current active span
583
+ */
584
+ getCurrentSpan(): Span | null {
585
+ return this.currentSpan;
586
+ }
587
+
588
+ /**
589
+ * Run a function within span context
590
+ */
591
+ async withSpan<T>(
592
+ name: string,
593
+ fn: (span: Span) => T | Promise<T>,
594
+ options: SpanOptions = {},
595
+ ): Promise<T> {
596
+ const span = this.startSpan(name, options);
597
+
598
+ // Push to stack
599
+ const previousSpan = this.currentSpan;
600
+ this.currentSpan = span;
601
+ this.spanStack.push(span);
602
+
603
+ try {
604
+ const result = await fn(span);
605
+ return result;
606
+ } catch (error) {
607
+ // Record error on span
608
+ this.setError(span, error as Error);
609
+ throw error;
610
+ } finally {
611
+ this.endSpan(span);
612
+
613
+ // Pop from stack
614
+ this.spanStack.pop();
615
+ this.currentSpan = previousSpan ?? this.spanStack[this.spanStack.length - 1] ?? null;
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Add an event to a span
621
+ */
622
+ addEvent(span: Span, name: string, attributes?: Record<string, string | number | boolean>): void {
623
+ if (span.ended) return;
624
+
625
+ span.events.push({
626
+ name,
627
+ timestamp: nowNanoseconds(),
628
+ attributes,
629
+ });
630
+ }
631
+
632
+ /**
633
+ * Set an attribute on a span
634
+ */
635
+ setAttribute(span: Span, key: string, value: string | number | boolean): void {
636
+ if (span.ended) return;
637
+ span.attributes[key] = value;
638
+ }
639
+
640
+ /**
641
+ * Set multiple attributes on a span
642
+ */
643
+ setAttributes(span: Span, attributes: Record<string, string | number | boolean>): void {
644
+ if (span.ended) return;
645
+ Object.assign(span.attributes, attributes);
646
+ }
647
+
648
+ /**
649
+ * Set span status
650
+ */
651
+ setStatus(span: Span, code: StatusCode, message?: string): void {
652
+ if (span.ended) return;
653
+ span.status = { code, message };
654
+ }
655
+
656
+ /**
657
+ * Record an error on a span
658
+ */
659
+ setError(span: Span, error: Error): void {
660
+ if (span.ended) return;
661
+
662
+ span.status = {
663
+ code: "error",
664
+ message: error.message,
665
+ };
666
+
667
+ span.events.push({
668
+ name: "exception",
669
+ timestamp: nowNanoseconds(),
670
+ attributes: {
671
+ "exception.type": error.name,
672
+ "exception.message": error.message,
673
+ "exception.stacktrace": error.stack ?? "",
674
+ },
675
+ });
676
+ }
677
+
678
+ /**
679
+ * Update span name
680
+ */
681
+ updateName(span: Span, name: string): void {
682
+ if (span.ended) return;
683
+ span.name = name;
684
+ }
685
+
686
+ /**
687
+ * Inject trace context into a carrier (for W3C TraceContext propagation)
688
+ */
689
+ injectContext(carrier: Record<string, string>, span?: Span): void {
690
+ const activeSpan = span ?? this.currentSpan;
691
+ if (!activeSpan) return;
692
+
693
+ // traceparent: version-traceid-spanid-flags
694
+ const traceFlags = activeSpan.status.code === "error" ? 0 : 1;
695
+ const traceparent = `00-${activeSpan.traceId}-${activeSpan.spanId}-${traceFlags.toString(16).padStart(2, "0")}`;
696
+
697
+ carrier["traceparent"] = traceparent;
698
+
699
+ // tracestate is optional
700
+ // Could be extended to support tracestate
701
+ }
702
+
703
+ /**
704
+ * Extract trace context from a carrier (for W3C TraceContext propagation)
705
+ */
706
+ extractContext(carrier: Record<string, string>): TraceContext | null {
707
+ const traceparent = carrier["traceparent"] ?? carrier["Traceparent"];
708
+ if (!traceparent) return null;
709
+
710
+ // Parse traceparent: version-traceid-spanid-flags
711
+ const parts = traceparent.split("-");
712
+ if (parts.length !== 4) return null;
713
+
714
+ const [version, traceId, spanId, flags] = parts;
715
+
716
+ // Validate version
717
+ if (version !== "00") return null;
718
+
719
+ // Validate trace ID (32 hex chars)
720
+ if (!/^[0-9a-f]{32}$/i.test(traceId)) return null;
721
+
722
+ // Validate span ID (16 hex chars)
723
+ if (!/^[0-9a-f]{16}$/i.test(spanId)) return null;
724
+
725
+ return {
726
+ traceId,
727
+ spanId,
728
+ traceFlags: parseInt(flags, 16),
729
+ traceState: carrier["tracestate"] ?? carrier["Tracestate"],
730
+ };
731
+ }
732
+
733
+ /**
734
+ * Create a child span from extracted context
735
+ */
736
+ startSpanFromContext(
737
+ name: string,
738
+ context: TraceContext,
739
+ options: SpanOptions = {},
740
+ ): Span {
741
+ return this.startSpan(name, {
742
+ ...options,
743
+ parent: {
744
+ traceId: context.traceId,
745
+ spanId: context.spanId,
746
+ } as Span,
747
+ });
748
+ }
749
+
750
+ /**
751
+ * Flush pending spans
752
+ */
753
+ async flush(): Promise<void> {
754
+ if (this.exporter) {
755
+ await this.exporter.flush();
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Close the tracer
761
+ */
762
+ async close(): Promise<void> {
763
+ if (this.exporter) {
764
+ await this.exporter.close();
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Create a no-op span (not sampled)
770
+ */
771
+ private createNoopSpan(name: string, options: SpanOptions): Span {
772
+ return {
773
+ traceId: generateTraceId(),
774
+ spanId: generateSpanId(),
775
+ parentSpanId: options.parent?.spanId,
776
+ name,
777
+ kind: options.kind ?? "internal",
778
+ startTime: options.startTime ?? nowNanoseconds(),
779
+ attributes: {},
780
+ events: [],
781
+ status: { code: "unset" },
782
+ ended: false,
783
+ };
784
+ }
785
+ }
786
+
787
+ // ============= Factory Functions =============
788
+
789
+ /**
790
+ * Create a configured tracer
791
+ */
792
+ export function createTracer(serviceName: string, options: Omit<TracerOptions, "serviceName"> = {}): Tracer {
793
+ return new Tracer({
794
+ ...options,
795
+ serviceName,
796
+ });
797
+ }
798
+
799
+ // ============= Middleware Helpers =============
800
+
801
+ /**
802
+ * Request context for middleware
803
+ */
804
+ interface RequestContext {
805
+ method: string;
806
+ path: string;
807
+ url: URL;
808
+ headers: Record<string, string>;
809
+ getHeader: (name: string) => string | undefined;
810
+ setHeader: (name: string, value: string) => void;
811
+ status?: number;
812
+ }
813
+
814
+ /**
815
+ * Response context for middleware
816
+ */
817
+ interface ResponseContext {
818
+ status: number;
819
+ headers?: Record<string, string>;
820
+ }
821
+
822
+ /**
823
+ * Create middleware for automatic HTTP tracing
824
+ */
825
+ export function traceMiddleware(tracer: Tracer) {
826
+ return async (
827
+ ctx: RequestContext,
828
+ next: () => Promise<ResponseContext>,
829
+ ): Promise<ResponseContext> => {
830
+ // Extract context from incoming headers
831
+ const headers: Record<string, string> = {};
832
+ for (const [key, value] of Object.entries(ctx.headers)) {
833
+ headers[key.toLowerCase()] = value;
834
+ }
835
+
836
+ const parentContext = tracer.extractContext(headers);
837
+
838
+ // Start span
839
+ const spanOptions: SpanOptions = {
840
+ kind: "server",
841
+ attributes: {
842
+ "http.method": ctx.method,
843
+ "http.url": ctx.url?.toString() ?? ctx.path,
844
+ "http.route": ctx.path,
845
+ },
846
+ };
847
+
848
+ let span: Span;
849
+ if (parentContext) {
850
+ span = tracer.startSpanFromContext(`${ctx.method} ${ctx.path}`, parentContext, spanOptions);
851
+ } else {
852
+ span = tracer.startSpan(`${ctx.method} ${ctx.path}`, spanOptions);
853
+ }
854
+
855
+ // Inject context for downstream services
856
+ const outgoingHeaders: Record<string, string> = {};
857
+ tracer.injectContext(outgoingHeaders, span);
858
+ for (const [key, value] of Object.entries(outgoingHeaders)) {
859
+ ctx.setHeader(key, value);
860
+ }
861
+
862
+ try {
863
+ const response = await next();
864
+
865
+ // Set response attributes
866
+ tracer.setAttribute(span, "http.status_code", response.status);
867
+
868
+ if (response.status >= 400) {
869
+ tracer.setStatus(span, "error");
870
+ } else {
871
+ tracer.setStatus(span, "ok");
872
+ }
873
+
874
+ return response;
875
+ } catch (error) {
876
+ tracer.setError(span, error as Error);
877
+ throw error;
878
+ } finally {
879
+ tracer.endSpan(span);
880
+ }
881
+ };
882
+ }
883
+
884
+ // ============= Database Tracing Helper =============
885
+
886
+ /**
887
+ * Database interface for tracing
888
+ */
889
+ interface TracedDatabase {
890
+ query?: (sql: string, params?: unknown[]) => Promise<unknown>;
891
+ execute?: (sql: string, params?: unknown[]) => Promise<unknown>;
892
+ [key: string]: unknown;
893
+ }
894
+
895
+ /**
896
+ * Wrap database with tracing
897
+ */
898
+ export function traceDatabase(tracer: Tracer, db: TracedDatabase, system: string = "unknown"): TracedDatabase {
899
+ const tracedDb: TracedDatabase = { ...db };
900
+
901
+ // Wrap query method
902
+ if (typeof db.query === "function") {
903
+ const originalQuery = db.query.bind(db);
904
+ tracedDb.query = async (sql: string, params?: unknown[]) => {
905
+ return tracer.withSpan(
906
+ `db.query`,
907
+ async (span) => {
908
+ tracer.setAttributes(span, {
909
+ "db.system": system,
910
+ "db.statement": sql,
911
+ "db.operation": extractOperation(sql),
912
+ });
913
+
914
+ if (params) {
915
+ tracer.setAttribute(span, "db.params", JSON.stringify(params));
916
+ }
917
+
918
+ const result = await originalQuery(sql, params);
919
+ tracer.setStatus(span, "ok");
920
+ return result;
921
+ },
922
+ { kind: "client" },
923
+ );
924
+ };
925
+ }
926
+
927
+ // Wrap execute method
928
+ if (typeof db.execute === "function") {
929
+ const originalExecute = db.execute.bind(db);
930
+ tracedDb.execute = async (sql: string, params?: unknown[]) => {
931
+ return tracer.withSpan(
932
+ `db.execute`,
933
+ async (span) => {
934
+ tracer.setAttributes(span, {
935
+ "db.system": system,
936
+ "db.statement": sql,
937
+ "db.operation": extractOperation(sql),
938
+ });
939
+
940
+ if (params) {
941
+ tracer.setAttribute(span, "db.params", JSON.stringify(params));
942
+ }
943
+
944
+ const result = await originalExecute(sql, params);
945
+ tracer.setStatus(span, "ok");
946
+ return result;
947
+ },
948
+ { kind: "client" },
949
+ );
950
+ };
951
+ }
952
+
953
+ return tracedDb;
954
+ }
955
+
956
+ /**
957
+ * Extract operation type from SQL statement
958
+ */
959
+ function extractOperation(sql: string): string {
960
+ const normalized = sql.trim().toUpperCase();
961
+ const match = normalized.match(/^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE)/);
962
+ return match ? match[1] : "UNKNOWN";
963
+ }
964
+
965
+ // ============= Fetch Tracing Helper =============
966
+
967
+ /**
968
+ * Options for traced fetch
969
+ */
970
+ interface TracedFetchOptions extends RequestInit {
971
+ /** Span to use as parent */
972
+ parentSpan?: Span;
973
+ /** Additional attributes to add to span */
974
+ attributes?: Record<string, string | number | boolean>;
975
+ }
976
+
977
+ /**
978
+ * Create a traced fetch function
979
+ */
980
+ export function createTracedFetch(tracer: Tracer): (url: string | URL, options?: TracedFetchOptions) => Promise<Response> {
981
+ return async (url: string | URL, options: TracedFetchOptions = {}) => {
982
+ const { parentSpan, attributes = {}, ...fetchOptions } = options;
983
+ const urlStr = url.toString();
984
+
985
+ return tracer.withSpan(
986
+ `HTTP ${fetchOptions.method ?? "GET"}`,
987
+ async (span) => {
988
+ tracer.setAttributes(span, {
989
+ "http.method": fetchOptions.method ?? "GET",
990
+ "http.url": urlStr,
991
+ ...attributes,
992
+ });
993
+
994
+ // Inject trace context into headers
995
+ const headers = new Headers(fetchOptions.headers);
996
+ const carrier: Record<string, string> = {};
997
+ tracer.injectContext(carrier, span);
998
+ for (const [key, value] of Object.entries(carrier)) {
999
+ headers.set(key, value);
1000
+ }
1001
+
1002
+ try {
1003
+ const response = await fetch(url, {
1004
+ ...fetchOptions,
1005
+ headers,
1006
+ });
1007
+
1008
+ tracer.setAttribute(span, "http.status_code", response.status);
1009
+ tracer.setStatus(span, response.status >= 400 ? "error" : "ok");
1010
+
1011
+ return response;
1012
+ } catch (error) {
1013
+ tracer.setError(span, error as Error);
1014
+ throw error;
1015
+ }
1016
+ },
1017
+ { kind: "client", parent: parentSpan },
1018
+ );
1019
+ };
1020
+ }
1021
+
1022
+ // ============= Span Builder Helper =============
1023
+
1024
+ /**
1025
+ * Span builder for fluent API
1026
+ */
1027
+ export class SpanBuilder {
1028
+ private span: Span;
1029
+ private tracer: Tracer;
1030
+
1031
+ constructor(tracer: Tracer, name: string, options: SpanOptions = {}) {
1032
+ this.tracer = tracer;
1033
+ this.span = tracer.startSpan(name, options);
1034
+ }
1035
+
1036
+ /**
1037
+ * Set an attribute
1038
+ */
1039
+ setAttribute(key: string, value: string | number | boolean): this {
1040
+ this.tracer.setAttribute(this.span, key, value);
1041
+ return this;
1042
+ }
1043
+
1044
+ /**
1045
+ * Set multiple attributes
1046
+ */
1047
+ setAttributes(attributes: Record<string, string | number | boolean>): this {
1048
+ this.tracer.setAttributes(this.span, attributes);
1049
+ return this;
1050
+ }
1051
+
1052
+ /**
1053
+ * Add an event
1054
+ */
1055
+ addEvent(name: string, attributes?: Record<string, string | number | boolean>): this {
1056
+ this.tracer.addEvent(this.span, name, attributes);
1057
+ return this;
1058
+ }
1059
+
1060
+ /**
1061
+ * Set status
1062
+ */
1063
+ setStatus(code: StatusCode, message?: string): this {
1064
+ this.tracer.setStatus(this.span, code, message);
1065
+ return this;
1066
+ }
1067
+
1068
+ /**
1069
+ * Record error
1070
+ */
1071
+ setError(error: Error): this {
1072
+ this.tracer.setError(this.span, error);
1073
+ return this;
1074
+ }
1075
+
1076
+ /**
1077
+ * End the span
1078
+ */
1079
+ end(): Span {
1080
+ this.tracer.endSpan(this.span);
1081
+ return this.span;
1082
+ }
1083
+
1084
+ /**
1085
+ * Get the underlying span
1086
+ */
1087
+ getSpan(): Span {
1088
+ return this.span;
1089
+ }
1090
+ }
1091
+
1092
+ /**
1093
+ * Create a span builder
1094
+ */
1095
+ export function span(tracer: Tracer, name: string, options: SpanOptions = {}): SpanBuilder {
1096
+ return new SpanBuilder(tracer, name, options);
1097
+ }