Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(otel): Parse better op/description from otel span where possible #6084

Merged
merged 1 commit into from Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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