@stacksjs/bun-router 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/container/service-provider.d.ts +3 -3
- package/dist/development/performance-profiler.d.ts +3 -3
- package/dist/development/route-debugger.d.ts +1 -1
- package/dist/development/route-inspector.d.ts +2 -2
- package/dist/observability/integration.d.ts +1 -1
- package/dist/response/macros.d.ts +1 -1
- package/dist/router/index.d.ts +7 -7
- package/dist/router/router.d.ts +0 -32
- package/dist/testing/performance-testing.d.ts +2 -2
- package/dist/testing/test-client.d.ts +1 -1
- package/dist/testing/websocket-testing.d.ts +1 -1
- package/package.json +2 -3
- package/src/auth.ts +0 -489
- package/src/cache/lru-cache.ts +0 -457
- package/src/cache/middleware-memoization.ts +0 -531
- package/src/cache/route-cache-warmer.ts +0 -486
- package/src/cache/sqlite-cache.ts +0 -783
- package/src/cache/streaming-cache.ts +0 -572
- package/src/cli/colors.ts +0 -29
- package/src/cli/index.ts +0 -287
- package/src/cli/middleware.ts +0 -291
- package/src/cli/openapi.ts +0 -407
- package/src/cli/router.ts +0 -188
- package/src/cli/routes.ts +0 -265
- package/src/cli/utils.ts +0 -531
- package/src/cli.ts +0 -5
- package/src/config.ts +0 -322
- package/src/container/container.ts +0 -786
- package/src/container/contextual-binding.ts +0 -606
- package/src/container/decorators.ts +0 -368
- package/src/container/index.ts +0 -49
- package/src/container/service-provider.ts +0 -596
- package/src/development/hot-reload.ts +0 -673
- package/src/development/index.ts +0 -499
- package/src/development/performance-profiler.ts +0 -717
- package/src/development/route-debugger.ts +0 -527
- package/src/development/route-inspector.ts +0 -749
- package/src/development/typescript-utilities.ts +0 -682
- package/src/docs.ts +0 -397
- package/src/errors/circuit-breaker.ts +0 -733
- package/src/errors/error-handler.ts +0 -569
- package/src/errors/error-reporting.ts +0 -672
- package/src/errors/exceptions.ts +0 -536
- package/src/errors/graceful-degradation.ts +0 -623
- package/src/errors/index.ts +0 -21
- package/src/errors/router-errors.ts +0 -632
- package/src/file-serving/static-files.ts +0 -718
- package/src/index.ts +0 -66
- package/src/middleware/auth.ts +0 -220
- package/src/middleware/content_security_policy.ts +0 -215
- package/src/middleware/cors.ts +0 -142
- package/src/middleware/csrf.ts +0 -166
- package/src/middleware/ddos_protection.ts +0 -279
- package/src/middleware/file_security.ts +0 -191
- package/src/middleware/file_upload.ts +0 -275
- package/src/middleware/helmet.ts +0 -268
- package/src/middleware/index.ts +0 -117
- package/src/middleware/input_validation.ts +0 -453
- package/src/middleware/json_body.ts +0 -37
- package/src/middleware/performance_alerting.ts +0 -540
- package/src/middleware/performance_dashboard.ts +0 -661
- package/src/middleware/performance_monitor.ts +0 -945
- package/src/middleware/pipeline.ts +0 -489
- package/src/middleware/rate_limit.ts +0 -251
- package/src/middleware/request_id.ts +0 -36
- package/src/middleware/request_signing.ts +0 -638
- package/src/middleware/request_tracer.ts +0 -653
- package/src/middleware/response_cache.ts +0 -778
- package/src/middleware/security.ts +0 -482
- package/src/middleware/security_suite.ts +0 -257
- package/src/middleware/session.ts +0 -91
- package/src/model-binding/index.ts +0 -18
- package/src/model-binding/model-middleware.ts +0 -425
- package/src/model-binding/model-registry.ts +0 -550
- package/src/model-binding.ts +0 -370
- package/src/model-resolver-factory.ts +0 -106
- package/src/observability/correlation.ts +0 -691
- package/src/observability/health-checks.ts +0 -729
- package/src/observability/index.ts +0 -184
- package/src/observability/integration.ts +0 -548
- package/src/observability/metrics.ts +0 -753
- package/src/observability/tracing.ts +0 -638
- package/src/optimization/bun-utilities.ts +0 -778
- package/src/query-builder-integration.ts +0 -137
- package/src/request/context.ts +0 -109
- package/src/request/enhanced-request.ts +0 -876
- package/src/request/macros.ts +0 -822
- package/src/response/macros.ts +0 -665
- package/src/response/response-factory.ts +0 -596
- package/src/router/api-routes.ts +0 -243
- package/src/router/file-based-routing.ts +0 -697
- package/src/router/file-streaming.ts +0 -383
- package/src/router/fluent-routing.ts +0 -866
- package/src/router/group-organization.ts +0 -229
- package/src/router/handler-resolver.ts +0 -327
- package/src/router/http-methods.ts +0 -407
- package/src/router/index.ts +0 -193
- package/src/router/middleware-groups.ts +0 -222
- package/src/router/middleware-integration.ts +0 -399
- package/src/router/middleware.ts +0 -234
- package/src/router/model-binding.ts +0 -215
- package/src/router/optimized-route-matching.ts +0 -302
- package/src/router/route-building.ts +0 -221
- package/src/router/route-compiler.ts +0 -691
- package/src/router/route-matching.ts +0 -368
- package/src/router/route-trie.ts +0 -450
- package/src/router/router.ts +0 -1616
- package/src/router/server.ts +0 -684
- package/src/router/validation-integration.ts +0 -449
- package/src/router/view-rendering.ts +0 -233
- package/src/router/websocket.ts +0 -100
- package/src/routing/route-caching.ts +0 -402
- package/src/routing/route-throttling.ts +0 -469
- package/src/routing/subdomain-routing.ts +0 -492
- package/src/session/database-store.ts +0 -109
- package/src/session/file-store.ts +0 -148
- package/src/session/index.ts +0 -244
- package/src/session/memory-store.ts +0 -88
- package/src/session/redis-store.ts +0 -93
- package/src/streaming/index.ts +0 -17
- package/src/streaming/sse-handler.ts +0 -482
- package/src/streaming/stream-handler.ts +0 -552
- package/src/testing/auth-testing.ts +0 -448
- package/src/testing/file-upload-testing.ts +0 -543
- package/src/testing/index.ts +0 -10
- package/src/testing/middleware-testing.ts +0 -322
- package/src/testing/model-binding-testing.ts +0 -645
- package/src/testing/performance-testing.ts +0 -738
- package/src/testing/test-client.ts +0 -310
- package/src/testing/test-request.ts +0 -315
- package/src/testing/test-response.ts +0 -332
- package/src/testing/types.ts +0 -224
- package/src/testing/websocket-testing.ts +0 -590
- package/src/types/controller-types.ts +0 -385
- package/src/types/core.ts +0 -683
- package/src/types/middleware-types.ts +0 -422
- package/src/types/request-response-augmentation.ts +0 -489
- package/src/types/route-inference.ts +0 -372
- package/src/types.ts +0 -2123
- package/src/url.ts +0 -131
- package/src/utils/index.ts +0 -1
- package/src/utils/query-preservation.ts +0 -201
- package/src/utils.ts +0 -395
- package/src/validation/validator.ts +0 -688
- package/src/websocket/clustering.ts +0 -769
|
@@ -1,653 +0,0 @@
|
|
|
1
|
-
import type { EnhancedRequest, NextFunction } from '../types'
|
|
2
|
-
import { config } from '../config'
|
|
3
|
-
|
|
4
|
-
export interface TraceSpan {
|
|
5
|
-
traceId: string
|
|
6
|
-
spanId: string
|
|
7
|
-
parentSpanId?: string
|
|
8
|
-
operationName: string
|
|
9
|
-
startTime: number
|
|
10
|
-
endTime?: number
|
|
11
|
-
duration?: number
|
|
12
|
-
tags: Record<string, unknown>
|
|
13
|
-
logs: Array<{
|
|
14
|
-
timestamp: number
|
|
15
|
-
level: 'debug' | 'info' | 'warn' | 'error'
|
|
16
|
-
message: string
|
|
17
|
-
fields?: Record<string, unknown>
|
|
18
|
-
}>
|
|
19
|
-
status: 'ok' | 'error' | 'timeout'
|
|
20
|
-
error?: string
|
|
21
|
-
stackTrace?: string[]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Destination for exported spans. `endpoint`/`headers` configure the
|
|
26
|
-
* HTTP-based exporters (jaeger/zipkin/otlp); `customExporter` receives
|
|
27
|
-
* the spans directly.
|
|
28
|
-
*/
|
|
29
|
-
export interface TraceExporterConfig {
|
|
30
|
-
type: 'console' | 'jaeger' | 'zipkin' | 'otlp' | 'custom'
|
|
31
|
-
endpoint?: string
|
|
32
|
-
headers?: Record<string, string>
|
|
33
|
-
customExporter?: (spans: TraceSpan[]) => Promise<void>
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface TracingOptions {
|
|
37
|
-
enabled?: boolean
|
|
38
|
-
sampleRate?: number
|
|
39
|
-
maxSpans?: number
|
|
40
|
-
includeHeaders?: boolean
|
|
41
|
-
includeQueryParams?: boolean
|
|
42
|
-
includeRequestBody?: boolean
|
|
43
|
-
includeResponseBody?: boolean
|
|
44
|
-
maxBodySize?: number
|
|
45
|
-
exporters?: TraceExporterConfig[]
|
|
46
|
-
propagation?: {
|
|
47
|
-
enabled?: boolean
|
|
48
|
-
headers?: string[]
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default class RequestTracer {
|
|
53
|
-
private options: TracingOptions
|
|
54
|
-
private spans: Map<string, TraceSpan> = new Map()
|
|
55
|
-
private activeSpans: Map<string, string> = new Map() // requestId -> spanId
|
|
56
|
-
private exportQueue: TraceSpan[] = []
|
|
57
|
-
private exportInterval?: ReturnType<typeof setInterval>
|
|
58
|
-
|
|
59
|
-
constructor(options: TracingOptions = {}) {
|
|
60
|
-
const tracingConfig = (config.server?.performance?.monitoring as any)?.tracing || {}
|
|
61
|
-
|
|
62
|
-
this.options = {
|
|
63
|
-
enabled: options.enabled ?? tracingConfig.enabled ?? false,
|
|
64
|
-
sampleRate: options.sampleRate ?? tracingConfig.sampleRate ?? 1.0,
|
|
65
|
-
maxSpans: options.maxSpans ?? 10000,
|
|
66
|
-
includeHeaders: options.includeHeaders ?? false,
|
|
67
|
-
includeQueryParams: options.includeQueryParams ?? true,
|
|
68
|
-
includeRequestBody: options.includeRequestBody ?? false,
|
|
69
|
-
includeResponseBody: options.includeResponseBody ?? false,
|
|
70
|
-
maxBodySize: options.maxBodySize ?? 1024 * 1024, // 1MB
|
|
71
|
-
exporters: options.exporters ?? [{ type: 'console' }],
|
|
72
|
-
propagation: {
|
|
73
|
-
enabled: true,
|
|
74
|
-
headers: ['x-trace-id', 'x-span-id', 'x-parent-span-id'],
|
|
75
|
-
...options.propagation,
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (this.options.enabled) {
|
|
80
|
-
this.startExportInterval()
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private startExportInterval(): void {
|
|
85
|
-
this.exportInterval = setInterval(async () => {
|
|
86
|
-
try {
|
|
87
|
-
await this.exportSpans()
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
console.warn('Error exporting spans:', error)
|
|
91
|
-
}
|
|
92
|
-
}, 100) // Export every 100ms for faster testing
|
|
93
|
-
// Background export must not keep the process alive
|
|
94
|
-
this.exportInterval.unref?.()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private shouldSample(): boolean {
|
|
98
|
-
return Math.random() < (this.options.sampleRate || 1.0)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private generateId(): string {
|
|
102
|
-
return Math.random().toString(36).substring(2, 15)
|
|
103
|
-
+ Math.random().toString(36).substring(2, 15)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private extractPropagationHeaders(req: EnhancedRequest): {
|
|
107
|
-
traceId?: string
|
|
108
|
-
parentSpanId?: string
|
|
109
|
-
} {
|
|
110
|
-
if (!this.options.propagation?.enabled) {
|
|
111
|
-
return {}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const headers = this.options.propagation.headers || []
|
|
115
|
-
let traceId: string | undefined
|
|
116
|
-
|
|
117
|
-
// Try to find trace ID from configured headers
|
|
118
|
-
for (const header of headers) {
|
|
119
|
-
const value = req.headers.get(header)
|
|
120
|
-
if (value) {
|
|
121
|
-
traceId = value
|
|
122
|
-
break
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Look for parent span ID
|
|
127
|
-
const parentSpanId = req.headers.get('x-parent-span-id') || undefined
|
|
128
|
-
|
|
129
|
-
return { traceId, parentSpanId }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private async captureRequestBody(req: EnhancedRequest): Promise<unknown> {
|
|
133
|
-
if (!this.options.includeRequestBody)
|
|
134
|
-
return undefined
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const contentType = req.headers.get('content-type') || ''
|
|
138
|
-
const contentLength = Number.parseInt(req.headers.get('content-length') || '0', 10)
|
|
139
|
-
|
|
140
|
-
if (contentLength > (this.options.maxBodySize || 1024 * 1024)) {
|
|
141
|
-
return { truncated: true, reason: 'Body too large', size: contentLength }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (contentType.includes('application/json')) {
|
|
145
|
-
const cloned = req.clone()
|
|
146
|
-
return await cloned.json()
|
|
147
|
-
}
|
|
148
|
-
else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
149
|
-
const cloned = req.clone()
|
|
150
|
-
const formData = await cloned.formData()
|
|
151
|
-
const result: Record<string, any> = {}
|
|
152
|
-
for (const [key, value] of (formData as any).entries()) {
|
|
153
|
-
result[key] = value
|
|
154
|
-
}
|
|
155
|
-
return result
|
|
156
|
-
}
|
|
157
|
-
else if (contentType.includes('text/')) {
|
|
158
|
-
const cloned = req.clone()
|
|
159
|
-
return await cloned.text()
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
return { error: 'Failed to capture body', message: error instanceof Error ? error.message : String(error) }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return undefined
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private async captureResponseBody(response: Response): Promise<unknown> {
|
|
170
|
-
if (!this.options.includeResponseBody)
|
|
171
|
-
return undefined
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
const contentType = response.headers.get('content-type') || ''
|
|
175
|
-
const contentLength = Number.parseInt(response.headers.get('content-length') || '0', 10)
|
|
176
|
-
|
|
177
|
-
if (contentLength > (this.options.maxBodySize || 1024 * 1024)) {
|
|
178
|
-
return { truncated: true, reason: 'Body too large', size: contentLength }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (contentType.includes('application/json')) {
|
|
182
|
-
const cloned = response.clone()
|
|
183
|
-
return await cloned.json()
|
|
184
|
-
}
|
|
185
|
-
else if (contentType.includes('text/')) {
|
|
186
|
-
const cloned = response.clone()
|
|
187
|
-
return await cloned.text()
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
return { error: 'Failed to capture body', message: error instanceof Error ? error.message : String(error) }
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return undefined
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private createSpan(
|
|
198
|
-
traceId: string,
|
|
199
|
-
spanId: string,
|
|
200
|
-
operationName: string,
|
|
201
|
-
parentSpanId?: string,
|
|
202
|
-
): TraceSpan {
|
|
203
|
-
return {
|
|
204
|
-
traceId,
|
|
205
|
-
spanId,
|
|
206
|
-
parentSpanId,
|
|
207
|
-
operationName,
|
|
208
|
-
startTime: performance.now(),
|
|
209
|
-
tags: {},
|
|
210
|
-
logs: [],
|
|
211
|
-
status: 'ok',
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private addSpanTag(spanId: string, key: string, value: unknown): void {
|
|
216
|
-
const span = this.spans.get(spanId)
|
|
217
|
-
if (span) {
|
|
218
|
-
span.tags[key] = value
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private addSpanLog(
|
|
223
|
-
spanId: string,
|
|
224
|
-
level: 'debug' | 'info' | 'warn' | 'error',
|
|
225
|
-
message: string,
|
|
226
|
-
fields?: Record<string, any>,
|
|
227
|
-
): void {
|
|
228
|
-
const span = this.spans.get(spanId)
|
|
229
|
-
if (span) {
|
|
230
|
-
span.logs.push({
|
|
231
|
-
timestamp: performance.now(),
|
|
232
|
-
level,
|
|
233
|
-
message,
|
|
234
|
-
fields,
|
|
235
|
-
})
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
private finishSpan(spanId: string, status: 'ok' | 'error' | 'timeout' = 'ok', error?: string): void {
|
|
240
|
-
const span = this.spans.get(spanId)
|
|
241
|
-
if (span) {
|
|
242
|
-
span.endTime = performance.now()
|
|
243
|
-
span.duration = span.endTime - span.startTime
|
|
244
|
-
span.status = status
|
|
245
|
-
if (error) {
|
|
246
|
-
span.error = error
|
|
247
|
-
if (this.options.includeHeaders) {
|
|
248
|
-
span.stackTrace = new Error('Trace error stack').stack?.split('\n')
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Move to export queue
|
|
253
|
-
this.exportQueue.push(span)
|
|
254
|
-
this.spans.delete(spanId)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
private async exportSpans(): Promise<void> {
|
|
259
|
-
// Drop orphaned spans: anything unfinished for over a minute will
|
|
260
|
-
// never be completed (its request died mid-flight) and would pin the
|
|
261
|
-
// map forever
|
|
262
|
-
if (this.spans.size > 0) {
|
|
263
|
-
const cutoff = performance.now() - 60_000
|
|
264
|
-
for (const [spanId, span] of this.spans) {
|
|
265
|
-
if (span.endTime === undefined && span.startTime < cutoff) {
|
|
266
|
-
this.spans.delete(spanId)
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (this.exportQueue.length === 0) {
|
|
272
|
-
// Also check if there are any finished spans that need to be exported
|
|
273
|
-
// This ensures spans are exported even if they weren't properly moved to the queue
|
|
274
|
-
const finishedSpans = Array.from(this.spans.values()).filter(span => span.endTime !== undefined)
|
|
275
|
-
if (finishedSpans.length > 0) {
|
|
276
|
-
// Move finished spans to export queue
|
|
277
|
-
this.exportQueue.push(...finishedSpans)
|
|
278
|
-
// Remove them from the spans map
|
|
279
|
-
for (const span of finishedSpans) {
|
|
280
|
-
this.spans.delete(span.spanId)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// If still no spans to export, return
|
|
285
|
-
if (this.exportQueue.length === 0) {
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const spansToExport = [...this.exportQueue]
|
|
291
|
-
this.exportQueue = []
|
|
292
|
-
|
|
293
|
-
// Make sure we have exporters configured
|
|
294
|
-
const exporters = this.options.exporters || []
|
|
295
|
-
if (exporters.length === 0) {
|
|
296
|
-
// Default to console if no exporters configured
|
|
297
|
-
this.exportToConsole(spansToExport)
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
for (const exporter of exporters) {
|
|
302
|
-
try {
|
|
303
|
-
switch (exporter.type) {
|
|
304
|
-
case 'console':
|
|
305
|
-
this.exportToConsole(spansToExport)
|
|
306
|
-
break
|
|
307
|
-
case 'jaeger':
|
|
308
|
-
await this.exportToJaeger(spansToExport, exporter)
|
|
309
|
-
break
|
|
310
|
-
case 'zipkin':
|
|
311
|
-
await this.exportToZipkin(spansToExport, exporter)
|
|
312
|
-
break
|
|
313
|
-
case 'custom':
|
|
314
|
-
if (typeof exporter.customExporter === 'function') {
|
|
315
|
-
await exporter.customExporter(spansToExport)
|
|
316
|
-
}
|
|
317
|
-
break
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
// Use allowed console method
|
|
322
|
-
console.warn(`Failed to export spans to ${exporter.type}:`, error)
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
private exportToConsole(spans: TraceSpan[]): void {
|
|
328
|
-
for (const span of spans) {
|
|
329
|
-
// Use allowed console method
|
|
330
|
-
console.warn('Trace Span:', {
|
|
331
|
-
traceId: span.traceId,
|
|
332
|
-
spanId: span.spanId,
|
|
333
|
-
operation: span.operationName,
|
|
334
|
-
duration: `${Math.round((span.duration || 0) * 100) / 100}ms`,
|
|
335
|
-
status: span.status,
|
|
336
|
-
tags: span.tags,
|
|
337
|
-
logs: span.logs.length,
|
|
338
|
-
error: span.error,
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
private async exportToJaeger(spans: TraceSpan[], exporter: TraceExporterConfig): Promise<void> {
|
|
344
|
-
if (!exporter.endpoint)
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
const jaegerSpans = spans.map(span => ({
|
|
348
|
-
traceID: span.traceId,
|
|
349
|
-
spanID: span.spanId,
|
|
350
|
-
parentSpanID: span.parentSpanId,
|
|
351
|
-
operationName: span.operationName,
|
|
352
|
-
startTime: Math.floor(span.startTime * 1000), // microseconds
|
|
353
|
-
duration: Math.floor((span.duration || 0) * 1000),
|
|
354
|
-
tags: Object.entries(span.tags).map(([key, value]) => ({
|
|
355
|
-
key,
|
|
356
|
-
type: typeof value === 'string' ? 'string' : 'number',
|
|
357
|
-
value: String(value),
|
|
358
|
-
})),
|
|
359
|
-
logs: span.logs.map(log => ({
|
|
360
|
-
timestamp: Math.floor(log.timestamp * 1000),
|
|
361
|
-
fields: [
|
|
362
|
-
{ key: 'level', value: log.level },
|
|
363
|
-
{ key: 'message', value: log.message },
|
|
364
|
-
...(log.fields ? Object.entries(log.fields).map(([k, v]) => ({ key: k, value: String(v) })) : []),
|
|
365
|
-
],
|
|
366
|
-
})),
|
|
367
|
-
process: {
|
|
368
|
-
serviceName: 'bun-router',
|
|
369
|
-
tags: [
|
|
370
|
-
{ key: 'hostname', value: Bun.env.HOSTNAME || 'localhost' },
|
|
371
|
-
{ key: 'pid', value: String(123) }, // Use a placeholder since Bun.pid is not available
|
|
372
|
-
],
|
|
373
|
-
},
|
|
374
|
-
}))
|
|
375
|
-
|
|
376
|
-
await fetch(`${exporter.endpoint}/api/traces`, {
|
|
377
|
-
method: 'POST',
|
|
378
|
-
headers: {
|
|
379
|
-
'Content-Type': 'application/json',
|
|
380
|
-
...exporter.headers,
|
|
381
|
-
},
|
|
382
|
-
body: JSON.stringify({ data: [{ spans: jaegerSpans }] }),
|
|
383
|
-
})
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
private async exportToZipkin(spans: TraceSpan[], exporter: TraceExporterConfig): Promise<void> {
|
|
387
|
-
if (!exporter.endpoint)
|
|
388
|
-
return
|
|
389
|
-
|
|
390
|
-
const zipkinSpans = spans.map(span => ({
|
|
391
|
-
traceId: span.traceId,
|
|
392
|
-
id: span.spanId,
|
|
393
|
-
parentId: span.parentSpanId,
|
|
394
|
-
name: span.operationName,
|
|
395
|
-
timestamp: Math.floor(span.startTime * 1000), // microseconds
|
|
396
|
-
duration: Math.floor((span.duration || 0) * 1000),
|
|
397
|
-
kind: 'SERVER',
|
|
398
|
-
localEndpoint: {
|
|
399
|
-
serviceName: 'bun-router',
|
|
400
|
-
},
|
|
401
|
-
tags: span.tags,
|
|
402
|
-
annotations: span.logs.map(log => ({
|
|
403
|
-
timestamp: Math.floor(log.timestamp * 1000),
|
|
404
|
-
value: `${log.level}: ${log.message}`,
|
|
405
|
-
})),
|
|
406
|
-
}))
|
|
407
|
-
|
|
408
|
-
await fetch(`${exporter.endpoint}/api/v2/spans`, {
|
|
409
|
-
method: 'POST',
|
|
410
|
-
headers: {
|
|
411
|
-
'Content-Type': 'application/json',
|
|
412
|
-
...exporter.headers,
|
|
413
|
-
},
|
|
414
|
-
body: JSON.stringify(zipkinSpans),
|
|
415
|
-
})
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
async handle(req: EnhancedRequest, next: NextFunction): Promise<Response> {
|
|
419
|
-
if (!this.options.enabled || !this.shouldSample()) {
|
|
420
|
-
const response = await next()
|
|
421
|
-
return response || new Response('Internal Server Error', { status: 500 })
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const requestId = req.requestId || this.generateId()
|
|
425
|
-
const propagation = this.extractPropagationHeaders(req)
|
|
426
|
-
const traceId = propagation.traceId || this.generateId()
|
|
427
|
-
const spanId = this.generateId()
|
|
428
|
-
const url = new URL(req.url)
|
|
429
|
-
|
|
430
|
-
// For debugging
|
|
431
|
-
console.warn(`[RequestTracer] Creating span for ${req.method} ${url.pathname}, traceId=${traceId}, spanId=${spanId}`)
|
|
432
|
-
|
|
433
|
-
// Create main request span
|
|
434
|
-
const span = this.createSpan(
|
|
435
|
-
traceId,
|
|
436
|
-
spanId,
|
|
437
|
-
`${req.method} ${url.pathname}`,
|
|
438
|
-
propagation.parentSpanId,
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
this.spans.set(spanId, span)
|
|
442
|
-
this.activeSpans.set(requestId, spanId)
|
|
443
|
-
|
|
444
|
-
// Add trace info to request
|
|
445
|
-
req.traceId = traceId
|
|
446
|
-
req.spanId = spanId
|
|
447
|
-
|
|
448
|
-
// Ensure these properties are accessible
|
|
449
|
-
Object.defineProperties(req, {
|
|
450
|
-
traceId: {
|
|
451
|
-
value: traceId,
|
|
452
|
-
writable: true,
|
|
453
|
-
enumerable: true,
|
|
454
|
-
configurable: true,
|
|
455
|
-
},
|
|
456
|
-
spanId: {
|
|
457
|
-
value: spanId,
|
|
458
|
-
writable: true,
|
|
459
|
-
enumerable: true,
|
|
460
|
-
configurable: true,
|
|
461
|
-
},
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
// Add basic tags
|
|
465
|
-
this.addSpanTag(spanId, 'http.method', req.method)
|
|
466
|
-
this.addSpanTag(spanId, 'http.url', req.url)
|
|
467
|
-
this.addSpanTag(spanId, 'http.path', url.pathname)
|
|
468
|
-
this.addSpanTag(spanId, 'user_agent', req.headers.get('user-agent'))
|
|
469
|
-
|
|
470
|
-
if (this.options.includeHeaders) {
|
|
471
|
-
const headers: Record<string, string> = {}
|
|
472
|
-
for (const [key, value] of req.headers.entries()) {
|
|
473
|
-
headers[key] = value
|
|
474
|
-
}
|
|
475
|
-
this.addSpanTag(spanId, 'http.headers', headers)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (this.options.includeQueryParams && url.searchParams.size > 0) {
|
|
479
|
-
const params: Record<string, string> = {}
|
|
480
|
-
for (const [key, value] of url.searchParams.entries()) {
|
|
481
|
-
params[key] = value
|
|
482
|
-
}
|
|
483
|
-
this.addSpanTag(spanId, 'http.query_params', params)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Capture request body if enabled
|
|
487
|
-
if (this.options.includeRequestBody && req.method !== 'GET' && req.method !== 'HEAD') {
|
|
488
|
-
try {
|
|
489
|
-
const body = await this.captureRequestBody(req)
|
|
490
|
-
if (body !== undefined) {
|
|
491
|
-
this.addSpanTag(spanId, 'http.request_body', body)
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
this.addSpanLog(spanId, 'warn', 'Failed to capture request body', { error: String(error) })
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
this.addSpanLog(spanId, 'info', 'Request started')
|
|
500
|
-
|
|
501
|
-
let response: Response
|
|
502
|
-
let error: string | undefined
|
|
503
|
-
|
|
504
|
-
try {
|
|
505
|
-
const nextResponse = await next()
|
|
506
|
-
response = nextResponse || new Response('Internal Server Error', { status: 500 })
|
|
507
|
-
|
|
508
|
-
// Add response tags
|
|
509
|
-
this.addSpanTag(spanId, 'http.status_code', response.status)
|
|
510
|
-
this.addSpanTag(spanId, 'http.status_text', response.statusText)
|
|
511
|
-
|
|
512
|
-
if (this.options.includeResponseBody) {
|
|
513
|
-
try {
|
|
514
|
-
const body = await this.captureResponseBody(response)
|
|
515
|
-
if (body !== undefined) {
|
|
516
|
-
this.addSpanTag(spanId, 'http.response_body', body)
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
catch (err) {
|
|
520
|
-
this.addSpanLog(spanId, 'warn', 'Failed to capture response body', { error: String(err) })
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
this.addSpanLog(spanId, 'info', 'Request completed', {
|
|
525
|
-
status: response.status,
|
|
526
|
-
statusText: response.statusText,
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
// Mark as error if status code is 4xx or 5xx
|
|
530
|
-
const isError = response.status >= 400
|
|
531
|
-
if (isError) {
|
|
532
|
-
this.addSpanTag(spanId, 'error', true)
|
|
533
|
-
this.addSpanTag(spanId, 'error.status_code', response.status)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
this.finishSpan(spanId, isError ? 'error' : 'ok', isError ? `HTTP ${response.status}` : undefined)
|
|
537
|
-
}
|
|
538
|
-
catch (err) {
|
|
539
|
-
error = err instanceof Error ? err.message : String(err)
|
|
540
|
-
this.addSpanTag(spanId, 'error', true)
|
|
541
|
-
this.addSpanTag(spanId, 'error.message', error)
|
|
542
|
-
this.addSpanTag(spanId, 'error.type', err instanceof Error ? err.constructor.name : 'Unknown')
|
|
543
|
-
this.addSpanLog(spanId, 'error', 'Request failed', { error })
|
|
544
|
-
|
|
545
|
-
// Ensure the span is properly finished with error status
|
|
546
|
-
this.finishSpan(spanId, 'error', error)
|
|
547
|
-
|
|
548
|
-
// Force immediate export of this error span
|
|
549
|
-
const errorSpan = this.spans.get(spanId)
|
|
550
|
-
if (errorSpan) {
|
|
551
|
-
// Make sure it's finished
|
|
552
|
-
if (errorSpan.endTime === undefined) {
|
|
553
|
-
errorSpan.endTime = performance.now()
|
|
554
|
-
errorSpan.duration = errorSpan.endTime - errorSpan.startTime
|
|
555
|
-
errorSpan.status = 'error'
|
|
556
|
-
errorSpan.error = error
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Add to export queue and remove from spans
|
|
560
|
-
this.exportQueue.push(errorSpan)
|
|
561
|
-
this.spans.delete(spanId)
|
|
562
|
-
|
|
563
|
-
// Force immediate export
|
|
564
|
-
this.exportSpans().catch((exportErr) => {
|
|
565
|
-
console.warn('Failed to export error span:', exportErr)
|
|
566
|
-
})
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
response = new Response('Internal Server Error', { status: 500 })
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Cleanup
|
|
573
|
-
this.activeSpans.delete(requestId)
|
|
574
|
-
|
|
575
|
-
// Add trace headers to response for propagation
|
|
576
|
-
if (this.options.propagation?.enabled) {
|
|
577
|
-
response.headers.set('x-trace-id', traceId)
|
|
578
|
-
response.headers.set('x-span-id', spanId)
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return response
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Public API methods
|
|
585
|
-
getActiveSpans(): TraceSpan[] {
|
|
586
|
-
return Array.from(this.spans.values())
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
getSpanById(spanId: string): TraceSpan | undefined {
|
|
590
|
-
return this.spans.get(spanId)
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
createChildSpan(parentSpanId: string, operationName: string): string | null {
|
|
594
|
-
const parentSpan = this.spans.get(parentSpanId)
|
|
595
|
-
if (!parentSpan)
|
|
596
|
-
return null
|
|
597
|
-
|
|
598
|
-
const childSpanId = this.generateId()
|
|
599
|
-
const childSpan = this.createSpan(
|
|
600
|
-
parentSpan.traceId,
|
|
601
|
-
childSpanId,
|
|
602
|
-
operationName,
|
|
603
|
-
parentSpanId,
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
this.spans.set(childSpanId, childSpan)
|
|
607
|
-
return childSpanId
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
addTag(spanId: string, key: string, value: unknown): void {
|
|
611
|
-
this.addSpanTag(spanId, key, value)
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
addLog(spanId: string, level: 'debug' | 'info' | 'warn' | 'error', message: string, fields?: Record<string, any>): void {
|
|
615
|
-
this.addSpanLog(spanId, level, message, fields)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
finish(spanId: string, status: 'ok' | 'error' | 'timeout' = 'ok', error?: string): void {
|
|
619
|
-
this.finishSpan(spanId, status, error)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
async flush(): Promise<void> {
|
|
623
|
-
// Force all active spans to be finished and exported
|
|
624
|
-
const activeSpanIds = Array.from(this.spans.keys())
|
|
625
|
-
console.warn(`[RequestTracer] Flushing ${activeSpanIds.length} active spans`)
|
|
626
|
-
|
|
627
|
-
// Finish any active spans
|
|
628
|
-
for (const spanId of activeSpanIds) {
|
|
629
|
-
const span = this.spans.get(spanId)
|
|
630
|
-
if (span && !span.endTime) {
|
|
631
|
-
this.finishSpan(spanId, 'ok')
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Export all spans in the queue
|
|
636
|
-
await this.exportSpans()
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
destroy(): void {
|
|
640
|
-
if (this.exportInterval) {
|
|
641
|
-
clearInterval(this.exportInterval)
|
|
642
|
-
}
|
|
643
|
-
this.spans.clear()
|
|
644
|
-
this.activeSpans.clear()
|
|
645
|
-
this.exportQueue = []
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Factory function for easy use
|
|
650
|
-
export function requestTracer(options: TracingOptions = {}): (req: EnhancedRequest, next: NextFunction) => Promise<Response> {
|
|
651
|
-
const instance = new RequestTracer(options)
|
|
652
|
-
return async (req: EnhancedRequest, next: NextFunction) => instance.handle(req, next)
|
|
653
|
-
}
|