diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 3891a326359a..099d86a9e3e1 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -6,6 +6,7 @@ import { Span as SentrySpan, TransactionContext } from '@sentry/types'; import { logger } from '@sentry/utils'; import { mapOtelStatus } from './utils/map-otel-status'; +import { parseSpanDescription } from './utils/parse-otel-span-description'; /** * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via @@ -136,6 +137,10 @@ function updateSpanWithOtelData(sentrySpan: SentrySpan, otelSpan: OtelSpan): voi const value = attributes[prop]; sentrySpan.setData(prop, value); }); + + const { op, description } = parseSpanDescription(otelSpan); + sentrySpan.op = op; + sentrySpan.description = description; } function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelSpan): void { diff --git a/packages/opentelemetry-node/src/utils/parse-otel-span-description.ts b/packages/opentelemetry-node/src/utils/parse-otel-span-description.ts new file mode 100644 index 000000000000..022280cefea9 --- /dev/null +++ b/packages/opentelemetry-node/src/utils/parse-otel-span-description.ts @@ -0,0 +1,89 @@ +import { AttributeValue, SpanKind } from '@opentelemetry/api'; +import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +interface SpanDescription { + op: string | undefined; + description: string; +} + +/** + * Extract better op/description from an otel span. + * + * Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306 + * + * @param otelSpan + * @returns Better op/description to use, or undefined + */ +export function parseSpanDescription(otelSpan: OtelSpan): SpanDescription { + const { attributes, name } = otelSpan; + + // if http.method exists, this is an http request span + const httpMethod = attributes[SemanticAttributes.HTTP_METHOD]; + if (httpMethod) { + return descriptionForHttpMethod(otelSpan, httpMethod); + } + + // If db.type exists then this is a database call span. + const dbSystem = attributes[SemanticAttributes.DB_SYSTEM]; + if (dbSystem) { + return descriptionForDbSystem(otelSpan, dbSystem); + } + + // If rpc.service exists then this is a rpc call span. + const rpcService = attributes[SemanticAttributes.RPC_SERVICE]; + if (rpcService) { + return { + op: 'rpc', + description: name, + }; + } + + // If messaging.system exists then this is a messaging system span. + const messagingSystem = attributes[SemanticAttributes.MESSAGING_SYSTEM]; + if (messagingSystem) { + return { + op: 'message', + description: name, + }; + } + + // If faas.trigger exists then this is a function as a service span. + const faasTrigger = attributes[SemanticAttributes.FAAS_TRIGGER]; + if (faasTrigger) { + return { op: faasTrigger.toString(), description: name }; + } + + return { op: undefined, description: name }; +} + +function descriptionForDbSystem(otelSpan: OtelSpan, _dbSystem: AttributeValue): SpanDescription { + const { attributes, name } = otelSpan; + + // Use DB statement (Ex "SELECT * FROM table") if possible as description. + const statement = attributes[SemanticAttributes.DB_STATEMENT]; + + const description = statement ? statement.toString() : name; + + return { op: 'db', description }; +} + +function descriptionForHttpMethod(otelSpan: OtelSpan, httpMethod: AttributeValue): SpanDescription { + const { name, kind } = otelSpan; + + const opParts = ['http']; + + switch (kind) { + case SpanKind.CLIENT: + opParts.push('client'); + break; + case SpanKind.SERVER: + opParts.push('server'); + break; + } + + // Ex. description="GET /api/users/{user_id}". + const description = `${httpMethod} ${name}`; + + return { op: opParts.join('.'), description }; +} diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index e371dc453617..a86645cbe462 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -1,4 +1,5 @@ import * as OpenTelemetry from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; @@ -328,6 +329,162 @@ describe('SentrySpanProcessor', () => { expect(transaction?.status).toBe(expected); }, ); + + it('updates op/description for span on end', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('SELECT * FROM users;', child => { + const sentrySpan = getSpanForOtelSpan(child); + + child.updateName('new name'); + + expect(sentrySpan?.op).toBe(undefined); + expect(sentrySpan?.description).toBe('SELECT * FROM users;'); + + child.end(); + + expect(sentrySpan?.op).toBe(undefined); + expect(sentrySpan?.description).toBe('new name'); + + parentOtelSpan.end(); + }); + }); + }); + + it('updates op/description based on attributes for HTTP_METHOD for client', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('/users/all', { kind: SpanKind.CLIENT }, child => { + const sentrySpan = getSpanForOtelSpan(child); + + child.setAttribute(SemanticAttributes.HTTP_METHOD, 'GET'); + + child.end(); + + expect(sentrySpan?.op).toBe('http.client'); + expect(sentrySpan?.description).toBe('GET /users/all'); + + parentOtelSpan.end(); + }); + }); + }); + + it('updates op/description based on attributes for HTTP_METHOD for server', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('/users/all', { kind: SpanKind.SERVER }, child => { + const sentrySpan = getSpanForOtelSpan(child); + + child.setAttribute(SemanticAttributes.HTTP_METHOD, 'GET'); + + child.end(); + + expect(sentrySpan?.op).toBe('http.server'); + expect(sentrySpan?.description).toBe('GET /users/all'); + + parentOtelSpan.end(); + }); + }); + }); + + it('updates op/description based on attributes for DB_SYSTEM', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('fetch users from DB', child => { + const sentrySpan = getSpanForOtelSpan(child); + + child.setAttribute(SemanticAttributes.DB_SYSTEM, 'MySQL'); + child.setAttribute(SemanticAttributes.DB_STATEMENT, 'SELECT * FROM users'); + + child.end(); + + expect(sentrySpan?.op).toBe('db'); + expect(sentrySpan?.description).toBe('SELECT * FROM users'); + + parentOtelSpan.end(); + }); + }); + }); + + it('updates op/description based on attributes for DB_SYSTEM without DB_STATEMENT', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('fetch users from DB', child => { + const sentrySpan = getSpanForOtelSpan(child); + + child.setAttribute(SemanticAttributes.DB_SYSTEM, 'MySQL'); + + child.end(); + + expect(sentrySpan?.op).toBe('db'); + expect(sentrySpan?.description).toBe('fetch users from DB'); + + parentOtelSpan.end(); + }); + }); + }); + + it('updates op/description based on attributes for RPC_SERVICE', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('test operation', child => { + const sentrySpan = getSpanForOtelSpan(child); + + child.setAttribute(SemanticAttributes.RPC_SERVICE, 'rpc service'); + + child.end(); + + expect(sentrySpan?.op).toBe('rpc'); + expect(sentrySpan?.description).toBe('test operation'); + + parentOtelSpan.end(); + }); + }); + }); + + it('updates op/description based on attributes for MESSAGING_SYSTEM', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('test operation', child => { + const sentrySpan = getSpanForOtelSpan(child); + + child.setAttribute(SemanticAttributes.MESSAGING_SYSTEM, 'messaging system'); + + child.end(); + + expect(sentrySpan?.op).toBe('message'); + expect(sentrySpan?.description).toBe('test operation'); + + parentOtelSpan.end(); + }); + }); + }); + + it('updates op/description based on attributes for FAAS_TRIGGER', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + tracer.startActiveSpan('test operation', child => { + const sentrySpan = getSpanForOtelSpan(child); + + child.setAttribute(SemanticAttributes.FAAS_TRIGGER, 'test faas trigger'); + + child.end(); + + expect(sentrySpan?.op).toBe('test faas trigger'); + expect(sentrySpan?.description).toBe('test operation'); + + parentOtelSpan.end(); + }); + }); + }); }); // OTEL expects a custom date format