Skip to content

Commit a60391a

Browse files
authored
feat(opentelemetry): Align span options with core span options (#10761)
This aligns the options for `startSpan` in otel with the core ones. Only the `kind` is there in addition for now (we may want to remove this, let's see). Especially, this also adds the ability to pass a `scope` there and pick up the correct context & span to continue, as well as adding tests for the options.
1 parent 2ea788c commit a60391a

File tree

6 files changed

+204
-54
lines changed

6 files changed

+204
-54
lines changed

packages/core/test/lib/tracing/trace.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ describe('startSpan', () => {
291291
expect(spanToJSON(_span!).timestamp).toBeDefined();
292292
});
293293

294-
it('allows to pass a `startTime` yyy', () => {
294+
it('allows to pass a `startTime`', () => {
295295
const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => {
296296
return spanToJSON(span!).start_timestamp;
297297
});

packages/opentelemetry/src/contextManager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
SENTRY_FORK_SET_SCOPE_CONTEXT_KEY,
99
} from './constants';
1010
import { getCurrentHub } from './custom/getCurrentHub';
11-
import { getScopesFromContext, setHubOnContext, setScopesOnContext } from './utils/contextData';
11+
import { getScopesFromContext, setContextOnScope, setHubOnContext, setScopesOnContext } from './utils/contextData';
1212

1313
/**
1414
* Wrap an OpenTelemetry ContextManager in a way that ensures the context is kept in sync with the Sentry Hub.
@@ -70,6 +70,8 @@ export function wrapContextManagerClass<ContextManagerInstance extends ContextMa
7070
.deleteValue(SENTRY_FORK_SET_SCOPE_CONTEXT_KEY)
7171
.deleteValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY);
7272

73+
setContextOnScope(newCurrentScope, ctx3);
74+
7375
return super.with(ctx3, fn, thisArg, ...args);
7476
}
7577
}

packages/opentelemetry/src/trace.ts

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Span, Tracer } from '@opentelemetry/api';
1+
import type { Context, Span, SpanOptions, Tracer } from '@opentelemetry/api';
22
import { context } from '@opentelemetry/api';
33
import { SpanStatusCode, trace } from '@opentelemetry/api';
44
import { suppressTracing } from '@opentelemetry/core';
@@ -7,6 +7,7 @@ import type { Client, Scope } from '@sentry/types';
77

88
import { InternalSentrySemanticAttributes } from './semanticAttributes';
99
import type { OpenTelemetryClient, OpenTelemetrySpanContext } from './types';
10+
import { getContextFromScope } from './utils/contextData';
1011
import { setSpanMetadata } from './utils/spanData';
1112

1213
/**
@@ -18,17 +19,19 @@ import { setSpanMetadata } from './utils/spanData';
1819
*
1920
* Note that you'll always get a span passed to the callback, it may just be a NonRecordingSpan if the span is not sampled.
2021
*/
21-
export function startSpan<T>(spanContext: OpenTelemetrySpanContext, callback: (span: Span) => T): T {
22+
export function startSpan<T>(options: OpenTelemetrySpanContext, callback: (span: Span) => T): T {
2223
const tracer = getTracer();
2324

24-
const { name } = spanContext;
25+
const { name } = options;
2526

26-
const activeCtx = context.active();
27-
const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx);
27+
const activeCtx = getContext(options.scope);
28+
const shouldSkipSpan = options.onlyIfParent && !trace.getSpan(activeCtx);
2829
const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
2930

31+
const spanContext = getSpanContext(options);
32+
3033
return tracer.startActiveSpan(name, spanContext, ctx, span => {
31-
_applySentryAttributesToSpan(span, spanContext);
34+
_applySentryAttributesToSpan(span, options);
3235

3336
return handleCallbackErrors(
3437
() => callback(span),
@@ -49,17 +52,19 @@ export function startSpan<T>(spanContext: OpenTelemetrySpanContext, callback: (s
4952
*
5053
* Note that you'll always get a span passed to the callback, it may just be a NonRecordingSpan if the span is not sampled.
5154
*/
52-
export function startSpanManual<T>(spanContext: OpenTelemetrySpanContext, callback: (span: Span) => T): T {
55+
export function startSpanManual<T>(options: OpenTelemetrySpanContext, callback: (span: Span) => T): T {
5356
const tracer = getTracer();
5457

55-
const { name } = spanContext;
58+
const { name } = options;
5659

57-
const activeCtx = context.active();
58-
const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx);
60+
const activeCtx = getContext(options.scope);
61+
const shouldSkipSpan = options.onlyIfParent && !trace.getSpan(activeCtx);
5962
const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
6063

64+
const spanContext = getSpanContext(options);
65+
6166
return tracer.startActiveSpan(name, spanContext, ctx, span => {
62-
_applySentryAttributesToSpan(span, spanContext);
67+
_applySentryAttributesToSpan(span, options);
6368

6469
return handleCallbackErrors(
6570
() => callback(span),
@@ -85,18 +90,20 @@ export const startActiveSpan = startSpan;
8590
* or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans
8691
* and the `span` returned from the callback will be undefined.
8792
*/
88-
export function startInactiveSpan(spanContext: OpenTelemetrySpanContext): Span {
93+
export function startInactiveSpan(options: OpenTelemetrySpanContext): Span {
8994
const tracer = getTracer();
9095

91-
const { name } = spanContext;
96+
const { name } = options;
9297

93-
const activeCtx = context.active();
94-
const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx);
98+
const activeCtx = getContext(options.scope);
99+
const shouldSkipSpan = options.onlyIfParent && !trace.getSpan(activeCtx);
95100
const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
96101

102+
const spanContext = getSpanContext(options);
103+
97104
const span = tracer.startSpan(name, spanContext, ctx);
98105

99-
_applySentryAttributesToSpan(span, spanContext);
106+
_applySentryAttributesToSpan(span, options);
100107

101108
return span;
102109
}
@@ -120,8 +127,9 @@ function getTracer(): Tracer {
120127
return (client && client.tracer) || trace.getTracer('@sentry/opentelemetry', SDK_VERSION);
121128
}
122129

123-
function _applySentryAttributesToSpan(span: Span, spanContext: OpenTelemetrySpanContext): void {
124-
const { origin, op, source, metadata } = spanContext;
130+
function _applySentryAttributesToSpan(span: Span, options: OpenTelemetrySpanContext): void {
131+
// eslint-disable-next-line deprecation/deprecation
132+
const { origin, op, source, metadata } = options;
125133

126134
if (origin) {
127135
span.setAttribute(InternalSentrySemanticAttributes.ORIGIN, origin);
@@ -139,3 +147,32 @@ function _applySentryAttributesToSpan(span: Span, spanContext: OpenTelemetrySpan
139147
setSpanMetadata(span, metadata);
140148
}
141149
}
150+
151+
function getSpanContext(options: OpenTelemetrySpanContext): SpanOptions {
152+
const { startTime, attributes, kind } = options;
153+
154+
// OTEL expects timestamps in ms, not seconds
155+
const fixedStartTime = typeof startTime === 'number' ? ensureTimestampInMilliseconds(startTime) : startTime;
156+
157+
return {
158+
attributes,
159+
kind,
160+
startTime: fixedStartTime,
161+
};
162+
}
163+
164+
function ensureTimestampInMilliseconds(timestamp: number): number {
165+
const isMs = timestamp < 9999999999;
166+
return isMs ? timestamp * 1000 : timestamp;
167+
}
168+
169+
function getContext(scope?: Scope): Context {
170+
if (scope) {
171+
const ctx = getContextFromScope(scope);
172+
if (ctx) {
173+
return ctx;
174+
}
175+
}
176+
177+
return context.active();
178+
}

packages/opentelemetry/src/types.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
import type { Attributes, Span as WriteableSpan, SpanKind, TimeInput, Tracer } from '@opentelemetry/api';
1+
import type { Span as WriteableSpan, SpanKind, Tracer } from '@opentelemetry/api';
22
import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base';
3-
import type { Scope, SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types';
3+
import type { Scope, StartSpanOptions } from '@sentry/types';
44

55
export interface OpenTelemetryClient {
66
tracer: Tracer;
77
traceProvider: BasicTracerProvider | undefined;
88
}
99

10-
export interface OpenTelemetrySpanContext {
11-
name: string;
12-
op?: string;
13-
metadata?: Partial<TransactionMetadata>;
14-
origin?: SpanOrigin;
15-
source?: TransactionSource;
16-
scope?: Scope;
17-
onlyIfParent?: boolean;
18-
19-
// Base SpanOptions we support
20-
attributes?: Attributes;
10+
export interface OpenTelemetrySpanContext extends StartSpanOptions {
11+
// Additional otel-only option, for now...?
2112
kind?: SpanKind;
22-
startTime?: TimeInput;
2313
}
2414

2515
/**

packages/opentelemetry/src/utils/contextData.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,17 @@ export function getScopesFromContext(context: Context): CurrentScopes | undefine
5555
* This will return a forked context with the Propagation Context set.
5656
*/
5757
export function setScopesOnContext(context: Context, scopes: CurrentScopes): Context {
58-
// So we can look up the context from the scope later
59-
SCOPE_CONTEXT_MAP.set(scopes.scope, context);
60-
6158
return context.setValue(SENTRY_SCOPES_CONTEXT_KEY, scopes);
6259
}
6360

61+
/**
62+
* Set the context on the scope so we can later look it up.
63+
* We need this to get the context from the scope in the `trace` functions.
64+
*/
65+
export function setContextOnScope(scope: Scope, context: Context): void {
66+
SCOPE_CONTEXT_MAP.set(scope, context);
67+
}
68+
6469
/**
6570
* Get the context related to a scope.
6671
* TODO v8: Use this for the `trace` functions.

0 commit comments

Comments
 (0)