Skip to content
This repository was archived by the owner on Nov 10, 2022. It is now read-only.

Commit 81fd6a9

Browse files
naseemkullahdyladan
andcommitted
feat: add tracer.startActiveSpan()
Add a helper method that starts a new and sets it in the currently active (or otherwise specified) context and calls a given function passing it the created span as first argument. This method is not strictly part of the spec, but the spec does allow (encourage?) it to be added for convenience: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span-creation For best UX, this method has overloads such that it can take in name,fn or name,opts,fn or name,opts,ctx,fn as args. These overloads caused some issues with sinon.stub in the proxy-tracer test but was able to work around it by use of "any". A link to the gh issue was added as a comment in said test. Signed-off-by: naseemkullah <[email protected]> Co-authored-by: Gerhard Stöbich <[email protected]> Co-authored-by: Daniel Dyla <[email protected]>
1 parent 8435e0a commit 81fd6a9

File tree

5 files changed

+177
-10
lines changed

5 files changed

+177
-10
lines changed

src/trace/NoopTracer.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { getSpanContext } from '../trace/context-utils';
17+
import { context } from '../';
1818
import { Context } from '../context/types';
19+
import { getSpanContext, setSpan } from '../trace/context-utils';
1920
import { NonRecordingSpan } from './NonRecordingSpan';
2021
import { Span } from './span';
2122
import { isSpanContextValid } from './spancontext-utils';
@@ -45,6 +46,45 @@ export class NoopTracer implements Tracer {
4546
return new NonRecordingSpan();
4647
}
4748
}
49+
50+
startActiveSpan<F extends (span: Span) => ReturnType<F>>(
51+
name: string,
52+
arg2: F | SpanOptions,
53+
arg3?: F | Context,
54+
arg4?: F
55+
): ReturnType<F> | undefined {
56+
let fn: F | undefined,
57+
options: SpanOptions | undefined,
58+
activeContext: Context | undefined;
59+
if (arguments.length === 2 && typeof arg2 === 'function') {
60+
fn = arg2;
61+
} else if (
62+
arguments.length === 3 &&
63+
typeof arg2 === 'object' &&
64+
typeof arg3 === 'function'
65+
) {
66+
options = arg2;
67+
fn = arg3;
68+
} else if (
69+
arguments.length === 4 &&
70+
typeof arg2 === 'object' &&
71+
typeof arg3 === 'object' &&
72+
typeof arg4 === 'function'
73+
) {
74+
options = arg2;
75+
activeContext = arg3;
76+
fn = arg4;
77+
}
78+
79+
const parentContext = activeContext ?? context.active();
80+
const span = this.startSpan(name, options, parentContext);
81+
const contextWithSpanSet = setSpan(parentContext, span);
82+
83+
if (fn) {
84+
return context.with(contextWithSpanSet, fn, undefined, span);
85+
}
86+
return;
87+
}
4888
}
4989

5090
function isSpanContext(spanContext: any): spanContext is SpanContext {

src/trace/ProxyTracer.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ export class ProxyTracer implements Tracer {
3838
return this._getTracer().startSpan(name, options, context);
3939
}
4040

41+
startActiveSpan<F extends (span: Span) => unknown>(
42+
_name: string,
43+
_options: F | SpanOptions,
44+
_context?: F | Context,
45+
_fn?: F
46+
): ReturnType<F> {
47+
const tracer = this._getTracer();
48+
return Reflect.apply(tracer.startActiveSpan, tracer, arguments);
49+
}
50+
4151
/**
4252
* Try to get a tracer from the proxy tracer provider.
4353
* If the proxy tracer provider has no delegate, return a noop tracer.

src/trace/tracer.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,63 @@ export interface Tracer {
3737
* span.end();
3838
*/
3939
startSpan(name: string, options?: SpanOptions, context?: Context): Span;
40+
41+
/**
42+
* Starts a new {@link Span} and calls the given function passing it the
43+
* created span as first argument.
44+
* Additionally the new span gets set in context and this context is activated
45+
* for the duration of the function call.
46+
*
47+
* @param name The name of the span
48+
* @param [options] SpanOptions used for span creation
49+
* @param [context] Context to use to extract parent
50+
* @param fn function called in the context of the span and receives the newly created span as an argument
51+
* @returns return value of fn
52+
* @example
53+
* const something = tracer.startActiveSpan('op', span => {
54+
* try {
55+
* do some work
56+
* span.setStatus({code: SpanStatusCode.OK});
57+
* return something;
58+
* } catch (err) {
59+
* span.setStatus({
60+
* code: SpanStatusCode.ERROR,
61+
* message: err.message,
62+
* });
63+
* throw err;
64+
* } finally {
65+
* span.end();
66+
* }
67+
* });
68+
* @example
69+
* const span = tracer.startActiveSpan('op', span => {
70+
* try {
71+
* do some work
72+
* return span;
73+
* } catch (err) {
74+
* span.setStatus({
75+
* code: SpanStatusCode.ERROR,
76+
* message: err.message,
77+
* });
78+
* throw err;
79+
* }
80+
* });
81+
* do some more work
82+
* span.end();
83+
*/
84+
startActiveSpan<F extends (span: Span) => unknown>(
85+
name: string,
86+
fn: F
87+
): ReturnType<F>;
88+
startActiveSpan<F extends (span: Span) => unknown>(
89+
name: string,
90+
options: SpanOptions,
91+
fn: F
92+
): ReturnType<F>;
93+
startActiveSpan<F extends (span: Span) => unknown>(
94+
name: string,
95+
options: SpanOptions,
96+
context: Context,
97+
fn: F
98+
): ReturnType<F>;
4099
}

