@onebun/trace 0.2.2 → 0.2.3
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 +1 -1
- package/src/auto-trace.ts +343 -0
- package/src/index.ts +15 -0
- package/src/middleware.ts +96 -4
package/package.json
CHANGED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { SpanStatusCode as OtelSpanStatusCode, trace as otelTrace } from '@opentelemetry/api';
|
|
2
|
+
|
|
3
|
+
import { type SpanAttributeEntry, SPAN_ATTRIBUTES } from './middleware.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Symbols for trace metadata on classes and methods
|
|
7
|
+
*/
|
|
8
|
+
export const NO_TRACE = Symbol.for('onebun:noTrace');
|
|
9
|
+
export const TRACE_ALL = Symbol.for('onebun:traceAll');
|
|
10
|
+
export const ALREADY_TRACED = Symbol.for('onebun:traced');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Filter options for auto-tracing
|
|
14
|
+
*/
|
|
15
|
+
export interface TraceFilterOptions {
|
|
16
|
+
asyncOnly?: boolean;
|
|
17
|
+
excludeMethods?: string[];
|
|
18
|
+
includeClasses?: string[];
|
|
19
|
+
excludeClasses?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Built-in methods that should never be auto-traced.
|
|
24
|
+
* Includes framework base class methods, lifecycle hooks, and internals.
|
|
25
|
+
*/
|
|
26
|
+
const EXCLUDED_METHODS = new Set([
|
|
27
|
+
'constructor',
|
|
28
|
+
// BaseService
|
|
29
|
+
'initializeService',
|
|
30
|
+
'runEffect',
|
|
31
|
+
'formatError',
|
|
32
|
+
// BaseController
|
|
33
|
+
'initializeController',
|
|
34
|
+
'getService',
|
|
35
|
+
'setService',
|
|
36
|
+
'isJson',
|
|
37
|
+
'parseJson',
|
|
38
|
+
'success',
|
|
39
|
+
'error',
|
|
40
|
+
'json',
|
|
41
|
+
'text',
|
|
42
|
+
'sse',
|
|
43
|
+
// Lifecycle hooks
|
|
44
|
+
'onModuleInit',
|
|
45
|
+
'onApplicationInit',
|
|
46
|
+
'onModuleDestroy',
|
|
47
|
+
'beforeApplicationDestroy',
|
|
48
|
+
'onApplicationDestroy',
|
|
49
|
+
'onQueueReady',
|
|
50
|
+
// Middleware
|
|
51
|
+
'use',
|
|
52
|
+
'configureMiddleware',
|
|
53
|
+
// WebSocket
|
|
54
|
+
'_initializeBase',
|
|
55
|
+
'afterInit',
|
|
56
|
+
'handleConnection',
|
|
57
|
+
'handleDisconnect',
|
|
58
|
+
// BaseService/Controller span getter
|
|
59
|
+
'span',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Simple glob matching: supports `*` as wildcard.
|
|
64
|
+
* `*Service` matches `UserService`, `OrderService`.
|
|
65
|
+
* `User*` matches `UserService`, `UserController`.
|
|
66
|
+
* `*` matches everything.
|
|
67
|
+
*/
|
|
68
|
+
function matchGlob(pattern: string, name: string): boolean {
|
|
69
|
+
if (pattern === '*') {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (pattern.startsWith('*') && pattern.endsWith('*')) {
|
|
74
|
+
return name.includes(pattern.slice(1, -1));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (pattern.startsWith('*')) {
|
|
78
|
+
return name.endsWith(pattern.slice(1));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (pattern.endsWith('*')) {
|
|
82
|
+
return name.startsWith(pattern.slice(0, -1));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return name === pattern;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a class name matches the glob filter.
|
|
90
|
+
*/
|
|
91
|
+
function matchesClassFilter(
|
|
92
|
+
className: string,
|
|
93
|
+
filter?: TraceFilterOptions,
|
|
94
|
+
): boolean {
|
|
95
|
+
if (!filter) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If includeClasses is set, class must match at least one pattern
|
|
100
|
+
if (filter.includeClasses && filter.includeClasses.length > 0) {
|
|
101
|
+
if (!filter.includeClasses.some((pattern) => matchGlob(pattern, className))) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If excludeClasses is set, class must NOT match any pattern
|
|
107
|
+
if (filter.excludeClasses && filter.excludeClasses.length > 0) {
|
|
108
|
+
if (filter.excludeClasses.some((pattern) => matchGlob(pattern, className))) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a method is async (its constructor is AsyncFunction).
|
|
118
|
+
*/
|
|
119
|
+
function isAsyncFunction(fn: unknown): boolean {
|
|
120
|
+
return typeof fn === 'function' && fn.constructor.name === 'AsyncFunction';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Wrap a method with a span. Same pattern as @Traced() but applied at runtime.
|
|
125
|
+
*/
|
|
126
|
+
function wrapMethodWithSpan(
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
prototype: any,
|
|
129
|
+
methodName: string,
|
|
130
|
+
className: string,
|
|
131
|
+
): void {
|
|
132
|
+
const original = prototype[methodName];
|
|
133
|
+
const spanName = `${className}.${methodName}`;
|
|
134
|
+
|
|
135
|
+
const wrapped = async function (
|
|
136
|
+
this: unknown,
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
...args: any[]
|
|
139
|
+
) {
|
|
140
|
+
const tracer = otelTrace.getTracer('@onebun/trace');
|
|
141
|
+
|
|
142
|
+
return await tracer.startActiveSpan(spanName, async (activeSpan) => {
|
|
143
|
+
// Apply @SpanAttribute metadata if present
|
|
144
|
+
const meta = prototype[SPAN_ATTRIBUTES]?.[methodName] as
|
|
145
|
+
| SpanAttributeEntry[]
|
|
146
|
+
| undefined;
|
|
147
|
+
if (meta) {
|
|
148
|
+
for (const { paramIndex, attrName } of meta) {
|
|
149
|
+
const value = args[paramIndex];
|
|
150
|
+
if (value !== undefined && value !== null) {
|
|
151
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
152
|
+
activeSpan.setAttribute(attrName, value);
|
|
153
|
+
} else {
|
|
154
|
+
activeSpan.setAttribute(attrName, JSON.stringify(value));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await original.apply(this, args);
|
|
162
|
+
activeSpan.setStatus({ code: OtelSpanStatusCode.OK });
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
167
|
+
activeSpan.setStatus({ code: OtelSpanStatusCode.ERROR, message: err.message });
|
|
168
|
+
activeSpan.recordException(err);
|
|
169
|
+
throw error;
|
|
170
|
+
} finally {
|
|
171
|
+
activeSpan.end();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Mark as auto-traced to prevent double-wrapping
|
|
177
|
+
(wrapped as unknown as Record<symbol, boolean>)[ALREADY_TRACED] = true;
|
|
178
|
+
prototype[methodName] = wrapped;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Apply automatic tracing to all eligible methods on an instance.
|
|
183
|
+
*
|
|
184
|
+
* Walks the prototype chain (stopping at Object.prototype) and wraps
|
|
185
|
+
* methods with OpenTelemetry spans. Respects @NoTrace(), @Traced(),
|
|
186
|
+
* and filter options.
|
|
187
|
+
*
|
|
188
|
+
* @param instance - The service or controller instance
|
|
189
|
+
* @param className - The class name for span naming
|
|
190
|
+
* @param filter - Optional filter options
|
|
191
|
+
*/
|
|
192
|
+
export function applyAutoTrace(
|
|
193
|
+
instance: unknown,
|
|
194
|
+
className: string,
|
|
195
|
+
filter?: TraceFilterOptions,
|
|
196
|
+
): void {
|
|
197
|
+
if (!instance || typeof instance !== 'object') {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const asyncOnly = filter?.asyncOnly ?? true;
|
|
202
|
+
const extraExcludedMethods = filter?.excludeMethods
|
|
203
|
+
? new Set(filter.excludeMethods)
|
|
204
|
+
: null;
|
|
205
|
+
|
|
206
|
+
// Walk prototype chain, collecting methods to wrap
|
|
207
|
+
let proto = Object.getPrototypeOf(instance);
|
|
208
|
+
while (proto && proto !== Object.prototype) {
|
|
209
|
+
const methodNames = Object.getOwnPropertyNames(proto);
|
|
210
|
+
|
|
211
|
+
for (const methodName of methodNames) {
|
|
212
|
+
// Skip built-in excluded methods
|
|
213
|
+
if (EXCLUDED_METHODS.has(methodName)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Skip user-specified excluded methods
|
|
218
|
+
if (extraExcludedMethods?.has(methodName)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
|
|
223
|
+
if (!descriptor || !descriptor.value || typeof descriptor.value !== 'function') {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Skip getters/setters
|
|
228
|
+
if (descriptor.get || descriptor.set) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const method = descriptor.value;
|
|
233
|
+
|
|
234
|
+
// Skip already-traced methods (@Traced was applied manually)
|
|
235
|
+
if ((method as unknown as Record<symbol, boolean>)[ALREADY_TRACED]) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Skip @NoTrace methods
|
|
240
|
+
if ((method as unknown as Record<symbol, boolean>)[NO_TRACE]) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// If asyncOnly, skip sync methods
|
|
245
|
+
if (asyncOnly && !isAsyncFunction(method)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
wrapMethodWithSpan(proto, methodName, className);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
proto = Object.getPrototypeOf(proto);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @TraceAll() — class decorator.
|
|
258
|
+
* Marks a class for auto-tracing even when traceAll is false in config.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* \@Service()
|
|
263
|
+
* \@TraceAll()
|
|
264
|
+
* class UserService extends BaseService {
|
|
265
|
+
* async findAll() { ... } // auto-traced
|
|
266
|
+
* }
|
|
267
|
+
*/
|
|
268
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
269
|
+
export function TraceAll(): ClassDecorator {
|
|
270
|
+
return (target: Function) => {
|
|
271
|
+
(target as unknown as Record<symbol, boolean>)[TRACE_ALL] = true;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @NoTrace() — class or method decorator.
|
|
277
|
+
* Excludes a class or method from auto-tracing.
|
|
278
|
+
*
|
|
279
|
+
* On a class: prevents all methods from being auto-traced.
|
|
280
|
+
* On a method: prevents that specific method from being auto-traced.
|
|
281
|
+
* Note: @Traced() on a method takes priority over @NoTrace() on the class.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* \@Service()
|
|
286
|
+
* \@NoTrace()
|
|
287
|
+
* class InternalService extends BaseService {
|
|
288
|
+
* async internalWork() { ... } // NOT auto-traced
|
|
289
|
+
*
|
|
290
|
+
* \@Traced() // Override: this method IS traced
|
|
291
|
+
* async importantWork() { ... }
|
|
292
|
+
* }
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
296
|
+
export function NoTrace(): ClassDecorator & MethodDecorator {
|
|
297
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
298
|
+
return (target: any, propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => {
|
|
299
|
+
if (descriptor && propertyKey) {
|
|
300
|
+
// Method decorator
|
|
301
|
+
(descriptor.value as unknown as Record<symbol, boolean>)[NO_TRACE] = true;
|
|
302
|
+
} else {
|
|
303
|
+
// Class decorator
|
|
304
|
+
(target as unknown as Record<symbol, boolean>)[NO_TRACE] = true;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check if a class should be auto-traced based on global config and decorators.
|
|
311
|
+
*
|
|
312
|
+
* @param classConstructor - The class constructor
|
|
313
|
+
* @param className - The class name
|
|
314
|
+
* @param traceAll - Global traceAll setting
|
|
315
|
+
* @param filter - Filter options
|
|
316
|
+
* @returns true if the class should have auto-tracing applied
|
|
317
|
+
*/
|
|
318
|
+
export function shouldAutoTrace(
|
|
319
|
+
classConstructor: Function,
|
|
320
|
+
className: string,
|
|
321
|
+
traceAll: boolean,
|
|
322
|
+
filter?: TraceFilterOptions,
|
|
323
|
+
): boolean {
|
|
324
|
+
const hasTraceAll = (classConstructor as unknown as Record<symbol, boolean>)[TRACE_ALL] === true;
|
|
325
|
+
const hasNoTrace = (classConstructor as unknown as Record<symbol, boolean>)[NO_TRACE] === true;
|
|
326
|
+
|
|
327
|
+
// @NoTrace on class → skip (unless method-level @Traced, but that's handled in applyAutoTrace)
|
|
328
|
+
if (hasNoTrace) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// @TraceAll on class → trace regardless of global setting
|
|
333
|
+
if (hasTraceAll) {
|
|
334
|
+
return matchesClassFilter(className, filter);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Global traceAll
|
|
338
|
+
if (traceAll) {
|
|
339
|
+
return matchesClassFilter(className, filter);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return false;
|
|
343
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ export type { Context } from '@opentelemetry/api';
|
|
|
15
15
|
export {
|
|
16
16
|
createTraceMiddleware,
|
|
17
17
|
Span,
|
|
18
|
+
SPAN_ATTRIBUTES,
|
|
19
|
+
SpanAttribute,
|
|
20
|
+
type SpanAttributeEntry,
|
|
18
21
|
span,
|
|
19
22
|
Spanned,
|
|
20
23
|
Trace,
|
|
@@ -23,6 +26,18 @@ export {
|
|
|
23
26
|
trace,
|
|
24
27
|
} from './middleware.js';
|
|
25
28
|
|
|
29
|
+
// Auto-trace
|
|
30
|
+
export {
|
|
31
|
+
ALREADY_TRACED,
|
|
32
|
+
applyAutoTrace,
|
|
33
|
+
NO_TRACE,
|
|
34
|
+
NoTrace,
|
|
35
|
+
shouldAutoTrace,
|
|
36
|
+
TRACE_ALL,
|
|
37
|
+
TraceAll,
|
|
38
|
+
type TraceFilterOptions,
|
|
39
|
+
} from './auto-trace.js';
|
|
40
|
+
|
|
26
41
|
// OTLP exporter
|
|
27
42
|
export { OtlpFetchSpanExporter, type OtlpExporterOptions } from './otlp-exporter.js';
|
|
28
43
|
|
package/src/middleware.ts
CHANGED
|
@@ -5,8 +5,86 @@ import type { HttpTraceData, TraceHeaders } from './types.js';
|
|
|
5
5
|
|
|
6
6
|
import { HttpStatusCode } from '@onebun/requests';
|
|
7
7
|
|
|
8
|
+
import { ALREADY_TRACED } from './auto-trace.js';
|
|
8
9
|
import { traceService } from './trace.service.js';
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Symbol for storing @SpanAttribute metadata on the prototype
|
|
13
|
+
*/
|
|
14
|
+
export const SPAN_ATTRIBUTES = Symbol.for('onebun:spanAttributes');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Metadata entry for a @SpanAttribute parameter
|
|
18
|
+
*/
|
|
19
|
+
export interface SpanAttributeEntry {
|
|
20
|
+
paramIndex: number;
|
|
21
|
+
attrName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read @SpanAttribute metadata and set span attributes from method arguments
|
|
26
|
+
*/
|
|
27
|
+
function applySpanAttributes(
|
|
28
|
+
target: unknown,
|
|
29
|
+
methodName: string | symbol,
|
|
30
|
+
args: unknown[],
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
activeSpan: any,
|
|
33
|
+
): void {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
const meta = (target as any)?.[SPAN_ATTRIBUTES]?.[String(methodName)] as
|
|
36
|
+
| SpanAttributeEntry[]
|
|
37
|
+
| undefined;
|
|
38
|
+
if (!meta) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const { paramIndex, attrName } of meta) {
|
|
43
|
+
const value = args[paramIndex];
|
|
44
|
+
if (value === undefined || value === null) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
49
|
+
activeSpan.setAttribute(attrName, value);
|
|
50
|
+
} else {
|
|
51
|
+
activeSpan.setAttribute(attrName, JSON.stringify(value));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @SpanAttribute() — parameter decorator.
|
|
58
|
+
* Automatically records the method argument as a span attribute when used
|
|
59
|
+
* with @Traced() or @Spanned().
|
|
60
|
+
*
|
|
61
|
+
* @param name - The span attribute name (e.g. 'user.id', 'db.operation')
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* \@Traced()
|
|
66
|
+
* async findById(
|
|
67
|
+
* \@SpanAttribute('user.id') id: string,
|
|
68
|
+
* ): Promise<User | null> { ... }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
72
|
+
export function SpanAttribute(name: string): ParameterDecorator {
|
|
73
|
+
return (target: object, propertyKey: string | symbol | undefined, parameterIndex: number) => {
|
|
74
|
+
if (!propertyKey) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
const existing = (target as any)[SPAN_ATTRIBUTES] ?? {};
|
|
80
|
+
const key = String(propertyKey);
|
|
81
|
+
existing[key] = existing[key] ?? [];
|
|
82
|
+
existing[key].push({ paramIndex: parameterIndex, attrName: name });
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
(target as any)[SPAN_ATTRIBUTES] = existing;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
10
88
|
/**
|
|
11
89
|
* HTTP trace middleware for OneBun applications
|
|
12
90
|
*/
|
|
@@ -208,11 +286,14 @@ export function trace(operationName?: string): MethodDecorator {
|
|
|
208
286
|
? target.constructor
|
|
209
287
|
: { name: 'Unknown' };
|
|
210
288
|
const spanName = operationName || `${targetConstructor.name}.${String(propertyKey)}`;
|
|
289
|
+
const decoratorTarget = target;
|
|
211
290
|
|
|
212
|
-
|
|
291
|
+
const wrapped = async function (this: unknown, ...args: unknown[]) {
|
|
213
292
|
const tracer = otelTrace.getTracer('@onebun/trace');
|
|
214
293
|
|
|
215
294
|
return await tracer.startActiveSpan(spanName, async (activeSpan) => {
|
|
295
|
+
applySpanAttributes(decoratorTarget, propertyKey, args, activeSpan);
|
|
296
|
+
|
|
216
297
|
try {
|
|
217
298
|
const result = await originalMethod.apply(this, args);
|
|
218
299
|
activeSpan.setStatus({ code: OtelSpanStatusCode.OK });
|
|
@@ -229,6 +310,10 @@ export function trace(operationName?: string): MethodDecorator {
|
|
|
229
310
|
});
|
|
230
311
|
};
|
|
231
312
|
|
|
313
|
+
// Mark as traced to prevent auto-trace from double-wrapping
|
|
314
|
+
(wrapped as unknown as Record<symbol, boolean>)[ALREADY_TRACED] = true;
|
|
315
|
+
descriptor.value = wrapped;
|
|
316
|
+
|
|
232
317
|
return descriptor;
|
|
233
318
|
};
|
|
234
319
|
}
|
|
@@ -248,12 +333,15 @@ export function span(name?: string): MethodDecorator {
|
|
|
248
333
|
target && typeof target === 'object' && target.constructor
|
|
249
334
|
? target.constructor
|
|
250
335
|
: { name: 'Unknown' };
|
|
251
|
-
const
|
|
336
|
+
const resolvedName = name || `${targetConstructor.name}.${String(propertyKey)}`;
|
|
337
|
+
const decoratorTarget = target;
|
|
252
338
|
|
|
253
|
-
|
|
339
|
+
const wrapped = async function (this: unknown, ...args: unknown[]) {
|
|
254
340
|
const tracer = otelTrace.getTracer('@onebun/trace');
|
|
255
341
|
|
|
256
|
-
return await tracer.startActiveSpan(
|
|
342
|
+
return await tracer.startActiveSpan(resolvedName, async (activeSpan) => {
|
|
343
|
+
applySpanAttributes(decoratorTarget, propertyKey, args, activeSpan);
|
|
344
|
+
|
|
257
345
|
try {
|
|
258
346
|
const result = await originalMethod.apply(this, args);
|
|
259
347
|
|
|
@@ -268,6 +356,10 @@ export function span(name?: string): MethodDecorator {
|
|
|
268
356
|
});
|
|
269
357
|
};
|
|
270
358
|
|
|
359
|
+
// Mark as traced to prevent auto-trace from double-wrapping
|
|
360
|
+
(wrapped as unknown as Record<symbol, boolean>)[ALREADY_TRACED] = true;
|
|
361
|
+
descriptor.value = wrapped;
|
|
362
|
+
|
|
271
363
|
return descriptor;
|
|
272
364
|
};
|
|
273
365
|
}
|