Skip to content

Commit f33301f

Browse files
authored
feat(otel): Parse better op/description from otel span where possible (#6084)
Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306, we now try to parse a better `op`/`description` from the otel span data, and update the span accordingly when it ends. Note that we do not do anything for transactions for now.
1 parent 652eaac commit f33301f

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Span as SentrySpan, TransactionContext } from '@sentry/types';
66
import { logger } from '@sentry/utils';
77

88
import { mapOtelStatus } from './utils/map-otel-status';
9+
import { parseSpanDescription } from './utils/parse-otel-span-description';
910

1011
/**
1112
* Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via
@@ -136,6 +137,10 @@ function updateSpanWithOtelData(sentrySpan: SentrySpan, otelSpan: OtelSpan): voi
136137
const value = attributes[prop];
137138
sentrySpan.setData(prop, value);
138139
});
140+
141+
const { op, description } = parseSpanDescription(otelSpan);
142+
sentrySpan.op = op;
143+
sentrySpan.description = description;
139144
}
140145

141146
function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelSpan): void {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { AttributeValue, SpanKind } from '@opentelemetry/api';
2+
import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
3+
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
4+
5+
interface SpanDescription {
6+
op: string | undefined;
7+
description: string;
8+
}
9+
10+
/**
11+
* Extract better op/description from an otel span.
12+
*
13+
* Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306
14+
*
15+
* @param otelSpan
16+
* @returns Better op/description to use, or undefined
17+
*/
18+
export function parseSpanDescription(otelSpan: OtelSpan): SpanDescription {
19+
const { attributes, name } = otelSpan;
20+
21+
// if http.method exists, this is an http request span
22+
const httpMethod = attributes[SemanticAttributes.HTTP_METHOD];
23+
if (httpMethod) {
24+
return descriptionForHttpMethod(otelSpan, httpMethod);
25+
}
26+
27+
// If db.type exists then this is a database call span.
28+
const dbSystem = attributes[SemanticAttributes.DB_SYSTEM];
29+
if (dbSystem) {
30+
return descriptionForDbSystem(otelSpan, dbSystem);
31+
}
32+
33+
// If rpc.service exists then this is a rpc call span.
34+
const rpcService = attributes[SemanticAttributes.RPC_SERVICE];
35+
if (rpcService) {
36+
return {
37+
op: 'rpc',
38+
description: name,
39+
};
40+
}
41+
42+
// If messaging.system exists then this is a messaging system span.
43+
const messagingSystem = attributes[SemanticAttributes.MESSAGING_SYSTEM];
44+
if (messagingSystem) {
45+
return {
46+
op: 'message',
47+
description: name,
48+
};
49+
}
50+
51+
// If faas.trigger exists then this is a function as a service span.
52+
const faasTrigger = attributes[SemanticAttributes.FAAS_TRIGGER];
53+
if (faasTrigger) {
54+
return { op: faasTrigger.toString(), description: name };
55+
}
56+
57+
return { op: undefined, description: name };
58+
}
59+
60+
function descriptionForDbSystem(otelSpan: OtelSpan, _dbSystem: AttributeValue): SpanDescription {
61+
const { attributes, name } = otelSpan;
62+
63+
// Use DB statement (Ex "SELECT * FROM table") if possible as description.
64+
const statement = attributes[SemanticAttributes.DB_STATEMENT];
65+
66+
const description = statement ? statement.toString() : name;
67+
68+
return { op: 'db', description };
69+
}
70+
71+
function descriptionForHttpMethod(otelSpan: OtelSpan, httpMethod: AttributeValue): SpanDescription {
72+
const { name, kind } = otelSpan;
73+
74+
const opParts = ['http'];
75+
76+
switch (kind) {
77+
case SpanKind.CLIENT:
78+
opParts.push('client');
79+
break;
80+
case SpanKind.SERVER:
81+
opParts.push('server');
82+
break;
83+
}
84+
85+
// Ex. description="GET /api/users/{user_id}".
86+
const description = `${httpMethod} ${name}`;
87+
88+
return { op: opParts.join('.'), description };
89+
}

