@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.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- 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
|
+
}
|