Skip to content

Commit 80f3693

Browse files
Lms24claude
andauthored
feat(server-utils): Emit low cardinality mysql db span names (#23601)
With span streaming enabled: - `mysql` and `mysql2` query spans are named after `db.query.summary` - e.g. `SELECT solution FROM maths` becomes `SELECT maths` - falls back to `db.namespace`, then `db.system.name` - new `db.query.summary` attribute, set in both trace lifecycles Refs #23523 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b5b06be commit 80f3693

5 files changed

Lines changed: 100 additions & 8 deletions

File tree

‎dev-packages/node-integration-tests/suites/tracing/mysql/test.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,12 @@ describe('mysql auto instrumentation', () => {
263263
type: 'string',
264264
value: 'SELECT 1 + 1 AS solution',
265265
},
266+
'db.query.summary': {
267+
type: 'string',
268+
value: 'SELECT',
269+
},
266270
},
267-
name: 'SELECT 1 + 1 AS solution',
271+
name: 'SELECT',
268272
...COMMON_SPAN_PROPS,
269273
},
270274
{
@@ -274,8 +278,12 @@ describe('mysql auto instrumentation', () => {
274278
type: 'string',
275279
value: 'SELECT NOW()',
276280
},
281+
'db.query.summary': {
282+
type: 'string',
283+
value: 'SELECT',
284+
},
277285
},
278-
name: 'SELECT NOW()',
286+
name: 'SELECT',
279287
...COMMON_SPAN_PROPS,
280288
},
281289
]);

