Skip to content

Commit

Permalink
feat(otel): Parse better op/description from otel span where possible (
Browse files Browse the repository at this point in the history
…#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.
  • Loading branch information
mydea committed Oct 28, 2022
1 parent 652eaac commit f33301f
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/opentelemetry-node/src/spanprocessor.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
@@ -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 };
}
157 changes: 157 additions & 0 deletions 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';
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f33301f

Please sign in to comment.