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