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(instrumentation-mysql2): Add hook for setting span name #2555

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from '@opentelemetry/semantic-conventions';
import { addSqlCommenterComment } from '@opentelemetry/sql-common';
import type * as mysqlTypes from 'mysql2';
import { MySQL2InstrumentationConfig } from './types';
import { MySQL2InstrumentationConfig, MySQL2RequestInfo } from './types';
import {
getConnectionAttributes,
getDbStatement,
Expand Down Expand Up @@ -104,7 +104,30 @@ export class MySQL2Instrumentation extends InstrumentationBase<MySQL2Instrumenta
values = [_valuesOrCallback];
}

const span = thisPlugin.tracer.startSpan(getSpanName(query), {
const defaultSpanName = getSpanName(query);
const mysql2RequestInfo: MySQL2RequestInfo = {
query,
values,
database: this.config.database,
host: this.config.host,
};
const spanNameHook = thisPlugin.getConfig().spanNameHook;
let spanName = defaultSpanName;
if (spanNameHook) {
spanName =
safeExecuteInTheMiddle(
() => spanNameHook(mysql2RequestInfo, defaultSpanName),
(err, result) => {
if (err) {
thisPlugin._diag.warn('Failed executing spanNameHook', err);
}
return result;
},
true
) ?? defaultSpanName;
}

const span = thisPlugin.tracer.startSpan(spanName, {
kind: api.SpanKind.CLIENT,
attributes: {
...MySQL2Instrumentation.COMMON_ATTRIBUTES,
Expand Down
22 changes: 22 additions & 0 deletions plugins/node/opentelemetry-instrumentation-mysql2/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { InstrumentationConfig } from '@opentelemetry/instrumentation';
import type { Span } from '@opentelemetry/api';
import type { Query, QueryOptions } from 'mysql2';

export interface MySQL2ResponseHookInformation {
queryResults: any;
Expand All @@ -25,6 +26,22 @@ export interface MySQL2InstrumentationExecutionResponseHook {
(span: Span, responseHookInfo: MySQL2ResponseHookInformation): void;
}

export interface MySQL2RequestInfo {
host?: string;
database?: string;
query: string | Query | QueryOptions;
values?: unknown[];
}

export type SpanNameHook = (
info: MySQL2RequestInfo,
/**
* If no decision is taken based on RequestInfo, the default name
* supplied by the instrumentation can be used instead.
*/
defaultName: string
) => string;

export interface MySQL2InstrumentationConfig extends InstrumentationConfig {
/**
* Hook that allows adding custom span attributes based on the data
Expand All @@ -34,6 +51,11 @@ export interface MySQL2InstrumentationConfig extends InstrumentationConfig {
*/
responseHook?: MySQL2InstrumentationExecutionResponseHook;

/**
* Hook to override the name for an SQL span
*/
spanNameHook?: SpanNameHook;

/**
* If true, queries are modified to also include a comment with
* the tracing context, following the {@link https://github.com/open-telemetry/opentelemetry-sqlcommenter sqlcommenter} format
Expand Down
104 changes: 104 additions & 0 deletions plugins/node/opentelemetry-instrumentation-mysql2/test/mysql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,110 @@ describe('mysql2', () => {
});
});

describe('#spanNameHook', () => {
it('span hook gets request info as parameters', done => {
const config: MySQL2InstrumentationConfig = {
spanNameHook: (info, defaultName) => {
assert.strictEqual(defaultName, 'SELECT');
assert.deepStrictEqual(info, {
database: 'test_db',
host: '127.0.0.1',
query: 'SELECT ? as solution',
values: ['otel-user'],
});
return defaultName;
},
};
instrumentation.setConfig(config);

const span = provider.getTracer('default').startSpan('test span');
context.with(trace.setSpan(context.active(), span), () => {
const sql = 'SELECT ? as solution';
const query = connection.query(sql, ['otel-user']);

query.on('end', () => {
done();
});
});
});

describe('valid span name hook', () => {
beforeEach(() => {
const config: MySQL2InstrumentationConfig = {
spanNameHook: (info, defaultName) => {
const query =
typeof info.query === 'string' ? info.query : info.query.sql;
const prioritizedSqlVerbs = [
'DROP',
'DELETE',
'INSERT',
'UPDATE',
'SELECT',
];
for (const verb of prioritizedSqlVerbs) {
if (query.includes(verb)) {
return verb;
}
}
return 'UNKNOWN';
},
};
instrumentation.setConfig(config);
});

it('should set span name using spanNameHook', done => {
const span = provider.getTracer('default').startSpan('test span');
context.with(trace.setSpan(context.active(), span), () => {
const sql =
'WITH number AS (SELECT 1+1 as solution) SELECT solution FROM number';
connection.query(
sql,
['otel-user'],
(err, res: mysqlTypes.RowDataPacket[]) => {
assert.ifError(err);
assert.ok(res);
console.log(res[0]);
assert.strictEqual(res[0].solution, 2);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
assertSpan(spans[0], sql);
assert.strictEqual(spans[0].name, 'SELECT');
done();
}
);
});
});
});

describe('invalid span name hook', () => {
beforeEach(() => {
const config: MySQL2InstrumentationConfig = {
spanNameHook: () => {
throw new Error('could not decide on a name');
},
};
instrumentation.setConfig(config);
});

it('should not affect the behavior of the query', done => {
const span = provider.getTracer('default').startSpan('test span');
context.with(trace.setSpan(context.active(), span), () => {
const sql = 'SELECT 1+1 as solution';
connection.query(sql, (err, res: mysqlTypes.RowDataPacket[]) => {
assert.ifError(err);
assert.ok(res);
assert.strictEqual(res[0].solution, 2);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
assertSpan(spans[0], sql);
assert.strictEqual(spans[0].name, 'SELECT');
done();
});
});
});
});
});

describe('#responseHook', () => {
const queryResultAttribute = 'query_result';

Expand Down
Loading