Skip to content

Commit ad48e45

Browse files
committed
feat(instrumentation-mysql2): Add hook for setting span name
1 parent d5215f3 commit ad48e45

File tree

3 files changed

+151
-2
lines changed

3 files changed

+151
-2
lines changed

plugins/node/opentelemetry-instrumentation-mysql2/src/instrumentation.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
} from '@opentelemetry/semantic-conventions';
2929
import { addSqlCommenterComment } from '@opentelemetry/sql-common';
3030
import type * as mysqlTypes from 'mysql2';
31-
import { MySQL2InstrumentationConfig } from './types';
31+
import { MySQL2InstrumentationConfig, MySQL2RequestInfo } from './types';
3232
import {
3333
getConnectionAttributes,
3434
getDbStatement,
@@ -104,7 +104,30 @@ export class MySQL2Instrumentation extends InstrumentationBase<MySQL2Instrumenta
104104
values = [_valuesOrCallback];
105105
}
106106

107-
const span = thisPlugin.tracer.startSpan(getSpanName(query), {
107+
const defaultSpanName = getSpanName(query);
108+
const mysql2RequestInfo: MySQL2RequestInfo = {
109+
query,
110+
values,
111+
database: this.config.database,
112+
host: this.config.host,
113+
};
114+
const spanNameHook = thisPlugin.getConfig().spanNameHook;
115+
let spanName = defaultSpanName;
116+
if (spanNameHook) {
117+
spanName =
118+
safeExecuteInTheMiddle(
119+
() => spanNameHook(mysql2RequestInfo, defaultSpanName),
120+
(err, result) => {
121+
if (err) {
122+
thisPlugin._diag.warn('Failed executing spanNameHook', err);
123+
}
124+
return result;
125+
},
126+
true
127+
) ?? defaultSpanName;
128+
}
129+
130+
const span = thisPlugin.tracer.startSpan(spanName, {
108131
kind: api.SpanKind.CLIENT,
109132
attributes: {
110133
...MySQL2Instrumentation.COMMON_ATTRIBUTES,

plugins/node/opentelemetry-instrumentation-mysql2/src/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { InstrumentationConfig } from '@opentelemetry/instrumentation';
1818
import type { Span } from '@opentelemetry/api';
19+
import type { Query, QueryOptions } from 'mysql2';
1920

2021
export interface MySQL2ResponseHookInformation {
2122
queryResults: any;
@@ -25,6 +26,22 @@ export interface MySQL2InstrumentationExecutionResponseHook {
2526
(span: Span, responseHookInfo: MySQL2ResponseHookInformation): void;
2627
}
2728

29+
export interface MySQL2RequestInfo {
30+
host?: string;
31+
database?: string;
32+
query: string | Query | QueryOptions;
33+
values?: unknown[];
34+
}
35+
36+
export type SpanNameHook = (
37+
info: MySQL2RequestInfo,
38+
/**
39+
* If no decision is taken based on RequestInfo, the default name
40+
* supplied by the instrumentation can be used instead.
41+
*/
42+
defaultName: string
43+
) => string;
44+
2845
export interface MySQL2InstrumentationConfig extends InstrumentationConfig {
2946
/**
3047
* Hook that allows adding custom span attributes based on the data
@@ -34,6 +51,11 @@ export interface MySQL2InstrumentationConfig extends InstrumentationConfig {
3451
*/
3552
responseHook?: MySQL2InstrumentationExecutionResponseHook;
3653

54+
/**
55+
* Hook to override the name for an SQL span
56+
*/
57+
spanNameHook?: SpanNameHook;
58+
3759
/**
3860
* If true, queries are modified to also include a comment with
3961
* the tracing context, following the {@link https://github.com/open-telemetry/opentelemetry-sqlcommenter sqlcommenter} format

plugins/node/opentelemetry-instrumentation-mysql2/test/mysql.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,110 @@ describe('mysql2', () => {
11001100
});
11011101
});
11021102

1103+
describe('#spanNameHook', () => {
1104+
it('span hook gets request info as parameters', done => {
1105+
const config: MySQL2InstrumentationConfig = {
1106+
spanNameHook: (info, defaultName) => {
1107+
assert.strictEqual(defaultName, 'SELECT');
1108+
assert.deepStrictEqual(info, {
1109+
database: 'test_db',
1110+
host: '127.0.0.1',
1111+
query: 'SELECT ? as solution',
1112+
values: ['otel-user'],
1113+
});
1114+
return defaultName;
1115+
},
1116+
};
1117+
instrumentation.setConfig(config);
1118+
1119+
const span = provider.getTracer('default').startSpan('test span');
1120+
context.with(trace.setSpan(context.active(), span), () => {
1121+
const sql = 'SELECT ? as solution';
1122+
const query = connection.query(sql, ['otel-user']);
1123+
1124+
query.on('end', () => {
1125+
done();
1126+
});
1127+
});
1128+
});
1129+
1130+
describe('valid span name hook', () => {
1131+
beforeEach(() => {
1132+
const config: MySQL2InstrumentationConfig = {
1133+
spanNameHook: (info, defaultName) => {
1134+
const query =
1135+
typeof info.query === 'string' ? info.query : info.query.sql;
1136+
const prioritizedSqlVerbs = [
1137+
'DROP',
1138+
'DELETE',
1139+
'INSERT',
1140+
'UPDATE',
1141+
'SELECT',
1142+
];
1143+
for (const verb of prioritizedSqlVerbs) {
1144+
if (query.includes(verb)) {
1145+
return verb;
1146+
}
1147+
}
1148+
return 'UNKNOWN';
1149+
},
1150+
};
1151+
instrumentation.setConfig(config);
1152+
});
1153+
1154+
it('should set span name using spanNameHook', done => {
1155+
const span = provider.getTracer('default').startSpan('test span');
1156+
context.with(trace.setSpan(context.active(), span), () => {
1157+
const sql =
1158+
'WITH number AS (SELECT 1+1 as solution) SELECT solution FROM number';
1159+
connection.query(
1160+
sql,
1161+
['otel-user'],
1162+
(err, res: mysqlTypes.RowDataPacket[]) => {
1163+
assert.ifError(err);
1164+
assert.ok(res);
1165+
console.log(res[0]);
1166+
assert.strictEqual(res[0].solution, 2);
1167+
const spans = memoryExporter.getFinishedSpans();
1168+
assert.strictEqual(spans.length, 1);
1169+
assertSpan(spans[0], sql);
1170+
assert.strictEqual(spans[0].name, 'SELECT');
1171+
done();
1172+
}
1173+
);
1174+
});
1175+
});
1176+
});
1177+
1178+
describe('invalid span name hook', () => {
1179+
beforeEach(() => {
1180+
const config: MySQL2InstrumentationConfig = {
1181+
spanNameHook: () => {
1182+
throw new Error('could not decide on a name');
1183+
},
1184+
};
1185+
instrumentation.setConfig(config);
1186+
});
1187+
1188+
it('should not affect the behavior of the query', done => {
1189+
const span = provider.getTracer('default').startSpan('test span');
1190+
context.with(trace.setSpan(context.active(), span), () => {
1191+
const sql = 'SELECT 1+1 as solution';
1192+
connection.query(sql, (err, res: mysqlTypes.RowDataPacket[]) => {
1193+
assert.ifError(err);
1194+
assert.ok(res);
1195+
assert.strictEqual(res[0].solution, 2);
1196+
const spans = memoryExporter.getFinishedSpans();
1197+
assert.strictEqual(spans.length, 1);
1198+
assertSpan(spans[0], sql);
1199+
assert.strictEqual(spans[0].name, 'SELECT');
1200+
done();
1201+
});
1202+
});
1203+
});
1204+
});
1205+
});
1206+
11031207
describe('#responseHook', () => {
11041208
const queryResultAttribute = 'query_result';
11051209

0 commit comments

Comments
 (0)