Skip to content

Commit 5ed079c

Browse files
committed
feat(otel): Implement basic SpanProcessor
1 parent d6bcb13 commit 5ed079c

File tree

5 files changed

+164
-73
lines changed

5 files changed

+164
-73
lines changed

packages/opentelemetry-node/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
},
2929
"devDependencies": {
3030
"@opentelemetry/api": "^1.2.0",
31-
"@opentelemetry/sdk-trace-base": "^1.7.0"
31+
"@opentelemetry/sdk-trace-base": "^1.7.0",
32+
"@opentelemetry/sdk-trace-node": "^1.7.0"
3233
},
3334
"scripts": {
3435
"build": "run-p build:rollup build:types",

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,85 +2,85 @@ import { Context } from '@opentelemetry/api';
22
import { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base';
33
import { getCurrentHub } from '@sentry/core';
44
import { Span as SentrySpan, TransactionContext } from '@sentry/types';
5+
import { logger } from '@sentry/utils';
56

67
/**
78
* Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via
89
* the Sentry SDK.
910
*/
1011
export class SentrySpanProcessor implements OtelSpanProcessor {
11-
private readonly _map: Record<SentrySpan['spanId'], [SentrySpan, SentrySpan | undefined]> = {};
12+
// public only for testing
13+
public readonly _map: Map<SentrySpan['spanId'], SentrySpan> = new Map<SentrySpan['spanId'], SentrySpan>();
1214

1315
/**
1416
* @inheritDoc
1517
*/
1618
public onStart(otelSpan: OtelSpan, _parentContext: Context): void {
1719
const hub = getCurrentHub();
1820
if (!hub) {
21+
__DEBUG_BUILD__ && logger.error('SentrySpanProcessor has triggered onStart before a hub has been setup.');
1922
return;
2023
}
2124
const scope = hub.getScope();
2225
if (!scope) {
26+
__DEBUG_BUILD__ && logger.error('SentrySpanProcessor has triggered onStart before a scope has been setup.');
2327
return;
2428
}
2529

30+
// TODO: handle sentry requests
2631
// if isSentryRequest(otelSpan) return;
2732

2833
const otelSpanId = otelSpan.spanContext().spanId;
34+
const otelParentSpanId = otelSpan.parentSpanId;
35+
36+
// Otel supports having multiple non-nested spans at the same time
37+
// so we cannot use hub.getSpan(), as we cannot rely on this being on the current span
38+
const sentryParentSpan = otelParentSpanId && this._map.get(otelParentSpanId);
2939

30-
const sentryParentSpan = scope.getSpan();
3140
if (sentryParentSpan) {
3241
const sentryChildSpan = sentryParentSpan.startChild({
3342
description: otelSpan.name,
3443
// instrumentor: 'otel',
3544
startTimestamp: otelSpan.startTime[0],
45+
spanId: otelSpanId,
3646
});
37-
sentryChildSpan.spanId = otelSpanId;
38-
console.log(sentryParentSpan, sentryChildSpan, otelSpan);
3947

40-
this._map[otelSpanId] = [sentryChildSpan, sentryParentSpan];
41-
scope.setSpan(sentryChildSpan);
48+
this._map.set(otelSpanId, sentryChildSpan);
4249
} else {
4350
const traceCtx = getTraceData(otelSpan);
4451
const transaction = hub.startTransaction({
4552
name: otelSpan.name,
4653
...traceCtx,
4754
// instrumentor: 'otel',
4855
startTimestamp: otelSpan.startTime[0],
56+
spanId: otelSpanId,
4957
});
50-
transaction.spanId = otelSpanId;
51-
52-
this._map[otelSpanId] = [transaction, undefined];
5358

54-
scope.setSpan(transaction);
59+
this._map.set(otelSpanId, transaction);
5560
}
5661
}
5762

5863
/**
5964
* @inheritDoc
6065
*/
6166
public onEnd(otelSpan: OtelSpan): void {
62-
const hub = getCurrentHub();
63-
if (!hub) {
64-
return;
65-
}
66-
const scope = hub.getScope();
67-
if (!scope) {
67+
const otelSpanId = otelSpan.spanContext().spanId;
68+
const mapVal = this._map.get(otelSpanId);
69+
70+
if (!mapVal) {
71+
__DEBUG_BUILD__ &&
72+
logger.error(`SentrySpanProcessor could not find span with OTEL-spanId ${otelSpanId} to finish.`);
6873
return;
6974
}
7075

71-
const otelSpanId = otelSpan.spanContext().spanId;
72-
const mapVal = this._map[otelSpanId];
76+
const sentrySpan = mapVal;
7377

74-
if (mapVal) {
75-
const [sentrySpan, sentryParentSpan] = mapVal;
78+
// TODO: actually add context etc. to span
79+
// updateSpanWithOtelData(sentrySpan, otelSpan);
7680

77-
// updateSpanWithOtelData(sentrySpan, otelSpan);
81+
sentrySpan.finish(otelSpan.endTime[0]);
7882

79-
sentrySpan.finish(otelSpan.endTime[0]);
80-
scope.setSpan(sentryParentSpan);
81-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
82-
delete this._map[otelSpanId];
83-
}
83+
this._map.delete(otelSpanId);
8484
}
8585

8686
/**
Lines changed: 103 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as OpenTelemetry from '@opentelemetry/api';
2-
import { BasicTracerProvider, Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
2+
import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
3+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
34
import { Hub, makeMain } from '@sentry/core';
45
import { addExtensionMethods, Span as SentrySpan, Transaction } from '@sentry/tracing';
56

@@ -13,68 +14,128 @@ beforeAll(() => {
1314

1415
describe('SentrySpanProcessor', () => {
1516
let hub: Hub;
17+
let provider: NodeTracerProvider;
18+
let spanProcessor: SentrySpanProcessor;
19+
1620
beforeEach(() => {
1721
hub = new Hub();
1822
makeMain(hub);
1923

20-
const provider = new BasicTracerProvider();
21-
provider.addSpanProcessor(new SentrySpanProcessor());
24+
spanProcessor = new SentrySpanProcessor();
25+
provider = new NodeTracerProvider();
26+
provider.addSpanProcessor(spanProcessor);
2227
provider.register();
2328
});
2429

25-
describe('onStart', () => {
26-
it('create a transaction', () => {
27-
const otelSpan = OpenTelemetry.trace.getTracer('default').startSpan('GET /users') as OtelSpan;
28-
const sentrySpanTransaction = hub.getScope()?.getSpan() as Transaction;
29-
expect(sentrySpanTransaction).toBeInstanceOf(Transaction);
30+
afterEach(async () => {
31+
await provider.forceFlush();
32+
await provider.shutdown();
33+
});
3034

31-
// Make sure name is set
32-
expect(sentrySpanTransaction?.name).toBe('GET /users');
35+
function getSpanForOtelSpan(otelSpan: OtelSpan | OpenTelemetry.Span) {
36+
return spanProcessor._map.get(otelSpan.spanContext().spanId) as SentrySpan | undefined;
37+
}
3338

34-
// Enforce we use otel timestamps
35-
expect(sentrySpanTransaction.startTimestamp).toEqual(otelSpan.startTime[0]);
39+
it('creates a transaction', async () => {
40+
const startTime = otelNumberToHrtime(new Date().valueOf());
3641

37-
// Check for otel trace context
38-
expect(sentrySpanTransaction.traceId).toEqual(otelSpan.spanContext().traceId);
39-
expect(sentrySpanTransaction.parentSpanId).toEqual(otelSpan.parentSpanId);
40-
expect(sentrySpanTransaction.spanId).toEqual(otelSpan.spanContext().spanId);
41-
});
42+
const otelSpan = provider.getTracer('default').startSpan('GET /users', { startTime }) as OtelSpan;
43+
44+
const sentrySpanTransaction = getSpanForOtelSpan(otelSpan) as Transaction | undefined;
45+
expect(sentrySpanTransaction).toBeInstanceOf(Transaction);
4246

43-
it.only('creates a child span if there is a running transaction', () => {
44-
const tracer = OpenTelemetry.trace.getTracer('default');
47+
expect(sentrySpanTransaction?.name).toBe('GET /users');
48+
expect(sentrySpanTransaction?.startTimestamp).toEqual(otelSpan.startTime[0]);
49+
expect(sentrySpanTransaction?.startTimestamp).toEqual(startTime[0]);
50+
expect(sentrySpanTransaction?.traceId).toEqual(otelSpan.spanContext().traceId);
51+
expect(sentrySpanTransaction?.parentSpanId).toEqual(otelSpan.parentSpanId);
52+
expect(sentrySpanTransaction?.spanId).toEqual(otelSpan.spanContext().spanId);
4553

46-
tracer.startActiveSpan('GET /users', parentOtelSpan => {
47-
// console.log((parentOtelSpan as any).spanContext());
48-
// console.log(hub.getScope()?.getSpan()?.traceId);
49-
tracer.startActiveSpan('SELECT * FROM users;', child => {
50-
const childOtelSpan = child as OtelSpan;
54+
expect(hub.getScope()?.getSpan()).toBeUndefined();
5155

52-
const sentrySpan = hub.getScope()?.getSpan();
53-
expect(sentrySpan).toBeInstanceOf(SentrySpan);
54-
// console.log(hub.getScope()?.getSpan()?.traceId);
55-
// console.log(sentrySpan);
56+
const endTime = otelNumberToHrtime(new Date().valueOf());
57+
otelSpan.end(endTime);
5658

57-
// Make sure name is set
58-
expect(sentrySpan?.description).toBe('SELECT * FROM users;');
59+
expect(sentrySpanTransaction?.endTimestamp).toBe(endTime[0]);
60+
expect(sentrySpanTransaction?.endTimestamp).toBe(otelSpan.endTime[0]);
5961

60-
// Enforce we use otel timestamps
61-
expect(sentrySpan?.startTimestamp).toEqual(childOtelSpan.startTime[0]);
62+
expect(hub.getScope()?.getSpan()).toBeUndefined();
63+
});
6264

63-
// Check for otel trace context
64-
expect(sentrySpan?.spanId).toEqual(childOtelSpan.spanContext().spanId);
65+
it('creates a child span if there is a running transaction', () => {
66+
const tracer = provider.getTracer('default');
6567

66-
childOtelSpan.end();
67-
});
68+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
69+
tracer.startActiveSpan('SELECT * FROM users;', child => {
70+
const childOtelSpan = child as OtelSpan;
6871

69-
parentOtelSpan.end();
72+
const sentrySpanTransaction = getSpanForOtelSpan(parentOtelSpan) as Transaction | undefined;
73+
expect(sentrySpanTransaction).toBeInstanceOf(Transaction);
74+
75+
const sentrySpan = getSpanForOtelSpan(childOtelSpan);
76+
expect(sentrySpan).toBeInstanceOf(SentrySpan);
77+
expect(sentrySpan?.description).toBe('SELECT * FROM users;');
78+
expect(sentrySpan?.startTimestamp).toEqual(childOtelSpan.startTime[0]);
79+
expect(sentrySpan?.spanId).toEqual(childOtelSpan.spanContext().spanId);
80+
expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanId);
81+
82+
expect(hub.getScope()?.getSpan()).toBeUndefined();
83+
84+
const endTime = otelNumberToHrtime(new Date().valueOf());
85+
child.end(endTime);
86+
87+
expect(sentrySpan?.endTimestamp).toEqual(childOtelSpan.endTime[0]);
88+
expect(sentrySpan?.endTimestamp).toEqual(endTime[0]);
7089
});
90+
91+
parentOtelSpan.end();
7192
});
7293
});
7394

74-
// it('Creates a transaction if there is no running ', () => {
75-
// const otelSpan = OpenTelemetry.trace.getTracer('default').startSpan('GET /users') as OtelSpan;
76-
// processor.onStart(otelSpan, OpenTelemetry.context.active());
95+
it('allows to create multiple child spans on same level', () => {
96+
const tracer = provider.getTracer('default');
7797

78-
// const sentrySpanTransaction = hub.getScope()?.getSpan() as Transaction;
79-
// });
98+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
99+
const sentrySpanTransaction = getSpanForOtelSpan(parentOtelSpan) as Transaction | undefined;
100+
101+
expect(sentrySpanTransaction).toBeInstanceOf(SentrySpan);
102+
expect(sentrySpanTransaction?.name).toBe('GET /users');
103+
104+
// Create some parallel, independent spans
105+
const span1 = tracer.startSpan('SELECT * FROM users;') as OtelSpan;
106+
const span2 = tracer.startSpan('SELECT * FROM companies;') as OtelSpan;
107+
const span3 = tracer.startSpan('SELECT * FROM locations;') as OtelSpan;
108+
109+
const sentrySpan1 = getSpanForOtelSpan(span1);
110+
const sentrySpan2 = getSpanForOtelSpan(span2);
111+
const sentrySpan3 = getSpanForOtelSpan(span3);
112+
113+
expect(sentrySpan1?.parentSpanId).toEqual(sentrySpanTransaction?.spanId);
114+
expect(sentrySpan2?.parentSpanId).toEqual(sentrySpanTransaction?.spanId);
115+
expect(sentrySpan3?.parentSpanId).toEqual(sentrySpanTransaction?.spanId);
116+
117+
expect(sentrySpan1?.description).toEqual('SELECT * FROM users;');
118+
expect(sentrySpan2?.description).toEqual('SELECT * FROM companies;');
119+
expect(sentrySpan3?.description).toEqual('SELECT * FROM locations;');
120+
121+
span1.end();
122+
span2.end();
123+
span3.end();
124+
125+
parentOtelSpan.end();
126+
});
127+
});
80128
});
129+
130+
// OTEL expects a custom date format
131+
const NANOSECOND_DIGITS = 9;
132+
const SECOND_TO_NANOSECONDS = Math.pow(10, NANOSECOND_DIGITS);
133+
134+
function otelNumberToHrtime(epochMillis: number): OpenTelemetry.HrTime {
135+
const epochSeconds = epochMillis / 1000;
136+
// Decimals only.
137+
const seconds = Math.trunc(epochSeconds);
138+
// Round sub-nanosecond accuracy to nanosecond.
139+
const nanos = Number((epochSeconds - seconds).toFixed(NANOSECOND_DIGITS)) * SECOND_TO_NANOSECONDS;
140+
return [seconds, nanos];
141+
}

packages/types/src/span.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,7 @@ export interface Span extends SpanContext {
132132
* Creates a new `Span` while setting the current `Span.id` as `parentSpanId`.
133133
* Also the `sampled` decision will be inherited.
134134
*/
135-
startChild(
136-
spanContext?: Pick<SpanContext, Exclude<keyof SpanContext, 'spanId' | 'sampled' | 'traceId' | 'parentSpanId'>>,
137-
): Span;
135+
startChild(spanContext?: Pick<SpanContext, Exclude<keyof SpanContext, 'sampled' | 'traceId' | 'parentSpanId'>>): Span;
138136

139137
/**
140138
* Determines whether span was successful (HTTP200)

yarn.lock

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3898,6 +3898,11 @@
38983898
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.2.0.tgz#89ef99401cde6208cff98760b67663726ef26686"
38993899
integrity sha512-0nBr+VZNKm9tvNDZFstI3Pq1fCTEDK5OZTnVKNvBNAKgd0yIvmwsP4m61rEv7ZP+tOUjWJhROpxK5MsnlF911g==
39003900

3901+
"@opentelemetry/context-async-hooks@1.7.0":
3902+
version "1.7.0"
3903+
resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.7.0.tgz#b78d1f4f30b484d92d7926dc9d29ec1ccd489cf5"
3904+
integrity sha512-g4bMzyVW5dVBeMkyadaf3NRFpmNrdD4Pp9OJsrP29HwIam/zVMNfIWQpT5IBzjtTSMhl/ED5YQYR+UOSjVq3sQ==
3905+
39013906
"@opentelemetry/context-base@^0.12.0":
39023907
version "0.12.0"
39033908
resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.12.0.tgz#4906ae27359d3311e3dea1b63770a16f60848550"
@@ -3924,6 +3929,20 @@
39243929
"@opentelemetry/context-base" "^0.12.0"
39253930
semver "^7.1.3"
39263931

3932+
"@opentelemetry/propagator-b3@1.7.0":
3933+
version "1.7.0"
3934+
resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.7.0.tgz#8c089c2bab733ea7122cb4a5f7ffaaa355127555"
3935+
integrity sha512-8kKGS1KwArvkThdhubMZlomuREE9FaBcn9L4JrYHh2jly1FZpqOtFNO2byHymVRjH59d43Pa+eJuFpD0Fp7kSw==
3936+
dependencies:
3937+
"@opentelemetry/core" "1.7.0"
3938+
3939+
"@opentelemetry/propagator-jaeger@1.7.0":
3940+
version "1.7.0"
3941+
resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.7.0.tgz#1c1439866e05ba81da303ad28286aa25d129bf03"
3942+
integrity sha512-V7i/L1bx+R/ve4z6dTdn2jtvFxGThRsXS2wNb/tWZVfV8gqnePQp+HfoLrqB/Yz2iRPUcMWrcjx6vV78umvJFA==
3943+
dependencies:
3944+
"@opentelemetry/core" "1.7.0"
3945+
39273946
"@opentelemetry/resources@1.7.0":
39283947
version "1.7.0"
39293948
resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.7.0.tgz#90ccd3a6a86b4dfba4e833e73944bd64958d78c5"
@@ -3940,7 +3959,7 @@
39403959
"@opentelemetry/api" "^0.12.0"
39413960
"@opentelemetry/core" "^0.12.0"
39423961

3943-
"@opentelemetry/sdk-trace-base@^1.7.0":
3962+
"@opentelemetry/sdk-trace-base@1.7.0", "@opentelemetry/sdk-trace-base@^1.7.0":
39443963
version "1.7.0"
39453964
resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.7.0.tgz#b498424e0c6340a9d80de63fd408c5c2130a60a5"
39463965
integrity sha512-Iz84C+FVOskmauh9FNnj4+VrA+hG5o+tkMzXuoesvSfunVSioXib0syVFeNXwOm4+M5GdWCuW632LVjqEXStIg==
@@ -3949,6 +3968,18 @@
39493968
"@opentelemetry/resources" "1.7.0"
39503969
"@opentelemetry/semantic-conventions" "1.7.0"
39513970

3971+
"@opentelemetry/sdk-trace-node@^1.7.0":
3972+
version "1.7.0"
3973+
resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.7.0.tgz#83bf458c33db930144cebed72b524034135fce7b"
3974+
integrity sha512-DCAAbi0Zbb1pIofQcKzoAVy9/6bz24asFYeLb4fW/8QYAaawDnxumA++5Huw/RcYdJs8q8AIRBykwjYWWCm/5A==
3975+
dependencies:
3976+
"@opentelemetry/context-async-hooks" "1.7.0"
3977+
"@opentelemetry/core" "1.7.0"
3978+
"@opentelemetry/propagator-b3" "1.7.0"
3979+
"@opentelemetry/propagator-jaeger" "1.7.0"
3980+
"@opentelemetry/sdk-trace-base" "1.7.0"
3981+
semver "^7.3.5"
3982+
39523983
"@opentelemetry/semantic-conventions@1.7.0":
39533984
version "1.7.0"
39543985
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.7.0.tgz#af80a1ef7cf110ea3a68242acd95648991bcd763"

0 commit comments

Comments
 (0)