@onebun/trace 0.1.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/README.md +267 -0
- package/package.json +41 -0
- package/src/index.ts +48 -0
- package/src/middleware.ts +283 -0
- package/src/trace.service.ts +409 -0
- package/src/types.ts +243 -0
package/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# @onebun/trace
|
|
2
|
+
|
|
3
|
+
OpenTelemetry-compatible tracing module for the OneBun framework.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔍 Automatic HTTP request tracing
|
|
8
|
+
- 📊 W3C Trace Context support (traceparent, tracestate)
|
|
9
|
+
- 🎯 Effect.js integration
|
|
10
|
+
- 🏷️ Method tracing decorators
|
|
11
|
+
- 📝 Automatic trace information in logs
|
|
12
|
+
- 🔗 Custom span creation
|
|
13
|
+
- 🚀 OpenTelemetry-compatible API
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add @onebun/trace
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Basic setup
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { OneBunApplication } from '@onebun/core';
|
|
27
|
+
import { AppModule } from './app.module';
|
|
28
|
+
|
|
29
|
+
const app = new OneBunApplication(AppModule, {
|
|
30
|
+
tracing: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
serviceName: 'my-service',
|
|
33
|
+
serviceVersion: '1.0.0',
|
|
34
|
+
samplingRate: 1.0, // 100% sampling
|
|
35
|
+
traceHttpRequests: true,
|
|
36
|
+
traceDatabaseQueries: true,
|
|
37
|
+
defaultAttributes: {
|
|
38
|
+
'service.name': 'my-service',
|
|
39
|
+
'service.version': '1.0.0',
|
|
40
|
+
'deployment.environment': 'production'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Configuration options
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
interface TraceOptions {
|
|
50
|
+
enabled?: boolean; // Enable/disable tracing (default: true)
|
|
51
|
+
serviceName?: string; // Service name (default: 'onebun-service')
|
|
52
|
+
serviceVersion?: string; // Service version (default: '1.0.0')
|
|
53
|
+
samplingRate?: number; // Sampling rate 0.0-1.0 (default: 1.0)
|
|
54
|
+
traceHttpRequests?: boolean; // Automatic HTTP tracing (default: true)
|
|
55
|
+
traceDatabaseQueries?: boolean; // Automatic DB tracing (default: true)
|
|
56
|
+
defaultAttributes?: Record<string, string | number | boolean>;
|
|
57
|
+
exportOptions?: TraceExportOptions;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Decorators
|
|
62
|
+
|
|
63
|
+
#### @Trace
|
|
64
|
+
|
|
65
|
+
Used for tracing controller methods:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { Controller, Get, Post } from '@onebun/core';
|
|
69
|
+
import { Trace } from '@onebun/trace';
|
|
70
|
+
|
|
71
|
+
@Controller('/api')
|
|
72
|
+
export class UserController {
|
|
73
|
+
@Get('/users')
|
|
74
|
+
@Trace('get-all-users')
|
|
75
|
+
async getUsers() {
|
|
76
|
+
// Automatically creates a span named 'get-all-users'
|
|
77
|
+
return this.userService.findAll();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Post('/users')
|
|
81
|
+
@Trace() // Uses method name as span name
|
|
82
|
+
async createUser(@Body() userData: CreateUserDto) {
|
|
83
|
+
return this.userService.create(userData);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### @Span
|
|
89
|
+
|
|
90
|
+
Used for creating custom spans in services:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { Service } from '@onebun/core';
|
|
94
|
+
import { Span } from '@onebun/trace';
|
|
95
|
+
|
|
96
|
+
@Service()
|
|
97
|
+
export class UserService {
|
|
98
|
+
@Span('database-query')
|
|
99
|
+
async findAll() {
|
|
100
|
+
// Creates a span for tracking database query
|
|
101
|
+
return await this.database.query('SELECT * FROM users');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Span() // Uses 'UserService.validateUser' as name
|
|
105
|
+
async validateUser(id: string) {
|
|
106
|
+
// Span for validation
|
|
107
|
+
return await this.validateUserLogic(id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Manual trace management
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { TraceService } from '@onebun/trace';
|
|
116
|
+
import { Effect } from 'effect';
|
|
117
|
+
|
|
118
|
+
export class CustomService {
|
|
119
|
+
constructor(private traceService: TraceService) {}
|
|
120
|
+
|
|
121
|
+
async complexOperation() {
|
|
122
|
+
return Effect.runPromise(
|
|
123
|
+
Effect.flatMap(
|
|
124
|
+
this.traceService.startSpan('complex-operation'),
|
|
125
|
+
(span) => Effect.flatMap(
|
|
126
|
+
this.traceService.setAttributes({
|
|
127
|
+
'operation.type': 'data-processing',
|
|
128
|
+
'operation.complexity': 'high'
|
|
129
|
+
}),
|
|
130
|
+
() => Effect.flatMap(
|
|
131
|
+
this.performOperation(),
|
|
132
|
+
(result) => Effect.flatMap(
|
|
133
|
+
this.traceService.addEvent('operation-completed', {
|
|
134
|
+
'result.size': result.length,
|
|
135
|
+
'result.status': 'success'
|
|
136
|
+
}),
|
|
137
|
+
() => Effect.flatMap(
|
|
138
|
+
this.traceService.endSpan(span),
|
|
139
|
+
() => Effect.succeed(result)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Extracting and injecting trace context
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Extract from HTTP headers
|
|
154
|
+
const traceContext = await Effect.runPromise(
|
|
155
|
+
traceService.extractFromHeaders({
|
|
156
|
+
'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
|
|
157
|
+
'tracestate': 'rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Inject into HTTP headers
|
|
162
|
+
const headers = await Effect.runPromise(
|
|
163
|
+
traceService.injectIntoHeaders(traceContext)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// headers = {
|
|
167
|
+
// 'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
|
|
168
|
+
// 'x-trace-id': '4bf92f3577b34da6a3ce929d0e0e4736',
|
|
169
|
+
// 'x-span-id': '00f067aa0ba902b7'
|
|
170
|
+
// }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Log integration
|
|
174
|
+
|
|
175
|
+
Traces are automatically added to logs:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// In controller
|
|
179
|
+
@Get('/users/:id')
|
|
180
|
+
@Trace('get-user-by-id')
|
|
181
|
+
async getUser(@Param('id') id: string) {
|
|
182
|
+
this.logger.info('Fetching user', { userId: id });
|
|
183
|
+
// Log automatically includes trace information:
|
|
184
|
+
// {
|
|
185
|
+
// "timestamp": "2024-01-15T10:30:00.000Z",
|
|
186
|
+
// "level": "info",
|
|
187
|
+
// "message": "Fetching user",
|
|
188
|
+
// "trace": {
|
|
189
|
+
// "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
190
|
+
// "spanId": "00f067aa0ba902b7"
|
|
191
|
+
// },
|
|
192
|
+
// "context": { "userId": "123" }
|
|
193
|
+
// }
|
|
194
|
+
|
|
195
|
+
return this.userService.findById(id);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Log formats
|
|
200
|
+
|
|
201
|
+
#### Pretty format (development)
|
|
202
|
+
```
|
|
203
|
+
2024-01-15T10:30:00.000Z [INFO ] [trace:34da6a3c span:902b7f00] Fetching user
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### JSON format (production)
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"timestamp": "2024-01-15T10:30:00.000Z",
|
|
210
|
+
"level": "info",
|
|
211
|
+
"message": "Fetching user",
|
|
212
|
+
"trace": {
|
|
213
|
+
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
214
|
+
"spanId": "00f067aa0ba902b7",
|
|
215
|
+
"parentSpanId": "a3ce929d0e0e4736"
|
|
216
|
+
},
|
|
217
|
+
"context": { "userId": "123" }
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Automatic HTTP tracing
|
|
222
|
+
|
|
223
|
+
All HTTP requests are automatically traced with attributes:
|
|
224
|
+
|
|
225
|
+
- `http.method` - HTTP method
|
|
226
|
+
- `http.url` - Request URL
|
|
227
|
+
- `http.route` - Route (pattern)
|
|
228
|
+
- `http.status_code` - Response status code
|
|
229
|
+
- `http.duration` - Duration in milliseconds
|
|
230
|
+
- `http.user_agent` - Client User-Agent
|
|
231
|
+
- `http.remote_addr` - Client IP address
|
|
232
|
+
|
|
233
|
+
### Effect.js integration
|
|
234
|
+
|
|
235
|
+
The module is fully integrated with Effect.js:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { Effect } from 'effect';
|
|
239
|
+
import { TraceService } from '@onebun/trace';
|
|
240
|
+
|
|
241
|
+
const tracedOperation = Effect.flatMap(
|
|
242
|
+
TraceService,
|
|
243
|
+
(traceService) => Effect.flatMap(
|
|
244
|
+
traceService.startSpan('my-operation'),
|
|
245
|
+
(span) => Effect.flatMap(
|
|
246
|
+
performSomeWork(),
|
|
247
|
+
(result) => Effect.flatMap(
|
|
248
|
+
traceService.endSpan(span),
|
|
249
|
+
() => Effect.succeed(result)
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## OpenTelemetry compatibility
|
|
257
|
+
|
|
258
|
+
The module uses OpenTelemetry API and supports:
|
|
259
|
+
|
|
260
|
+
- W3C Trace Context propagation
|
|
261
|
+
- Standard span attributes
|
|
262
|
+
- Span events and statuses
|
|
263
|
+
- Export to external systems (when configured)
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onebun/trace",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTelemetry-compatible tracing module for OneBun framework",
|
|
5
|
+
"license": "LGPL-3.0",
|
|
6
|
+
"author": "RemRyahirev",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/RemRyahirev/onebun.git",
|
|
10
|
+
"directory": "packages/trace"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/RemRyahirev/onebun/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/RemRyahirev/onebun/tree/master/packages/trace#readme",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"main": "src/index.ts",
|
|
25
|
+
"module": "src/index.ts",
|
|
26
|
+
"types": "src/index.ts",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "bun run --watch src/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"effect": "^3.13.10",
|
|
32
|
+
"@opentelemetry/api": "^1.8.0",
|
|
33
|
+
"@onebun/requests": "^0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"bun-types": "1.2.2"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"bun": "1.2.12"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* \@onebun/trace - OpenTelemetry-compatible tracing module for OneBun framework
|
|
3
|
+
*
|
|
4
|
+
* This module provides:
|
|
5
|
+
* - Automatic HTTP request tracing
|
|
6
|
+
* - W3C trace context propagation
|
|
7
|
+
* - Effect.js integration
|
|
8
|
+
* - Decorators for method tracing
|
|
9
|
+
* - Custom span creation and management
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Re-export some OpenTelemetry types for convenience
|
|
13
|
+
export type { Context } from '@opentelemetry/api';
|
|
14
|
+
// Middleware and decorators
|
|
15
|
+
export {
|
|
16
|
+
createTraceMiddleware,
|
|
17
|
+
Span,
|
|
18
|
+
span,
|
|
19
|
+
// Backward compatibility aliases
|
|
20
|
+
Trace,
|
|
21
|
+
TraceMiddleware,
|
|
22
|
+
trace,
|
|
23
|
+
} from './middleware.js';
|
|
24
|
+
|
|
25
|
+
// Core service
|
|
26
|
+
export {
|
|
27
|
+
currentSpan,
|
|
28
|
+
currentTraceContext,
|
|
29
|
+
makeTraceService,
|
|
30
|
+
// Backward compatibility aliases
|
|
31
|
+
TraceService,
|
|
32
|
+
TraceServiceImpl,
|
|
33
|
+
TraceServiceLive,
|
|
34
|
+
traceService,
|
|
35
|
+
traceServiceLive,
|
|
36
|
+
} from './trace.service.js';
|
|
37
|
+
// Types
|
|
38
|
+
export type {
|
|
39
|
+
HttpTraceData,
|
|
40
|
+
SpanStatus,
|
|
41
|
+
TraceContext,
|
|
42
|
+
TraceEvent,
|
|
43
|
+
TraceExportOptions,
|
|
44
|
+
TraceHeaders,
|
|
45
|
+
TraceOptions,
|
|
46
|
+
TraceSpan,
|
|
47
|
+
} from './types.js';
|
|
48
|
+
export { SpanStatusCode } from './types.js';
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
|
|
3
|
+
import type { HttpTraceData, TraceHeaders } from './types.js';
|
|
4
|
+
|
|
5
|
+
import { HttpStatusCode } from '@onebun/requests';
|
|
6
|
+
|
|
7
|
+
import { traceService } from './trace.service.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* HTTP trace middleware for OneBun applications
|
|
11
|
+
*/
|
|
12
|
+
export class TraceMiddleware {
|
|
13
|
+
/**
|
|
14
|
+
* Create trace middleware Effect
|
|
15
|
+
*/
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
17
|
+
static create() {
|
|
18
|
+
return Effect.flatMap(traceService, (traceServiceInstance) =>
|
|
19
|
+
Effect.succeed((request: Request, next: () => Promise<Response>) => {
|
|
20
|
+
return Effect.runPromise(
|
|
21
|
+
Effect.flatMap(
|
|
22
|
+
traceServiceInstance.extractFromHeaders(TraceMiddleware.extractHeaders(request)),
|
|
23
|
+
(extractedContext) =>
|
|
24
|
+
Effect.flatMap(
|
|
25
|
+
extractedContext
|
|
26
|
+
? traceServiceInstance.setContext(extractedContext)
|
|
27
|
+
: Effect.flatMap(traceServiceInstance.generateTraceContext(), (newContext) =>
|
|
28
|
+
traceServiceInstance.setContext(newContext),
|
|
29
|
+
),
|
|
30
|
+
() => {
|
|
31
|
+
const httpData: Partial<HttpTraceData> = {
|
|
32
|
+
method: request.method,
|
|
33
|
+
url: request.url,
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
route: (request as any).route?.path,
|
|
36
|
+
userAgent: request.headers.get('user-agent') || undefined,
|
|
37
|
+
remoteAddr: TraceMiddleware.getRemoteAddress(request),
|
|
38
|
+
requestSize: TraceMiddleware.getRequestSize(request),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
|
|
43
|
+
return Effect.flatMap(
|
|
44
|
+
traceServiceInstance.startHttpTrace(httpData),
|
|
45
|
+
(_httpSpan) =>
|
|
46
|
+
Effect.tryPromise({
|
|
47
|
+
try: () => next(),
|
|
48
|
+
catch: (error) => error as Error,
|
|
49
|
+
}).pipe(
|
|
50
|
+
Effect.tap((response) => {
|
|
51
|
+
const endTime = Date.now();
|
|
52
|
+
const finalData: Partial<HttpTraceData> = {
|
|
53
|
+
statusCode:
|
|
54
|
+
(response && typeof response === 'object' && 'status' in response
|
|
55
|
+
? (response.status as number)
|
|
56
|
+
: undefined) || HttpStatusCode.OK,
|
|
57
|
+
responseSize: TraceMiddleware.getResponseSize(response),
|
|
58
|
+
duration: endTime - startTime,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return traceServiceInstance.endHttpTrace(_httpSpan, finalData);
|
|
62
|
+
}),
|
|
63
|
+
Effect.tapError((error) => {
|
|
64
|
+
const endTime = Date.now();
|
|
65
|
+
const errorData: Partial<HttpTraceData> = {
|
|
66
|
+
statusCode: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
67
|
+
duration: endTime - startTime,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return Effect.flatMap(
|
|
71
|
+
traceServiceInstance.addEvent('error', {
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
73
|
+
'error.type': error.constructor.name,
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
75
|
+
'error.message': error.message,
|
|
76
|
+
}),
|
|
77
|
+
() => traceServiceInstance.endHttpTrace(_httpSpan, errorData),
|
|
78
|
+
);
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extract trace headers from HTTP request
|
|
92
|
+
*/
|
|
93
|
+
private static extractHeaders(request: Request): TraceHeaders {
|
|
94
|
+
return {
|
|
95
|
+
traceparent: request.headers.get('traceparent') || undefined,
|
|
96
|
+
tracestate: request.headers.get('tracestate') || undefined,
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
98
|
+
'x-trace-id': request.headers.get('x-trace-id') || undefined,
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
100
|
+
'x-span-id': request.headers.get('x-span-id') || undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get remote address from request
|
|
106
|
+
*/
|
|
107
|
+
private static getRemoteAddress(request: unknown): string | undefined {
|
|
108
|
+
if (!request || typeof request !== 'object') {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const headers = 'headers' in request ? (request.headers as Record<string, string>) : {};
|
|
113
|
+
const socket = 'socket' in request ? (request.socket as { remoteAddress?: string }) : {};
|
|
114
|
+
const connection =
|
|
115
|
+
'connection' in request ? (request.connection as { remoteAddress?: string }) : {};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
headers['x-forwarded-for'] ||
|
|
119
|
+
headers['x-real-ip'] ||
|
|
120
|
+
socket.remoteAddress ||
|
|
121
|
+
connection.remoteAddress
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get request content length
|
|
127
|
+
*/
|
|
128
|
+
private static getRequestSize(request: unknown): number | undefined {
|
|
129
|
+
if (!request || typeof request !== 'object' || !('headers' in request)) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const headers = request.headers as Record<string, string>;
|
|
134
|
+
const contentLength = headers['content-length'];
|
|
135
|
+
|
|
136
|
+
return contentLength ? parseInt(contentLength, 10) : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get response content length
|
|
141
|
+
*/
|
|
142
|
+
private static getResponseSize(response: unknown): number | undefined {
|
|
143
|
+
if (!response || typeof response !== 'object') {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const DECIMAL_BASE = 10;
|
|
148
|
+
|
|
149
|
+
if ('headers' in response) {
|
|
150
|
+
const headers = response.headers as Record<string, string>;
|
|
151
|
+
const contentLength = headers['content-length'];
|
|
152
|
+
if (contentLength) {
|
|
153
|
+
return parseInt(contentLength, DECIMAL_BASE);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Try to estimate from body
|
|
158
|
+
if ('body' in response) {
|
|
159
|
+
const body = response.body;
|
|
160
|
+
|
|
161
|
+
if (typeof body === 'string') {
|
|
162
|
+
return Buffer.byteLength(body);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (Buffer.isBuffer(body)) {
|
|
166
|
+
return body.length;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (body && typeof body === 'object') {
|
|
170
|
+
return Buffer.byteLength(JSON.stringify(body));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create trace middleware function for Bun server
|
|
180
|
+
*/
|
|
181
|
+
export const createTraceMiddleware = (): TraceMiddleware => {
|
|
182
|
+
return TraceMiddleware.create();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Trace decorator for controller methods
|
|
187
|
+
*/
|
|
188
|
+
export function trace(operationName?: string): MethodDecorator {
|
|
189
|
+
return (target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
|
190
|
+
const originalMethod = descriptor.value;
|
|
191
|
+
const targetConstructor =
|
|
192
|
+
target && typeof target === 'object' && target.constructor
|
|
193
|
+
? target.constructor
|
|
194
|
+
: { name: 'Unknown' };
|
|
195
|
+
const spanName = operationName || `${targetConstructor.name}.${String(propertyKey)}`;
|
|
196
|
+
|
|
197
|
+
descriptor.value = function (...args: unknown[]) {
|
|
198
|
+
return Effect.flatMap(traceService, (traceServiceInstance) =>
|
|
199
|
+
Effect.flatMap(traceServiceInstance.startSpan(spanName), (_traceSpan) => {
|
|
200
|
+
const startTime = Date.now();
|
|
201
|
+
|
|
202
|
+
return Effect.tryPromise({
|
|
203
|
+
try: () => originalMethod.apply(this, args),
|
|
204
|
+
catch: (error) => error as Error,
|
|
205
|
+
}).pipe(
|
|
206
|
+
Effect.tap(() => {
|
|
207
|
+
const duration = Date.now() - startTime;
|
|
208
|
+
|
|
209
|
+
return Effect.flatMap(
|
|
210
|
+
traceServiceInstance.setAttributes({
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
212
|
+
'method.name': String(propertyKey),
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
214
|
+
'method.duration': duration,
|
|
215
|
+
}),
|
|
216
|
+
() => traceServiceInstance.endSpan(_traceSpan),
|
|
217
|
+
);
|
|
218
|
+
}),
|
|
219
|
+
Effect.tapError((error) => {
|
|
220
|
+
const duration = Date.now() - startTime;
|
|
221
|
+
|
|
222
|
+
return Effect.flatMap(
|
|
223
|
+
traceServiceInstance.setAttributes({
|
|
224
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
225
|
+
'method.name': String(propertyKey),
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
227
|
+
'method.duration': duration,
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
229
|
+
'error.type': error.constructor.name,
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
231
|
+
'error.message': error.message,
|
|
232
|
+
}),
|
|
233
|
+
() =>
|
|
234
|
+
traceServiceInstance.endSpan(_traceSpan, {
|
|
235
|
+
code: 2, // ERROR
|
|
236
|
+
message: error.message,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return descriptor;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Span decorator for creating custom spans
|
|
251
|
+
*/
|
|
252
|
+
export function span(name?: string): MethodDecorator {
|
|
253
|
+
return (target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
|
254
|
+
const originalMethod = descriptor.value;
|
|
255
|
+
const targetConstructor =
|
|
256
|
+
target && typeof target === 'object' && target.constructor
|
|
257
|
+
? target.constructor
|
|
258
|
+
: { name: 'Unknown' };
|
|
259
|
+
const spanName = name || `${targetConstructor.name}.${String(propertyKey)}`;
|
|
260
|
+
|
|
261
|
+
descriptor.value = function (...args: unknown[]) {
|
|
262
|
+
return Effect.flatMap(traceService, (traceServiceInstance) =>
|
|
263
|
+
Effect.flatMap(traceServiceInstance.startSpan(spanName), (spanInstance) =>
|
|
264
|
+
Effect.flatMap(
|
|
265
|
+
Effect.try(() => originalMethod.apply(this, args)),
|
|
266
|
+
(result) =>
|
|
267
|
+
Effect.flatMap(traceServiceInstance.endSpan(spanInstance), () =>
|
|
268
|
+
Effect.succeed(result),
|
|
269
|
+
),
|
|
270
|
+
),
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return descriptor;
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Backward compatibility aliases
|
|
280
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
281
|
+
export const Trace = trace;
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
283
|
+
export const Span = span;
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import {
|
|
2
|
+
context,
|
|
3
|
+
SpanStatusCode as OtelSpanStatusCode,
|
|
4
|
+
SpanKind,
|
|
5
|
+
trace,
|
|
6
|
+
} from '@opentelemetry/api';
|
|
7
|
+
import {
|
|
8
|
+
Context,
|
|
9
|
+
Effect,
|
|
10
|
+
FiberRef,
|
|
11
|
+
Layer,
|
|
12
|
+
} from 'effect';
|
|
13
|
+
|
|
14
|
+
import { HttpStatusCode } from '@onebun/requests';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
type HttpTraceData,
|
|
18
|
+
type SpanStatus,
|
|
19
|
+
SpanStatusCode,
|
|
20
|
+
type TraceContext,
|
|
21
|
+
type TraceHeaders,
|
|
22
|
+
type TraceOptions,
|
|
23
|
+
type TraceSpan,
|
|
24
|
+
} from './types.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Index of trace flags in W3C traceparent header regex match array
|
|
28
|
+
*/
|
|
29
|
+
const TRACE_FLAGS_MATCH_INDEX = 3;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Trace service interface
|
|
33
|
+
*/
|
|
34
|
+
export interface TraceService {
|
|
35
|
+
/**
|
|
36
|
+
* Get current trace context
|
|
37
|
+
*/
|
|
38
|
+
getCurrentContext(): Effect.Effect<TraceContext | null>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set trace context
|
|
42
|
+
*/
|
|
43
|
+
setContext(traceContext: TraceContext): Effect.Effect<void>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Start a new span
|
|
47
|
+
*/
|
|
48
|
+
startSpan(name: string, parentContext?: TraceContext): Effect.Effect<TraceSpan>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* End a span
|
|
52
|
+
*/
|
|
53
|
+
endSpan(span: TraceSpan, status?: SpanStatus): Effect.Effect<void>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add event to current span
|
|
57
|
+
*/
|
|
58
|
+
addEvent(
|
|
59
|
+
name: string,
|
|
60
|
+
attributes?: Record<string, string | number | boolean>,
|
|
61
|
+
): Effect.Effect<void>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set span attributes
|
|
65
|
+
*/
|
|
66
|
+
setAttributes(attributes: Record<string, string | number | boolean>): Effect.Effect<void>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract trace context from HTTP headers
|
|
70
|
+
*/
|
|
71
|
+
extractFromHeaders(headers: TraceHeaders): Effect.Effect<TraceContext | null>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Inject trace context into HTTP headers
|
|
75
|
+
*/
|
|
76
|
+
injectIntoHeaders(traceContext: TraceContext): Effect.Effect<TraceHeaders>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generate new trace context
|
|
80
|
+
*/
|
|
81
|
+
generateTraceContext(): Effect.Effect<TraceContext>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Start HTTP request tracing
|
|
85
|
+
*/
|
|
86
|
+
startHttpTrace(data: Partial<HttpTraceData>): Effect.Effect<TraceSpan>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* End HTTP request tracing
|
|
90
|
+
*/
|
|
91
|
+
endHttpTrace(span: TraceSpan, data: Partial<HttpTraceData>): Effect.Effect<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Trace service tag for Effect dependency injection
|
|
96
|
+
*/
|
|
97
|
+
export const traceService = Context.GenericTag<TraceService>('@onebun/trace/TraceService');
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Current trace context stored in fiber
|
|
101
|
+
*/
|
|
102
|
+
export const currentTraceContext = FiberRef.unsafeMake<TraceContext | null>(null);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Current span stored in fiber
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
export const currentSpan = FiberRef.unsafeMake<TraceSpan | null>(null);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Implementation of TraceService
|
|
112
|
+
*/
|
|
113
|
+
export class TraceServiceImpl implements TraceService {
|
|
114
|
+
private readonly tracer = trace.getTracer('@onebun/trace');
|
|
115
|
+
private readonly options: Required<TraceOptions>;
|
|
116
|
+
|
|
117
|
+
constructor(options: TraceOptions = {}) {
|
|
118
|
+
this.options = {
|
|
119
|
+
enabled: true,
|
|
120
|
+
serviceName: 'onebun-service',
|
|
121
|
+
serviceVersion: '1.0.0',
|
|
122
|
+
samplingRate: 1.0,
|
|
123
|
+
traceHttpRequests: true,
|
|
124
|
+
traceDatabaseQueries: true,
|
|
125
|
+
defaultAttributes: {},
|
|
126
|
+
exportOptions: {},
|
|
127
|
+
...options,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getCurrentContext(): Effect.Effect<TraceContext | null> {
|
|
132
|
+
return FiberRef.get(currentTraceContext);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setContext(traceContext: TraceContext): Effect.Effect<void> {
|
|
136
|
+
return FiberRef.set(currentTraceContext, traceContext);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
startSpan(name: string, parentContext?: TraceContext): Effect.Effect<TraceSpan> {
|
|
140
|
+
if (!this.options.enabled) {
|
|
141
|
+
return Effect.flatMap(
|
|
142
|
+
this.generateTraceContext(),
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
144
|
+
(context) => {
|
|
145
|
+
const mockSpan: TraceSpan = {
|
|
146
|
+
context,
|
|
147
|
+
name,
|
|
148
|
+
startTime: Date.now(),
|
|
149
|
+
attributes: {},
|
|
150
|
+
events: [],
|
|
151
|
+
status: { code: SpanStatusCode.OK },
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return Effect.flatMap(FiberRef.set(currentSpan, mockSpan), () =>
|
|
155
|
+
Effect.succeed(mockSpan),
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const currentContext = context.active();
|
|
162
|
+
const span = this.tracer.startSpan(
|
|
163
|
+
name,
|
|
164
|
+
{
|
|
165
|
+
kind: SpanKind.INTERNAL,
|
|
166
|
+
attributes: this.options.defaultAttributes,
|
|
167
|
+
},
|
|
168
|
+
currentContext,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const spanContext = span.spanContext();
|
|
172
|
+
const traceContext: TraceContext = {
|
|
173
|
+
traceId: spanContext.traceId,
|
|
174
|
+
spanId: spanContext.spanId,
|
|
175
|
+
traceFlags: spanContext.traceFlags,
|
|
176
|
+
parentSpanId: parentContext?.spanId,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const traceSpan: TraceSpan = {
|
|
180
|
+
context: traceContext,
|
|
181
|
+
name,
|
|
182
|
+
startTime: Date.now(),
|
|
183
|
+
attributes: { ...this.options.defaultAttributes },
|
|
184
|
+
events: [],
|
|
185
|
+
status: { code: SpanStatusCode.OK },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return Effect.flatMap(FiberRef.set(currentSpan, traceSpan), () =>
|
|
189
|
+
Effect.flatMap(this.setContext(traceContext), () => Effect.succeed(traceSpan)),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
endSpan(span: TraceSpan, status?: SpanStatus): Effect.Effect<void> {
|
|
194
|
+
if (!this.options.enabled) {
|
|
195
|
+
return Effect.void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
span.endTime = Date.now();
|
|
199
|
+
if (status) {
|
|
200
|
+
span.status = status;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Find the OTel span and end it
|
|
204
|
+
const activeSpan = trace.getActiveSpan();
|
|
205
|
+
if (activeSpan && activeSpan.spanContext().spanId === span.context.spanId) {
|
|
206
|
+
if (status?.code === SpanStatusCode.ERROR) {
|
|
207
|
+
activeSpan.setStatus({
|
|
208
|
+
code: OtelSpanStatusCode.ERROR,
|
|
209
|
+
message: status.message,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
activeSpan.end();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return FiberRef.set(currentSpan, null);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
addEvent(
|
|
219
|
+
name: string,
|
|
220
|
+
attributes?: Record<string, string | number | boolean>,
|
|
221
|
+
): Effect.Effect<void> {
|
|
222
|
+
if (!this.options.enabled) {
|
|
223
|
+
return Effect.void;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return Effect.flatMap(FiberRef.get(currentSpan), (span) => {
|
|
227
|
+
if (span) {
|
|
228
|
+
span.events.push({
|
|
229
|
+
name,
|
|
230
|
+
timestamp: Date.now(),
|
|
231
|
+
attributes,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const activeSpan = trace.getActiveSpan();
|
|
235
|
+
if (activeSpan) {
|
|
236
|
+
activeSpan.addEvent(name, attributes);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return Effect.void;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
setAttributes(attributes: Record<string, string | number | boolean>): Effect.Effect<void> {
|
|
245
|
+
if (!this.options.enabled) {
|
|
246
|
+
return Effect.void;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return Effect.flatMap(FiberRef.get(currentSpan), (span) => {
|
|
250
|
+
if (span) {
|
|
251
|
+
Object.assign(span.attributes, attributes);
|
|
252
|
+
|
|
253
|
+
const activeSpan = trace.getActiveSpan();
|
|
254
|
+
if (activeSpan) {
|
|
255
|
+
activeSpan.setAttributes(attributes);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return Effect.void;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
extractFromHeaders(headers: TraceHeaders): Effect.Effect<TraceContext | null> {
|
|
264
|
+
if (!this.options.enabled) {
|
|
265
|
+
return Effect.succeed(null);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Try W3C traceparent header first
|
|
269
|
+
const TRACE_ID_LENGTH = 32;
|
|
270
|
+
const SPAN_ID_LENGTH = 16;
|
|
271
|
+
const FLAGS_LENGTH = 2;
|
|
272
|
+
const HEX_BASE = 16;
|
|
273
|
+
const traceparent = headers['traceparent'];
|
|
274
|
+
if (traceparent) {
|
|
275
|
+
const match = traceparent.match(
|
|
276
|
+
new RegExp(
|
|
277
|
+
`^00-([0-9a-f]{${TRACE_ID_LENGTH}})-([0-9a-f]{${SPAN_ID_LENGTH}})-([0-9a-f]{${FLAGS_LENGTH}})$`,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
if (match) {
|
|
281
|
+
return Effect.succeed({
|
|
282
|
+
traceId: match[1],
|
|
283
|
+
spanId: match[2],
|
|
284
|
+
traceFlags: parseInt(match[TRACE_FLAGS_MATCH_INDEX], HEX_BASE),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Fallback to custom headers
|
|
290
|
+
const traceId = headers['x-trace-id'];
|
|
291
|
+
const spanId = headers['x-span-id'];
|
|
292
|
+
|
|
293
|
+
if (traceId && spanId) {
|
|
294
|
+
return Effect.succeed({
|
|
295
|
+
traceId,
|
|
296
|
+
spanId,
|
|
297
|
+
traceFlags: 1,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return Effect.succeed(null);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
injectIntoHeaders(traceContext: TraceContext): Effect.Effect<TraceHeaders> {
|
|
305
|
+
const HEX_BASE = 16;
|
|
306
|
+
const PAD_LENGTH = 2;
|
|
307
|
+
|
|
308
|
+
return Effect.succeed({
|
|
309
|
+
traceparent: `00-${traceContext.traceId}-${traceContext.spanId}-${traceContext.traceFlags.toString(HEX_BASE).padStart(PAD_LENGTH, '0')}`,
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
311
|
+
'x-trace-id': traceContext.traceId,
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
313
|
+
'x-span-id': traceContext.spanId,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
generateTraceContext(): Effect.Effect<TraceContext> {
|
|
318
|
+
const TRACE_ID_LENGTH = 32;
|
|
319
|
+
const SPAN_ID_LENGTH = 16;
|
|
320
|
+
|
|
321
|
+
return Effect.succeed({
|
|
322
|
+
traceId: this.generateId(TRACE_ID_LENGTH),
|
|
323
|
+
spanId: this.generateId(SPAN_ID_LENGTH),
|
|
324
|
+
traceFlags: Math.random() < this.options.samplingRate ? 1 : 0,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
startHttpTrace(data: Partial<HttpTraceData>): Effect.Effect<TraceSpan> {
|
|
329
|
+
const spanName = `HTTP ${data.method || 'REQUEST'} ${data.route || data.url || '/'}`;
|
|
330
|
+
|
|
331
|
+
return Effect.flatMap(this.startSpan(spanName), (span) => {
|
|
332
|
+
const attributes: Record<string, string | number | boolean> = {
|
|
333
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
334
|
+
'http.method': data.method || 'UNKNOWN',
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
336
|
+
'http.url': data.url || '',
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
338
|
+
'http.route': data.route || '',
|
|
339
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
340
|
+
'http.user_agent': data.userAgent || '',
|
|
341
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
342
|
+
'http.remote_addr': data.remoteAddr || '',
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (data.requestSize !== undefined) {
|
|
346
|
+
attributes['http.request_content_length'] = data.requestSize;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return Effect.flatMap(this.setAttributes(attributes), () => Effect.succeed(span));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
endHttpTrace(span: TraceSpan, data: Partial<HttpTraceData>): Effect.Effect<void> {
|
|
354
|
+
const attributes: Record<string, string | number | boolean> = {};
|
|
355
|
+
|
|
356
|
+
if (data.statusCode !== undefined) {
|
|
357
|
+
attributes['http.status_code'] = data.statusCode;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (data.responseSize !== undefined) {
|
|
361
|
+
attributes['http.response_content_length'] = data.responseSize;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (data.duration !== undefined) {
|
|
365
|
+
attributes['http.duration'] = data.duration;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const HTTP_ERROR_THRESHOLD = HttpStatusCode.BAD_REQUEST;
|
|
369
|
+
const status: SpanStatus = {
|
|
370
|
+
code:
|
|
371
|
+
data.statusCode && data.statusCode >= HTTP_ERROR_THRESHOLD
|
|
372
|
+
? SpanStatusCode.ERROR
|
|
373
|
+
: SpanStatusCode.OK,
|
|
374
|
+
message:
|
|
375
|
+
data.statusCode && data.statusCode >= HTTP_ERROR_THRESHOLD
|
|
376
|
+
? `HTTP ${data.statusCode}`
|
|
377
|
+
: undefined,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
return Effect.flatMap(this.setAttributes(attributes), () => this.endSpan(span, status));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private generateId(length: number): string {
|
|
384
|
+
const chars = '0123456789abcdef';
|
|
385
|
+
let result = '';
|
|
386
|
+
for (let i = 0; i < length; i++) {
|
|
387
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Create TraceService layer
|
|
396
|
+
*/
|
|
397
|
+
export const makeTraceService = (options?: TraceOptions): Layer.Layer<TraceService> =>
|
|
398
|
+
Layer.succeed(traceService, new TraceServiceImpl(options));
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Default trace service layer
|
|
402
|
+
*/
|
|
403
|
+
export const traceServiceLive = makeTraceService();
|
|
404
|
+
|
|
405
|
+
// Backward compatibility aliases
|
|
406
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
407
|
+
export const TraceService = traceService;
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
409
|
+
export const TraceServiceLive = traceServiceLive;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace context interface
|
|
3
|
+
*/
|
|
4
|
+
export interface TraceContext {
|
|
5
|
+
/**
|
|
6
|
+
* Trace ID
|
|
7
|
+
*/
|
|
8
|
+
traceId: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Span ID
|
|
12
|
+
*/
|
|
13
|
+
spanId: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parent span ID
|
|
17
|
+
*/
|
|
18
|
+
parentSpanId?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Trace flags
|
|
22
|
+
*/
|
|
23
|
+
traceFlags: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Baggage items
|
|
27
|
+
*/
|
|
28
|
+
baggage?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Span interface
|
|
33
|
+
*/
|
|
34
|
+
export interface TraceSpan {
|
|
35
|
+
/**
|
|
36
|
+
* Span context
|
|
37
|
+
*/
|
|
38
|
+
context: TraceContext;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Span name
|
|
42
|
+
*/
|
|
43
|
+
name: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Start time
|
|
47
|
+
*/
|
|
48
|
+
startTime: number;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* End time
|
|
52
|
+
*/
|
|
53
|
+
endTime?: number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Span attributes
|
|
57
|
+
*/
|
|
58
|
+
attributes: Record<string, string | number | boolean>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Span events
|
|
62
|
+
*/
|
|
63
|
+
events: TraceEvent[];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Span status
|
|
67
|
+
*/
|
|
68
|
+
status: SpanStatus;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Span event interface
|
|
73
|
+
*/
|
|
74
|
+
export interface TraceEvent {
|
|
75
|
+
/**
|
|
76
|
+
* Event name
|
|
77
|
+
*/
|
|
78
|
+
name: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Event timestamp
|
|
82
|
+
*/
|
|
83
|
+
timestamp: number;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Event attributes
|
|
87
|
+
*/
|
|
88
|
+
attributes?: Record<string, string | number | boolean>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Span status
|
|
93
|
+
*/
|
|
94
|
+
export interface SpanStatus {
|
|
95
|
+
/**
|
|
96
|
+
* Status code
|
|
97
|
+
*/
|
|
98
|
+
code: SpanStatusCode;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Status message
|
|
102
|
+
*/
|
|
103
|
+
message?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Span status codes
|
|
108
|
+
*/
|
|
109
|
+
export enum SpanStatusCode {
|
|
110
|
+
/**
|
|
111
|
+
* The operation completed successfully.
|
|
112
|
+
*/
|
|
113
|
+
OK = 1,
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* An error occurred.
|
|
117
|
+
*/
|
|
118
|
+
ERROR = 2,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* HTTP trace headers
|
|
123
|
+
*/
|
|
124
|
+
export interface TraceHeaders {
|
|
125
|
+
/**
|
|
126
|
+
* Trace parent header (W3C)
|
|
127
|
+
*/
|
|
128
|
+
traceparent?: string;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Trace state header (W3C)
|
|
132
|
+
*/
|
|
133
|
+
tracestate?: string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* X-Trace-Id header (custom)
|
|
137
|
+
*/
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
139
|
+
'x-trace-id'?: string;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* X-Span-Id header (custom)
|
|
143
|
+
*/
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
145
|
+
'x-span-id'?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Trace options configuration
|
|
150
|
+
*/
|
|
151
|
+
export interface TraceOptions {
|
|
152
|
+
/**
|
|
153
|
+
* Enable/disable tracing
|
|
154
|
+
* @defaultValue true
|
|
155
|
+
*/
|
|
156
|
+
enabled?: boolean;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Service name for tracing
|
|
160
|
+
*/
|
|
161
|
+
serviceName?: string;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Service version
|
|
165
|
+
*/
|
|
166
|
+
serviceVersion?: string;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Sampling rate (0.0 to 1.0)
|
|
170
|
+
* @defaultValue 1.0
|
|
171
|
+
*/
|
|
172
|
+
samplingRate?: number;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Enable automatic HTTP request tracing
|
|
176
|
+
* @defaultValue true
|
|
177
|
+
*/
|
|
178
|
+
traceHttpRequests?: boolean;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Enable automatic database query tracing
|
|
182
|
+
* @defaultValue true
|
|
183
|
+
*/
|
|
184
|
+
traceDatabaseQueries?: boolean;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Custom attributes to add to all spans
|
|
188
|
+
*/
|
|
189
|
+
defaultAttributes?: Record<string, string | number | boolean>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Export traces to external system
|
|
193
|
+
*/
|
|
194
|
+
exportOptions?: TraceExportOptions;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Trace export options
|
|
199
|
+
*/
|
|
200
|
+
export interface TraceExportOptions {
|
|
201
|
+
/**
|
|
202
|
+
* Export endpoint URL
|
|
203
|
+
*/
|
|
204
|
+
endpoint?: string;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Export headers
|
|
208
|
+
*/
|
|
209
|
+
headers?: Record<string, string>;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Export timeout in milliseconds
|
|
213
|
+
* @defaultValue 10000
|
|
214
|
+
*/
|
|
215
|
+
timeout?: number;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Batch size for exporting
|
|
219
|
+
* @defaultValue 100
|
|
220
|
+
*/
|
|
221
|
+
batchSize?: number;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Batch timeout in milliseconds
|
|
225
|
+
* @defaultValue 5000
|
|
226
|
+
*/
|
|
227
|
+
batchTimeout?: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* HTTP request trace data
|
|
232
|
+
*/
|
|
233
|
+
export interface HttpTraceData {
|
|
234
|
+
method: string;
|
|
235
|
+
url: string;
|
|
236
|
+
route?: string;
|
|
237
|
+
statusCode?: number;
|
|
238
|
+
userAgent?: string;
|
|
239
|
+
remoteAddr?: string;
|
|
240
|
+
requestSize?: number;
|
|
241
|
+
responseSize?: number;
|
|
242
|
+
duration?: number;
|
|
243
|
+
}
|