test/noop-implementations/noop-tracer.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
import * as assert from 'assert';
1818
import {
19+
context,
1920
NoopTracer,
21+
Span,
2022
SpanContext,
2123
SpanKind,
22-
TraceFlags,
23-
context,
2424
trace,
25+
TraceFlags,
2526
} from '../../src';
2627
import { NonRecordingSpan } from '../../src/trace/NonRecordingSpan';
2728

@@ -56,4 +57,28 @@ describe('NoopTracer', () => {
5657
assert(span.spanContext().spanId === parent.spanId);
5758
assert(span.spanContext().traceFlags === parent.traceFlags);
5859
});
60+
61+
it('should accept 2 to 4 args and start an active span', () => {
62+
const tracer = new NoopTracer();
63+
const name = 'span-name';
64+
const fn = (span: Span) => {
65+
try {
66+
return 1;
67+
} finally {
68+
span.end();
69+
}
70+
};
71+
const opts = { attributes: { foo: 'bar' } };
72+
const ctx = context.active();
73+
74+
const a = tracer.startActiveSpan(name, fn);
75+
assert.strictEqual(a, 1);
76+
77+
const b = tracer.startActiveSpan(name, opts, fn);
78+
79+
assert.strictEqual(b, 1);
80+
81+
const c = tracer.startActiveSpan(name, opts, ctx, fn);
82+
assert.strictEqual(c, 1);
83+
});
5984
});

test/proxy-implementations/proxy-tracer.test.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@
1717
import * as assert from 'assert';
1818
import * as sinon from 'sinon';
1919
import {
20-
ProxyTracerProvider,
21-
SpanKind,
22-
TracerProvider,
23-
ProxyTracer,
24-
Tracer,
25-
Span,
20+
context,
2621
NoopTracer,
22+
ProxyTracer,
23+
ProxyTracerProvider,
2724
ROOT_CONTEXT,
25+
Span,
26+
SpanKind,
2827
SpanOptions,
28+
Tracer,
29+
TracerProvider,
2930
} from '../../src';
3031
import { NonRecordingSpan } from '../../src/trace/NonRecordingSpan';
31-
3232
describe('ProxyTracer', () => {
3333
let provider: ProxyTracerProvider;
3434
const sandbox = sinon.createSandbox();
@@ -96,6 +96,10 @@ describe('ProxyTracer', () => {
9696
startSpan() {
9797
return delegateSpan;
9898
},
99+
100+
startActiveSpan() {
101+
// stubbed
102+
},
99103
};
100104

101105
tracer = provider.getTracer('test');
@@ -114,6 +118,34 @@ describe('ProxyTracer', () => {
114118
assert.strictEqual(span, delegateSpan);
115119
});
116120

121+
it('should create active spans using the delegate tracer', () => {
122+
// sinon types are broken with overloads, hence the any
123+
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36436
124+
const startActiveSpanStub = sinon.stub<Tracer, any>(
125+
delegateTracer,
126+
'startActiveSpan'
127+
);
128+
129+
const name = 'span-name';
130+
const fn = (span: Span) => {
131+
try {
132+
return 1;
133+
} finally {
134+
span.end();
135+
}
136+
};
137+
const opts = { attributes: { foo: 'bar' } };
138+
const ctx = context.active();
139+
140+
startActiveSpanStub.withArgs(name, fn).returns(1);
141+
startActiveSpanStub.withArgs(name, opts, fn).returns(2);
142+
startActiveSpanStub.withArgs(name, opts, ctx, fn).returns(3);
143+
144+
assert.strictEqual(tracer.startActiveSpan(name, fn), 1);
145+
assert.strictEqual(tracer.startActiveSpan(name, opts, fn), 2);
146+
assert.strictEqual(tracer.startActiveSpan(name, opts, ctx, fn), 3);
147+
});
148+
117149
it('should pass original arguments to DelegateTracer#startSpan', () => {
118150
const startSpanStub = sandbox.stub(delegateTracer, 'startSpan');
119151

@@ -130,6 +162,7 @@ describe('ProxyTracer', () => {
130162
assert.deepStrictEqual(Object.getOwnPropertyNames(NoopTracer.prototype), [
131163
'constructor',
132164
'startSpan',
165+
'startActiveSpan',
133166
]);
134167
sandbox.assert.calledOnceWithExactly(startSpanStub, name, options, ctx);
135168
});

0 commit comments

Comments
 (0)