@onebun/trace 0.2.1 → 0.2.2
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/package.json +7 -3
- package/src/index.ts +5 -1
- package/src/middleware.ts +61 -61
- package/src/otlp-exporter.ts +245 -0
- package/src/provider.ts +73 -0
- package/src/trace.service.ts +22 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/trace",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "OpenTelemetry-compatible tracing for OneBun framework - distributed tracing support",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -38,9 +38,13 @@
|
|
|
38
38
|
"dev": "bun run --watch src/index.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"
|
|
41
|
+
"@onebun/requests": "^0.2.1",
|
|
42
42
|
"@opentelemetry/api": "^1.8.0",
|
|
43
|
-
"@
|
|
43
|
+
"@opentelemetry/core": "^2.6.1",
|
|
44
|
+
"@opentelemetry/resources": "^2.6.1",
|
|
45
|
+
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
|
46
|
+
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
47
|
+
"effect": "^3.13.10"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
50
|
"bun-types": "^1.3.8"
|
package/src/index.ts
CHANGED
|
@@ -16,12 +16,16 @@ export {
|
|
|
16
16
|
createTraceMiddleware,
|
|
17
17
|
Span,
|
|
18
18
|
span,
|
|
19
|
-
|
|
19
|
+
Spanned,
|
|
20
20
|
Trace,
|
|
21
|
+
Traced,
|
|
21
22
|
TraceMiddleware,
|
|
22
23
|
trace,
|
|
23
24
|
} from './middleware.js';
|
|
24
25
|
|
|
26
|
+
// OTLP exporter
|
|
27
|
+
export { OtlpFetchSpanExporter, type OtlpExporterOptions } from './otlp-exporter.js';
|
|
28
|
+
|
|
25
29
|
// Core service
|
|
26
30
|
export {
|
|
27
31
|
currentSpan,
|
package/src/middleware.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SpanStatusCode as OtelSpanStatusCode, trace as otelTrace } from '@opentelemetry/api';
|
|
1
2
|
import { Effect } from 'effect';
|
|
2
3
|
|
|
3
4
|
import type { HttpTraceData, TraceHeaders } from './types.js';
|
|
@@ -183,7 +184,21 @@ export const createTraceMiddleware = (): TraceMiddleware => {
|
|
|
183
184
|
};
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
|
-
* Trace decorator for controller methods
|
|
187
|
+
* Trace decorator for async/await controller and service methods.
|
|
188
|
+
*
|
|
189
|
+
* Creates an OpenTelemetry span around the decorated method. Works with
|
|
190
|
+
* methods that return Promises — the span is automatically ended when
|
|
191
|
+
* the Promise resolves or rejects.
|
|
192
|
+
*
|
|
193
|
+
* @param operationName - Custom span name. Defaults to `ClassName.methodName`
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* class WorkspaceService extends BaseService {
|
|
198
|
+
* \@Traced('workspace.findAll')
|
|
199
|
+
* async findAll(): Promise<Workspace[]> { ... }
|
|
200
|
+
* }
|
|
201
|
+
* ```
|
|
187
202
|
*/
|
|
188
203
|
export function trace(operationName?: string): MethodDecorator {
|
|
189
204
|
return (target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
|
@@ -194,52 +209,24 @@ export function trace(operationName?: string): MethodDecorator {
|
|
|
194
209
|
: { name: 'Unknown' };
|
|
195
210
|
const spanName = operationName || `${targetConstructor.name}.${String(propertyKey)}`;
|
|
196
211
|
|
|
197
|
-
descriptor.value = function (...args: unknown[]) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
);
|
|
212
|
+
descriptor.value = async function (...args: unknown[]) {
|
|
213
|
+
const tracer = otelTrace.getTracer('@onebun/trace');
|
|
214
|
+
|
|
215
|
+
return await tracer.startActiveSpan(spanName, async (activeSpan) => {
|
|
216
|
+
try {
|
|
217
|
+
const result = await originalMethod.apply(this, args);
|
|
218
|
+
activeSpan.setStatus({ code: OtelSpanStatusCode.OK });
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
223
|
+
activeSpan.setStatus({ code: OtelSpanStatusCode.ERROR, message: err.message });
|
|
224
|
+
activeSpan.recordException(err);
|
|
225
|
+
throw error;
|
|
226
|
+
} finally {
|
|
227
|
+
activeSpan.end();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
243
230
|
};
|
|
244
231
|
|
|
245
232
|
return descriptor;
|
|
@@ -247,7 +234,12 @@ export function trace(operationName?: string): MethodDecorator {
|
|
|
247
234
|
}
|
|
248
235
|
|
|
249
236
|
/**
|
|
250
|
-
* Span decorator for creating custom spans
|
|
237
|
+
* Span decorator for creating custom spans around async methods.
|
|
238
|
+
*
|
|
239
|
+
* Lighter version of \@trace — creates a span without automatic error attribute
|
|
240
|
+
* enrichment. Best for internal methods where you want basic span tracking.
|
|
241
|
+
*
|
|
242
|
+
* @param name - Custom span name. Defaults to `ClassName.methodName`
|
|
251
243
|
*/
|
|
252
244
|
export function span(name?: string): MethodDecorator {
|
|
253
245
|
return (target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
|
@@ -258,26 +250,34 @@ export function span(name?: string): MethodDecorator {
|
|
|
258
250
|
: { name: 'Unknown' };
|
|
259
251
|
const spanName = name || `${targetConstructor.name}.${String(propertyKey)}`;
|
|
260
252
|
|
|
261
|
-
descriptor.value = function (...args: unknown[]) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
|
|
253
|
+
descriptor.value = async function (...args: unknown[]) {
|
|
254
|
+
const tracer = otelTrace.getTracer('@onebun/trace');
|
|
255
|
+
|
|
256
|
+
return await tracer.startActiveSpan(spanName, async (activeSpan) => {
|
|
257
|
+
try {
|
|
258
|
+
const result = await originalMethod.apply(this, args);
|
|
259
|
+
|
|
260
|
+
return result;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
263
|
+
activeSpan.setStatus({ code: OtelSpanStatusCode.ERROR, message: err.message });
|
|
264
|
+
throw error;
|
|
265
|
+
} finally {
|
|
266
|
+
activeSpan.end();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
273
269
|
};
|
|
274
270
|
|
|
275
271
|
return descriptor;
|
|
276
272
|
};
|
|
277
273
|
}
|
|
278
274
|
|
|
279
|
-
//
|
|
275
|
+
// Aliases — all point to the same async-compatible implementations
|
|
280
276
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
281
277
|
export const Trace = trace;
|
|
282
278
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
283
279
|
export const Span = span;
|
|
280
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
281
|
+
export const Traced = trace;
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
283
|
+
export const Spanned = span;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { ExportResultCode } from '@opentelemetry/core';
|
|
2
|
+
|
|
3
|
+
import type { ExportResult } from '@opentelemetry/core';
|
|
4
|
+
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OTLP Span Exporter options
|
|
8
|
+
*/
|
|
9
|
+
export interface OtlpExporterOptions {
|
|
10
|
+
/**
|
|
11
|
+
* OTLP endpoint URL (e.g. 'http://localhost:4318')
|
|
12
|
+
*/
|
|
13
|
+
endpoint: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Custom HTTP headers for export requests
|
|
17
|
+
*/
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Request timeout in milliseconds
|
|
22
|
+
* @defaultValue 10000
|
|
23
|
+
*/
|
|
24
|
+
timeout?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
28
|
+
const NANOSECONDS_PER_MILLISECOND = 1000000;
|
|
29
|
+
const NANOSECONDS_PER_SECOND = 1000000000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert hrtime [seconds, nanoseconds] to nanosecond string
|
|
33
|
+
*/
|
|
34
|
+
function hrTimeToNanos(hrTime: [number, number]): string {
|
|
35
|
+
const nanos = BigInt(hrTime[0]) * BigInt(NANOSECONDS_PER_SECOND) + BigInt(hrTime[1]);
|
|
36
|
+
|
|
37
|
+
return nanos.toString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert OTel attribute value to OTLP JSON attribute value
|
|
42
|
+
*/
|
|
43
|
+
function toOtlpValue(value: unknown): Record<string, unknown> {
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
return { stringValue: value };
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === 'number') {
|
|
48
|
+
return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'boolean') {
|
|
51
|
+
return { boolValue: value };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { stringValue: String(value) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Convert OTel attributes to OTLP JSON format
|
|
59
|
+
*/
|
|
60
|
+
function toOtlpAttributes(
|
|
61
|
+
attributes: Record<string, unknown>,
|
|
62
|
+
): Array<{ key: string; value: Record<string, unknown> }> {
|
|
63
|
+
return Object.entries(attributes).map(([key, value]) => ({
|
|
64
|
+
key,
|
|
65
|
+
value: toOtlpValue(value),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Map OTel SpanKind to OTLP integer
|
|
71
|
+
*/
|
|
72
|
+
function mapSpanKind(kind: number): number {
|
|
73
|
+
// OTel SpanKind: INTERNAL=0, SERVER=1, CLIENT=2, PRODUCER=3, CONSUMER=4
|
|
74
|
+
// OTLP: INTERNAL=1, SERVER=2, CLIENT=3, PRODUCER=4, CONSUMER=5
|
|
75
|
+
return kind + 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Map OTel SpanStatusCode to OTLP status code
|
|
80
|
+
*/
|
|
81
|
+
function mapStatusCode(code: number): number {
|
|
82
|
+
// OTel: UNSET=0, OK=1, ERROR=2
|
|
83
|
+
// OTLP: STATUS_CODE_UNSET=0, STATUS_CODE_OK=1, STATUS_CODE_ERROR=2
|
|
84
|
+
return code;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert a ReadableSpan to OTLP JSON span format
|
|
89
|
+
*/
|
|
90
|
+
function spanToOtlp(span: ReadableSpan): Record<string, unknown> {
|
|
91
|
+
const spanContext = span.spanContext();
|
|
92
|
+
|
|
93
|
+
const otlpSpan: Record<string, unknown> = {
|
|
94
|
+
traceId: spanContext.traceId,
|
|
95
|
+
spanId: spanContext.spanId,
|
|
96
|
+
name: span.name,
|
|
97
|
+
kind: mapSpanKind(span.kind),
|
|
98
|
+
startTimeUnixNano: hrTimeToNanos(span.startTime),
|
|
99
|
+
endTimeUnixNano: hrTimeToNanos(span.endTime),
|
|
100
|
+
attributes: toOtlpAttributes(span.attributes as Record<string, unknown>),
|
|
101
|
+
status: {
|
|
102
|
+
code: mapStatusCode(span.status.code),
|
|
103
|
+
message: span.status.message || '',
|
|
104
|
+
},
|
|
105
|
+
events: span.events.map((event) => ({
|
|
106
|
+
timeUnixNano: hrTimeToNanos(event.time),
|
|
107
|
+
name: event.name,
|
|
108
|
+
attributes: event.attributes
|
|
109
|
+
? toOtlpAttributes(event.attributes as Record<string, unknown>)
|
|
110
|
+
: [],
|
|
111
|
+
})),
|
|
112
|
+
links: span.links.map((link) => ({
|
|
113
|
+
traceId: link.context.traceId,
|
|
114
|
+
spanId: link.context.spanId,
|
|
115
|
+
attributes: link.attributes
|
|
116
|
+
? toOtlpAttributes(link.attributes as Record<string, unknown>)
|
|
117
|
+
: [],
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (span.parentSpanContext?.spanId) {
|
|
122
|
+
otlpSpan.parentSpanId = span.parentSpanContext.spanId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (span.droppedAttributesCount > 0) {
|
|
126
|
+
otlpSpan.droppedAttributesCount = span.droppedAttributesCount;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (span.droppedEventsCount > 0) {
|
|
130
|
+
otlpSpan.droppedEventsCount = span.droppedEventsCount;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (span.droppedLinksCount > 0) {
|
|
134
|
+
otlpSpan.droppedLinksCount = span.droppedLinksCount;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return otlpSpan;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Custom OTLP Span Exporter using native fetch() for Bun compatibility.
|
|
142
|
+
*
|
|
143
|
+
* Unlike @opentelemetry/exporter-trace-otlp-http which relies on XMLHttpRequest
|
|
144
|
+
* or Node's http module, this exporter uses the native fetch() API that is
|
|
145
|
+
* guaranteed to work in Bun.
|
|
146
|
+
*/
|
|
147
|
+
export class OtlpFetchSpanExporter implements SpanExporter {
|
|
148
|
+
private readonly endpoint: string;
|
|
149
|
+
private readonly headers: Record<string, string>;
|
|
150
|
+
private readonly timeout: number;
|
|
151
|
+
private isShutdown = false;
|
|
152
|
+
|
|
153
|
+
constructor(options: OtlpExporterOptions) {
|
|
154
|
+
this.endpoint = options.endpoint.replace(/\/$/, '');
|
|
155
|
+
this.headers = {
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
...options.headers,
|
|
159
|
+
};
|
|
160
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
|
|
164
|
+
if (this.isShutdown) {
|
|
165
|
+
resultCallback({ code: ExportResultCode.FAILED });
|
|
166
|
+
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.sendSpans(spans)
|
|
171
|
+
.then(() => {
|
|
172
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
173
|
+
})
|
|
174
|
+
.catch(() => {
|
|
175
|
+
resultCallback({ code: ExportResultCode.FAILED });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async shutdown(): Promise<void> {
|
|
180
|
+
this.isShutdown = true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async forceFlush(): Promise<void> {
|
|
184
|
+
// No internal buffering — BatchSpanProcessor handles batching
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async sendSpans(spans: ReadableSpan[]): Promise<void> {
|
|
188
|
+
if (spans.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Group spans by instrumentation scope
|
|
193
|
+
const scopeSpans = new Map<string, Record<string, unknown>[]>();
|
|
194
|
+
for (const span of spans) {
|
|
195
|
+
const scopeName = span.instrumentationScope.name;
|
|
196
|
+
if (!scopeSpans.has(scopeName)) {
|
|
197
|
+
scopeSpans.set(scopeName, []);
|
|
198
|
+
}
|
|
199
|
+
scopeSpans.get(scopeName)!.push(spanToOtlp(span));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build resource attributes from the first span's resource
|
|
203
|
+
const resource = spans[0].resource;
|
|
204
|
+
const resourceAttributes = toOtlpAttributes(
|
|
205
|
+
resource.attributes as Record<string, unknown>,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const payload = {
|
|
209
|
+
resourceSpans: [
|
|
210
|
+
{
|
|
211
|
+
resource: { attributes: resourceAttributes },
|
|
212
|
+
scopeSpans: Array.from(scopeSpans.entries()).map(([name, scopeSpanList]) => ({
|
|
213
|
+
scope: { name },
|
|
214
|
+
spans: scopeSpanList,
|
|
215
|
+
})),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch(`${this.endpoint}/v1/traces`, {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: this.headers,
|
|
227
|
+
body: JSON.stringify(payload),
|
|
228
|
+
signal: controller.signal,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new Error(`OTLP export failed: ${response.status} ${response.statusText}`);
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
clearTimeout(timeoutId);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Utility: convert milliseconds to nanosecond string
|
|
242
|
+
*/
|
|
243
|
+
export function msToNanos(ms: number): string {
|
|
244
|
+
return (BigInt(Math.round(ms)) * BigInt(NANOSECONDS_PER_MILLISECOND)).toString();
|
|
245
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { trace } from '@opentelemetry/api';
|
|
2
|
+
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
3
|
+
import { BatchSpanProcessor, BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
4
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
5
|
+
|
|
6
|
+
import type { TraceOptions } from './types.js';
|
|
7
|
+
|
|
8
|
+
import { OtlpFetchSpanExporter } from './otlp-exporter.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Result of TracerProvider initialization
|
|
12
|
+
*/
|
|
13
|
+
export interface TracerProviderResult {
|
|
14
|
+
/**
|
|
15
|
+
* The initialized BasicTracerProvider
|
|
16
|
+
*/
|
|
17
|
+
provider: BasicTracerProvider;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shutdown function that flushes pending spans and shuts down the provider
|
|
21
|
+
*/
|
|
22
|
+
shutdown: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize and register a global TracerProvider.
|
|
27
|
+
*
|
|
28
|
+
* When `exportOptions.endpoint` is configured, creates a BatchSpanProcessor
|
|
29
|
+
* with a custom fetch-based OTLP exporter for Bun compatibility.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Trace configuration options
|
|
32
|
+
* @returns Provider instance and shutdown function
|
|
33
|
+
*/
|
|
34
|
+
export function initTracerProvider(options: TraceOptions): TracerProviderResult {
|
|
35
|
+
const resource = resourceFromAttributes({
|
|
36
|
+
[ATTR_SERVICE_NAME]: options.serviceName ?? 'onebun-service',
|
|
37
|
+
[ATTR_SERVICE_VERSION]: options.serviceVersion ?? '1.0.0',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const spanProcessors = [];
|
|
41
|
+
|
|
42
|
+
if (options.exportOptions?.endpoint) {
|
|
43
|
+
const exporter = new OtlpFetchSpanExporter({
|
|
44
|
+
endpoint: options.exportOptions.endpoint,
|
|
45
|
+
headers: options.exportOptions.headers,
|
|
46
|
+
timeout: options.exportOptions.timeout,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
spanProcessors.push(
|
|
50
|
+
new BatchSpanProcessor(exporter, {
|
|
51
|
+
maxExportBatchSize: options.exportOptions.batchSize,
|
|
52
|
+
scheduledDelayMillis: options.exportOptions.batchTimeout,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const provider = new BasicTracerProvider({
|
|
58
|
+
resource,
|
|
59
|
+
spanProcessors,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Register as global TracerProvider so trace.getTracer() returns real tracers
|
|
63
|
+
trace.setGlobalTracerProvider(provider);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
provider,
|
|
67
|
+
async shutdown() {
|
|
68
|
+
await provider.shutdown();
|
|
69
|
+
// Disable the global provider to avoid using a shutdown provider
|
|
70
|
+
trace.disable();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/trace.service.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
import { HttpStatusCode } from '@onebun/requests';
|
|
15
15
|
|
|
16
|
+
import { initTracerProvider, type TracerProviderResult } from './provider.js';
|
|
16
17
|
import {
|
|
17
18
|
type HttpTraceData,
|
|
18
19
|
type SpanStatus,
|
|
@@ -89,6 +90,11 @@ export interface TraceService {
|
|
|
89
90
|
* End HTTP request tracing
|
|
90
91
|
*/
|
|
91
92
|
endHttpTrace(span: TraceSpan, data: Partial<HttpTraceData>): Effect.Effect<void>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Shutdown the trace service, flushing pending spans
|
|
96
|
+
*/
|
|
97
|
+
shutdown(): Promise<void>;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
/**
|
|
@@ -111,8 +117,9 @@ export const currentSpan = FiberRef.unsafeMake<TraceSpan | null>(null);
|
|
|
111
117
|
* Implementation of TraceService
|
|
112
118
|
*/
|
|
113
119
|
export class TraceServiceImpl implements TraceService {
|
|
114
|
-
private readonly tracer
|
|
120
|
+
private readonly tracer;
|
|
115
121
|
private readonly options: Required<TraceOptions>;
|
|
122
|
+
private readonly providerResult: TracerProviderResult | null = null;
|
|
116
123
|
|
|
117
124
|
constructor(options: TraceOptions = {}) {
|
|
118
125
|
this.options = {
|
|
@@ -126,6 +133,20 @@ export class TraceServiceImpl implements TraceService {
|
|
|
126
133
|
exportOptions: {},
|
|
127
134
|
...options,
|
|
128
135
|
};
|
|
136
|
+
|
|
137
|
+
// Initialize TracerProvider BEFORE creating the tracer
|
|
138
|
+
// so trace.getTracer() returns a real tracer, not NoopTracer
|
|
139
|
+
if (this.options.enabled) {
|
|
140
|
+
this.providerResult = initTracerProvider(this.options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.tracer = trace.getTracer('@onebun/trace');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async shutdown(): Promise<void> {
|
|
147
|
+
if (this.providerResult) {
|
|
148
|
+
await this.providerResult.shutdown();
|
|
149
|
+
}
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
getCurrentContext(): Effect.Effect<TraceContext | null> {
|