‎packages/server-utils/src/integrations/mysql.ts‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as diagnosticsChannel from 'node:diagnostics_channel';
22
import {
33
DB_NAMESPACE,
4+
DB_QUERY_SUMMARY,
45
DB_QUERY_TEXT,
56
DB_SYSTEM_NAME,
67
DB_USER,
@@ -10,10 +11,14 @@ import {
1011
} from '@sentry/conventions/attributes';
1112
import type { IntegrationFn, Scope } from '@sentry/core';
1213
import {
14+
_INTERNAL_getSqlQuerySummary,
15+
_INTERNAL_sanitizeSqlQuery,
1316
isObjectLike,
1417
bindScopeToEmitter,
1518
defineIntegration,
19+
getClient,
1620
getCurrentScope,
21+
hasSpanStreamingEnabled,
1722
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1823
startInactiveSpan,
1924
} from '@sentry/core';
@@ -30,6 +35,8 @@ const INTEGRATION_NAME = 'Mysql' as const;
3035
// `@opentelemetry/instrumentation-mysql`'s default shape.
3136
const ATTR_DB_CONNECTION_STRING = 'db.connection_string';
3237

38+
const DB_SYSTEM_NAME_VALUE_MYSQL = 'mysql' as const;
39+
3340
/**
3441
* The shape orchestrion's transform attaches to the tracing-channel `context` object. Documented here
3542
* rather than imported because orchestrion's runtime doesn't export it.
@@ -80,17 +87,26 @@ function instrumentMysql(): void {
8087
// handler with the caller's context lost. `deferSpanEnd` replays this scope onto the emitter.
8188
data._sentryCallerScope = getCurrentScope();
8289

90+
const querySummary = sql ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(sql)) : undefined;
91+
92+
const client = getClient();
93+
const name =
94+
client && hasSpanStreamingEnabled(client)
95+
? querySummary || database || DB_SYSTEM_NAME_VALUE_MYSQL
96+
: (sql ?? 'mysql.query');
97+
8398
return startInactiveSpan({
84-
name: sql ?? 'mysql.query',
99+
name,
85100
op: 'db',
86101
attributes: {
87102
[SENTRY_KIND]: 'client',
88-
[DB_SYSTEM_NAME]: 'mysql',
103+
[DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL,
89104
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.mysql',
90105
[ATTR_DB_CONNECTION_STRING]: getJDBCString(host, portIsNumber ? portNumber : undefined, database),
91106
...(database ? { [DB_NAMESPACE]: database } : {}),
92107
...(user ? { [DB_USER]: user } : {}),
93108
...(sql ? { [DB_QUERY_TEXT]: sql } : {}),
109+
[DB_QUERY_SUMMARY]: querySummary,
94110
[SERVER_ADDRESS]: host,
95111
[SERVER_PORT]: portIsNumber ? portNumber : undefined,
96112
},

‎packages/server-utils/src/integrations/mysql2/index.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import * as diagnosticsChannel from 'node:diagnostics_channel';
22
import type { IntegrationFn, SpanAttributes } from '@sentry/core';
33
import {
4+
_INTERNAL_getSqlQuerySummary,
5+
_INTERNAL_sanitizeSqlQuery,
46
defineIntegration,
7+
getClient,
8+
hasSpanStreamingEnabled,
59
isObjectLike,
610
SEMANTIC_ATTRIBUTE_SENTRY_OP,
711
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
@@ -16,6 +20,7 @@ import { mysql2ModuleNames } from '../../orchestrion/config/mysql2';
1620
import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation';
1721
import {
1822
DB_NAMESPACE,
23+
DB_QUERY_SUMMARY,
1924
DB_QUERY_TEXT,
2025
DB_SYSTEM_NAME,
2126
DB_USER,
@@ -78,16 +83,25 @@ function subscribeQueryChannel(channelName: ChannelName): void {
7883
diagnosticsChannel.tracingChannel<Mysql2QueryChannelContext>(channelName),
7984
data => {
8085
const statement = getQueryText(data.arguments);
86+
const connectionAttributes = getConnectionAttributes(data.self?.config);
87+
const querySummary = statement ? _INTERNAL_getSqlQuerySummary(_INTERNAL_sanitizeSqlQuery(statement)) : undefined;
88+
89+
const client = getClient();
90+
const name =
91+
client && hasSpanStreamingEnabled(client)
92+
? querySummary || (connectionAttributes[DB_NAMESPACE] as string | undefined) || DB_SYSTEM_VALUE_MYSQL
93+
: (statement ?? 'mysql2.query');
8194

8295
return startInactiveSpan({
83-
name: statement ?? 'mysql2.query',
96+
name,
8497
attributes: {
8598
[SENTRY_KIND]: 'client',
8699
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
87100
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
88101
[DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL,
89-
...getConnectionAttributes(data.self?.config),
90102
[DB_QUERY_TEXT]: statement || undefined,
103+
[DB_QUERY_SUMMARY]: querySummary,
104+
...connectionAttributes,
91105
},
92106
});
93107
},

‎packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import type { TracingChannel } from 'node:diagnostics_channel';
22
import {
33
DB_NAMESPACE,
44
DB_OPERATION_NAME,
5+
DB_QUERY_SUMMARY,
56
DB_QUERY_TEXT,
67
DB_SYSTEM_NAME,
78
SERVER_ADDRESS,
89
SERVER_PORT,
910
} from '@sentry/conventions/attributes';
1011
import {
12+
_INTERNAL_getSqlQuerySummary,
1113
_INTERNAL_sanitizeSqlQuery,
14+
getClient,
15+
hasSpanStreamingEnabled,
1216
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1317
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1418
startInactiveSpan,
@@ -100,14 +104,22 @@ function setupQueryChannel(tracingChannel: MySQL2TracingChannelFactory, channelN
100104
// literal before it leaves the process; `values` is never attached.
101105
const queryText = data.query ? _INTERNAL_sanitizeSqlQuery(data.query) : undefined;
102106
const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase();
107+
const querySummary = _INTERNAL_getSqlQuerySummary(queryText);
108+
109+
const client = getClient();
110+
const name =
111+
client && hasSpanStreamingEnabled(client)
112+
? querySummary || data.database || DB_SYSTEM_NAME_VALUE_MYSQL
113+
: queryText || 'mysql2.query';
103114

104115
return startInactiveSpan({
105-
name: queryText || 'mysql2.query',
116+
name,
106117
attributes: {
107118
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
108119
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
109120
[DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL,
110121
[DB_QUERY_TEXT]: queryText,
122+
[DB_QUERY_SUMMARY]: querySummary,
111123
[DB_OPERATION_NAME]: operation,
112124
[DB_NAMESPACE]: data.database || undefined,
113125
[SERVER_ADDRESS]: data.serverAddress,

‎packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ class TestClient extends Client<any> {
4141
}
4242
}
4343

44-
function initTestClient(): void {
44+
function initTestClient(traceLifecycle: 'static' | 'stream' = 'static'): void {
4545
initAndBind(TestClient, {
4646
dsn: 'https://username@domain/123',
4747
integrations: [],
4848
sendClientReports: false,
4949
stackParser: () => [],
50+
traceLifecycle,
5051
tracesSampleRate: 1,
5152
transport: () => createTransport({ recordDroppedEvent: () => undefined }, () => resolvedSyncPromise({})),
5253
});
@@ -199,6 +200,8 @@ describe('subscribeMysql2DiagnosticChannels', () => {
199200
expect(json.attributes['sentry.origin']).toBe('auto.db.mysql2.diagnostic_channel');
200201
expect(json.attributes['db.system.name']).toBe('mysql');
201202
expect(json.attributes['db.operation.name']).toBe('SELECT');
203+
// reported regardless of trace lifecycle, even though only span streaming names the span after it
204+
expect(json.attributes['db.query.summary']).toBe('SELECT maths');
202205
expect(json.attributes['db.namespace']).toBe('test');
203206
expect(json.attributes['server.address']).toBe('127.0.0.1');
204207
expect(json.attributes['server.port']).toBe(3306);
@@ -222,6 +225,45 @@ describe('subscribeMysql2DiagnosticChannels', () => {
222225
expect(json.name).toBe('SELECT * FROM users WHERE email = ? AND age = ?');
223226
});
224227

228+
it('names the span after the query summary with span streaming enabled', async () => {
229+
initTestClient('stream');
230+
231+
const { span } = await traceOperation(
232+
MYSQL2_DC_CHANNEL_QUERY,
233+
{ query: 'SELECT solution FROM maths' },
234+
{ result: [] },
235+
);
236+
237+
const json = spanToJSON(span!);
238+
expect(json.name).toBe('SELECT maths');
239+
expect(json.attributes['db.query.summary']).toBe('SELECT maths');
240+
// the statement is still reported, just not as the name
241+
expect(json.attributes['db.query.text']).toBe('SELECT solution FROM maths');
242+
});
243+
244+
it('walks the name conventions when no query summary can be derived', async () => {
245+
initTestClient('stream');
246+
247+
const { span } = await traceOperation(
248+
MYSQL2_DC_CHANNEL_QUERY,
249+
{ query: '', database: 'test', serverAddress: '127.0.0.1', serverPort: 3306 },
250+
{ result: [] },
251+
);
252+
253+
// no summary and no operation, so `{db.namespace}` is the first template that can be filled
254+
const json = spanToJSON(span!);
255+
expect(json.name).toBe('test');
256+
expect(json.attributes['db.query.summary']).toBeUndefined();
257+
});
258+
259+
it('falls back to the db system name when nothing else can be filled in', async () => {
260+
initTestClient('stream');
261+
262+
const { span } = await traceOperation(MYSQL2_DC_CHANNEL_QUERY, { query: '' }, { result: [] });
263+
264+
expect(spanToJSON(span!).name).toBe('mysql');
265+
});
266+
225267
it('does not attach raw values to the span', async () => {
226268
const { span } = await traceOperation(
227269
MYSQL2_DC_CHANNEL_QUERY,

0 commit comments

Comments
 (0)