@@ -90,6 +90,40 @@ function getValidatedState(state: Partial<MetricBase>, definition: MetricDefinit
90
90
return missingFields . length !== 0 ? Object . assign ( { missingFields } , state ) : state
91
91
}
92
92
93
+ /**
94
+ * Options used for the creation of a span
95
+ */
96
+ export type SpanOptions = {
97
+ /** True if this span should emit its telemetry events. Defaults to true if undefined. */
98
+ emit ?: boolean
99
+
100
+ /**
101
+ * Adds a function entry to the span stack.
102
+ *
103
+ * This allows you to eventually retrieve the function entry stack by using {@link TelemetryTracer.getFunctionStack()},
104
+ * which tells you the chain of function executions to bring you to that point in the code.
105
+ *
106
+ * Example:
107
+ * ```
108
+ * function a() {
109
+ * telemetry.your_Metric.run(() => b(), { functionId: { name: 'a'} })
110
+ * }
111
+ *
112
+ * function b() {
113
+ * telemetry.your_Metric.run(() => c(), { functionId: { name: 'b'} })
114
+ * }
115
+ *
116
+ * function c() {
117
+ * telemetry.your_Metric.run(() => {
118
+ * const stack = telemetry.getFunctionStack()
119
+ * console.log(stack) // [ {source: 'a' }, { source: 'b' }, { source: 'c' }]
120
+ * }, { functionId: { name: 'c'} })
121
+ * }
122
+ * ```
123
+ */
124
+ functionId ?: FunctionEntry
125
+ }
126
+
93
127
/**
94
128
* A span represents a "unit of work" captured for logging/telemetry.
95
129
* It can contain other spans recursively, then it's called a "trace" or "flow".
@@ -99,6 +133,7 @@ function getValidatedState(state: Partial<MetricBase>, definition: MetricDefinit
99
133
*/
100
134
export class TelemetrySpan < T extends MetricBase = MetricBase > {
101
135
#startTime?: Date
136
+ #options: SpanOptions
102
137
103
138
private readonly state : Partial < T > = { }
104
139
private readonly definition = definitions [ this . name ] ?? {
@@ -113,7 +148,17 @@ export class TelemetrySpan<T extends MetricBase = MetricBase> {
113
148
*/
114
149
static readonly #excludedFields = [ 'passive' , 'value' ]
115
150
116
- public constructor ( public readonly name : string ) { }
151
+ public constructor (
152
+ public readonly name : string ,
153
+ options ?: SpanOptions
154
+ ) {
155
+ // set defaults on undefined options
156
+ this . #options = {
157
+ // do emit by default
158
+ emit : options ?. emit === undefined ? true : options . emit ,
159
+ functionId : options ?. functionId ,
160
+ }
161
+ }
117
162
118
163
public get startTime ( ) : Date | undefined {
119
164
return this . #startTime
@@ -124,6 +169,10 @@ export class TelemetrySpan<T extends MetricBase = MetricBase> {
124
169
return this
125
170
}
126
171
172
+ public getFunctionEntry ( ) : Readonly < FunctionEntry > | undefined {
173
+ return this . #options. functionId
174
+ }
175
+
127
176
public emit ( data ?: Partial < T > ) : void {
128
177
const state = getValidatedState ( { ...this . state , ...data } , this . definition )
129
178
const metadata = Object . entries ( state )
@@ -157,14 +206,16 @@ export class TelemetrySpan<T extends MetricBase = MetricBase> {
157
206
public stop ( err ?: unknown ) : void {
158
207
const duration = this . startTime !== undefined ? globals . clock . Date . now ( ) - this . startTime . getTime ( ) : undefined
159
208
160
- this . emit ( {
161
- duration,
162
- result : getTelemetryResult ( err ) ,
163
- reason : getTelemetryReason ( err ) ,
164
- reasonDesc : getTelemetryReasonDesc ( err ) ,
165
- requestId : getRequestId ( err ) ,
166
- httpStatusCode : getHttpStatusCode ( err ) ,
167
- } as Partial < T > )
209
+ if ( this . #options. emit ) {
210
+ this . emit ( {
211
+ duration,
212
+ result : getTelemetryResult ( err ) ,
213
+ reason : getTelemetryReason ( err ) ,
214
+ reasonDesc : getTelemetryReasonDesc ( err ) ,
215
+ requestId : getRequestId ( err ) ,
216
+ httpStatusCode : getHttpStatusCode ( err ) ,
217
+ } as Partial < T > )
218
+ }
168
219
169
220
this . #startTime = undefined
170
221
}
@@ -256,8 +307,12 @@ export class TelemetryTracer extends TelemetryBase {
256
307
* All changes made to {@link attributes} (via {@link record}) during the execution are
257
308
* reverted after the execution completes.
258
309
*/
259
- public run < T , U extends MetricName > ( name : U , fn : ( span : Metric < MetricShapes [ U ] > ) => T ) : T {
260
- const span = this . createSpan ( name ) . start ( )
310
+ public run < T , U extends MetricName > (
311
+ name : U ,
312
+ fn : ( span : Metric < MetricShapes [ U ] > ) => T ,
313
+ options ?: SpanOptions | undefined
314
+ ) : T {
315
+ const span = this . createSpan ( name , options ) . start ( )
261
316
const frame = this . switchContext ( span )
262
317
263
318
try {
@@ -301,6 +356,32 @@ export class TelemetryTracer extends TelemetryBase {
301
356
return this . #context. run ( frame , fn )
302
357
}
303
358
359
+ /**
360
+ * Returns the stack of all {@link FunctionEntry}s with the 0th
361
+ * index being the top level call, and the last index being the final
362
+ * nested call.
363
+ *
364
+ * Ensure that {@link TelemetryTracer.runWithCallEntry()} and/or {@link TelemetrySpan.recordCallEntry()}
365
+ * have been used before this method is called, otherwise it will return
366
+ * no useful information.
367
+ *
368
+ * Use {@link asStringifiedStack} to create a stringified version of this stack.
369
+ */
370
+ public getFunctionStack ( ) : FunctionEntry [ ] {
371
+ const stack : FunctionEntry [ ] = [ ]
372
+ const endIndex = this . spans . length - 1
373
+ let i = endIndex
374
+ while ( i >= 0 ) {
375
+ const span = this . spans [ i ]
376
+ const entry = span . getFunctionEntry ( )
377
+ if ( entry ) {
378
+ stack . push ( entry )
379
+ }
380
+ i -= 1
381
+ }
382
+ return stack
383
+ }
384
+
304
385
/**
305
386
* Wraps a function with {@link run}.
306
387
*
@@ -322,7 +403,7 @@ export class TelemetryTracer extends TelemetryBase {
322
403
name,
323
404
emit : ( data ) => getSpan ( ) . emit ( data ) ,
324
405
record : ( data ) => getSpan ( ) . record ( data ) ,
325
- run : ( fn ) => this . run ( name as MetricName , fn ) ,
406
+ run : ( fn , options ?: SpanOptions ) => this . run ( name as MetricName , fn , options ) ,
326
407
increment : ( data ) => getSpan ( ) . increment ( data ) ,
327
408
}
328
409
}
@@ -331,8 +412,8 @@ export class TelemetryTracer extends TelemetryBase {
331
412
return this . spans . find ( ( s ) => s . name === name ) ?? this . createSpan ( name )
332
413
}
333
414
334
- private createSpan ( name : string ) : TelemetrySpan {
335
- const span = new TelemetrySpan ( name ) . record ( this . attributes ?? { } )
415
+ private createSpan ( name : string , options ?: SpanOptions ) : TelemetrySpan {
416
+ const span = new TelemetrySpan ( name , options ) . record ( this . attributes ?? { } )
336
417
if ( this . activeSpan && this . activeSpan . name !== rootSpanName ) {
337
418
return span . record ( { parentMetric : this . activeSpan . name } satisfies { parentMetric : string } as any )
338
419
}
@@ -349,3 +430,62 @@ export class TelemetryTracer extends TelemetryBase {
349
430
return ctx
350
431
}
351
432
}
433
+
434
+ /**
435
+ * A Function Entry is a single entry in to a stack of Function Entries.
436
+ *
437
+ * Think of a Function Entry as one entry in the stack trace of an Error.
438
+ * So a stack of Function Entries will allows you to build a path of functions.
439
+ * This can allow you to trace the path of executions.
440
+ *
441
+ * In MOST cases, a Function Entry will represent a method/function call, but it is not
442
+ * limited to that.
443
+ */
444
+ export type FunctionEntry = {
445
+ /**
446
+ * An identifier that represents the callback. You'll probably want to use the function name.
447
+ */
448
+ readonly name : string
449
+
450
+ /**
451
+ * If the source is a method, you'll want to include the class name for better context.
452
+ */
453
+ readonly class ?: string
454
+ }
455
+
456
+ /**
457
+ * Returns a stringified version of the provided {@link ExecutionContext.stack}.
458
+ *
459
+ * Eg: "TestClassA1#methodA,methodB:TestClassA2#methodX,methodY,thisIsAlsoZ:someFunction"
460
+ *
461
+ * - '#' separates a class from its methods
462
+ * - ',' separates methods of the same class
463
+ * - ':' separates classes/functions
464
+ * - The call stack goes in order from left to right
465
+ * - The first item in the string is the top level, initial caller in the stack
466
+ * - The last item is the final caller in the stack
467
+ *
468
+ * See tests for examples.
469
+ */
470
+ export function asStringifiedStack ( stack : FunctionEntry [ ] ) : string {
471
+ let prevEntry : FunctionEntry | undefined
472
+ let currString : string = ''
473
+
474
+ // Iterate over each entry, appending the source and class to the final output string
475
+ for ( const currEntry of stack ) {
476
+ const prevClass = prevEntry ?. class
477
+ const newClass = currEntry . class
478
+
479
+ if ( prevClass && prevClass === newClass ) {
480
+ // The new class is same as the prev class, so we don't need to add the class since it already exists
481
+ currString = `${ currString } ,${ currEntry . name } `
482
+ } else {
483
+ // The new class may be different from the prev class, so start a new subsection, adding the new class if it exists.
484
+ currString = `${ currString ? currString + ':' : '' } ${ newClass ? newClass + '#' : '' } ${ currEntry . name } `
485
+ }
486
+
487
+ prevEntry = currEntry
488
+ }
489
+
490
+ return currString
491
+ }
0 commit comments