packages/opentelemetry-node/test/spanprocessor.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as OpenTelemetry from '@opentelemetry/api';
2+
import { SpanKind } from '@opentelemetry/api';
23
import { Resource } from '@opentelemetry/resources';
34
import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
45
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
@@ -328,6 +329,162 @@ describe('SentrySpanProcessor', () => {
328329
expect(transaction?.status).toBe(expected);
329330
},
330331
);
332+
333+
it('updates op/description for span on end', async () => {
334+
const tracer = provider.getTracer('default');
335+
336+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
337+
tracer.startActiveSpan('SELECT * FROM users;', child => {
338+
const sentrySpan = getSpanForOtelSpan(child);
339+
340+
child.updateName('new name');
341+
342+
expect(sentrySpan?.op).toBe(undefined);
343+
expect(sentrySpan?.description).toBe('SELECT * FROM users;');
344+
345+
child.end();
346+
347+
expect(sentrySpan?.op).toBe(undefined);
348+
expect(sentrySpan?.description).toBe('new name');
349+
350+
parentOtelSpan.end();
351+
});
352+
});
353+
});
354+
355+
it('updates op/description based on attributes for HTTP_METHOD for client', async () => {
356+
const tracer = provider.getTracer('default');
357+
358+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
359+
tracer.startActiveSpan('/users/all', { kind: SpanKind.CLIENT }, child => {
360+
const sentrySpan = getSpanForOtelSpan(child);
361+
362+
child.setAttribute(SemanticAttributes.HTTP_METHOD, 'GET');
363+
364+
child.end();
365+
366+
expect(sentrySpan?.op).toBe('http.client');
367+
expect(sentrySpan?.description).toBe('GET /users/all');
368+
369+
parentOtelSpan.end();
370+
});
371+
});
372+
});
373+
374+
it('updates op/description based on attributes for HTTP_METHOD for server', async () => {
375+
const tracer = provider.getTracer('default');
376+
377+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
378+
tracer.startActiveSpan('/users/all', { kind: SpanKind.SERVER }, child => {
379+
const sentrySpan = getSpanForOtelSpan(child);
380+
381+
child.setAttribute(SemanticAttributes.HTTP_METHOD, 'GET');
382+
383+
child.end();
384+
385+
expect(sentrySpan?.op).toBe('http.server');
386+
expect(sentrySpan?.description).toBe('GET /users/all');
387+
388+
parentOtelSpan.end();
389+
});
390+
});
391+
});
392+
393+
it('updates op/description based on attributes for DB_SYSTEM', async () => {
394+
const tracer = provider.getTracer('default');
395+
396+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
397+
tracer.startActiveSpan('fetch users from DB', child => {
398+
const sentrySpan = getSpanForOtelSpan(child);
399+
400+
child.setAttribute(SemanticAttributes.DB_SYSTEM, 'MySQL');
401+
child.setAttribute(SemanticAttributes.DB_STATEMENT, 'SELECT * FROM users');
402+
403+
child.end();
404+
405+
expect(sentrySpan?.op).toBe('db');
406+
expect(sentrySpan?.description).toBe('SELECT * FROM users');
407+
408+
parentOtelSpan.end();
409+
});
410+
});
411+
});
412+
413+
it('updates op/description based on attributes for DB_SYSTEM without DB_STATEMENT', async () => {
414+
const tracer = provider.getTracer('default');
415+
416+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
417+
tracer.startActiveSpan('fetch users from DB', child => {
418+
const sentrySpan = getSpanForOtelSpan(child);
419+
420+
child.setAttribute(SemanticAttributes.DB_SYSTEM, 'MySQL');
421+
422+
child.end();
423+
424+
expect(sentrySpan?.op).toBe('db');
425+
expect(sentrySpan?.description).toBe('fetch users from DB');
426+
427+
parentOtelSpan.end();
428+
});
429+
});
430+
});
431+
432+
it('updates op/description based on attributes for RPC_SERVICE', async () => {
433+
const tracer = provider.getTracer('default');
434+
435+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
436+
tracer.startActiveSpan('test operation', child => {
437+
const sentrySpan = getSpanForOtelSpan(child);
438+
439+
child.setAttribute(SemanticAttributes.RPC_SERVICE, 'rpc service');
440+
441+
child.end();
442+
443+
expect(sentrySpan?.op).toBe('rpc');
444+
expect(sentrySpan?.description).toBe('test operation');
445+
446+
parentOtelSpan.end();
447+
});
448+
});
449+
});
450+
451+
it('updates op/description based on attributes for MESSAGING_SYSTEM', async () => {
452+
const tracer = provider.getTracer('default');
453+
454+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
455+
tracer.startActiveSpan('test operation', child => {
456+
const sentrySpan = getSpanForOtelSpan(child);
457+
458+
child.setAttribute(SemanticAttributes.MESSAGING_SYSTEM, 'messaging system');
459+
460+
child.end();
461+
462+
expect(sentrySpan?.op).toBe('message');
463+
expect(sentrySpan?.description).toBe('test operation');
464+
465+
parentOtelSpan.end();
466+
});
467+
});
468+
});
469+
470+
it('updates op/description based on attributes for FAAS_TRIGGER', async () => {
471+
const tracer = provider.getTracer('default');
472+
473+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
474+
tracer.startActiveSpan('test operation', child => {
475+
const sentrySpan = getSpanForOtelSpan(child);
476+
477+
child.setAttribute(SemanticAttributes.FAAS_TRIGGER, 'test faas trigger');
478+
479+
child.end();
480+
481+
expect(sentrySpan?.op).toBe('test faas trigger');
482+
expect(sentrySpan?.description).toBe('test operation');
483+
484+
parentOtelSpan.end();
485+
});
486+
});
487+
});
331488
});
332489

333490
// OTEL expects a custom date format

0 commit comments

Comments
 (0)