Skip to content

Commit 04a9a44

Browse files
committed
feat: populate span with additional attrs
1 parent e805c81 commit 04a9a44

File tree

4 files changed

+284
-17
lines changed

4 files changed

+284
-17
lines changed

packages/nuxt/src/runtime/plugins/database.server.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,20 @@ import {
1313
import type { Database, PreparedStatement } from 'db0';
1414
// eslint-disable-next-line import/no-extraneous-dependencies
1515
import { defineNitroPlugin, useDatabase } from 'nitropack/runtime';
16+
import type { DatabaseConnectionConfig as DatabaseConfig } from 'nitropack/types';
1617
// @ts-expect-error - This is a virtual module
17-
import { databaseInstances } from '#sentry/database-config.mjs';
18+
import { databaseConfig } from '#sentry/database-config.mjs';
19+
import { getDatabaseSpanData } from '../utils/database';
1820

1921
type MaybeInstrumentedDatabase = Database & {
2022
__sentry_instrumented__?: boolean;
2123
};
2224

25+
interface DatabaseSpanData {
26+
[key: string]: string | undefined;
27+
'db.system.name': string;
28+
}
29+
2330
/**
2431
* Keeps track of prepared statements that have been patched.
2532
*/
@@ -35,12 +42,14 @@ const SENTRY_ORIGIN = 'auto.db.nuxt';
3542
*/
3643
export default defineNitroPlugin(() => {
3744
try {
45+
const _databaseConfig = databaseConfig as Record<string, DatabaseConfig>;
46+
const databaseInstances = Object.keys(databaseConfig);
3847
debug.log('[Nitro Database Plugin]: Instrumenting databases...');
3948

4049
for (const instance of databaseInstances) {
4150
debug.log('[Nitro Database Plugin]: Instrumenting database instance:', instance);
4251
const db = useDatabase(instance);
43-
instrumentDatabase(db);
52+
instrumentDatabase(db, _databaseConfig[instance]);
4453
}
4554

4655
debug.log('[Nitro Database Plugin]: Databases instrumented.');
@@ -55,17 +64,25 @@ export default defineNitroPlugin(() => {
5564
}
5665
});
5766

58-
function instrumentDatabase(db: MaybeInstrumentedDatabase): void {
67+
/**
68+
* Instruments a database instance with Sentry.
69+
*/
70+
function instrumentDatabase(db: MaybeInstrumentedDatabase, config?: DatabaseConfig): void {
5971
if (db.__sentry_instrumented__) {
6072
debug.log('[Nitro Database Plugin]: Database already instrumented. Skipping...');
6173
return;
6274
}
6375

76+
const metadata: DatabaseSpanData = {
77+
'db.system.name': config?.connector ?? db.dialect,
78+
...getDatabaseSpanData(config),
79+
};
80+
6481
db.prepare = new Proxy(db.prepare, {
6582
apply(target, thisArg, args: Parameters<typeof db.prepare>) {
6683
const [query] = args;
6784

68-
return instrumentPreparedStatement(target.apply(thisArg, args), query, db.dialect);
85+
return instrumentPreparedStatement(target.apply(thisArg, args), query, metadata);
6986
},
7087
});
7188

@@ -75,7 +92,7 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase): void {
7592
db.sql = new Proxy(db.sql, {
7693
apply(target, thisArg, args: Parameters<typeof db.sql>) {
7794
const query = args[0]?.[0] ?? '';
78-
const opts = createStartSpanOptions(query, db.dialect);
95+
const opts = createStartSpanOptions(query, metadata);
7996

8097
return startSpan(
8198
opts,
@@ -87,7 +104,7 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase): void {
87104
db.exec = new Proxy(db.exec, {
88105
apply(target, thisArg, args: Parameters<typeof db.exec>) {
89106
return startSpan(
90-
createStartSpanOptions(args[0], db.dialect),
107+
createStartSpanOptions(args[0], metadata),
91108
handleSpanStart(() => target.apply(thisArg, args), { query: args[0] }),
92109
);
93110
},
@@ -102,16 +119,20 @@ function instrumentDatabase(db: MaybeInstrumentedDatabase): void {
102119
* This is meant to be used as a top-level call, under the hood it calls `instrumentPreparedStatementQueries`
103120
* to patch the query methods. The reason for this abstraction is to ensure that the `bind` method is also patched.
104121
*/
105-
function instrumentPreparedStatement(statement: PreparedStatement, query: string, dialect: string): PreparedStatement {
122+
function instrumentPreparedStatement(
123+
statement: PreparedStatement,
124+
query: string,
125+
data: DatabaseSpanData,
126+
): PreparedStatement {
106127
// statement.bind() returns a new instance of D1PreparedStatement, so we have to patch it as well.
107128
// eslint-disable-next-line @typescript-eslint/unbound-method
108129
statement.bind = new Proxy(statement.bind, {
109130
apply(target, thisArg, args: Parameters<typeof statement.bind>) {
110-
return instrumentPreparedStatementQueries(target.apply(thisArg, args), query, dialect);
131+
return instrumentPreparedStatementQueries(target.apply(thisArg, args), query, data);
111132
},
112133
});
113134

114-
return instrumentPreparedStatementQueries(statement, query, dialect);
135+
return instrumentPreparedStatementQueries(statement, query, data);
115136
}
116137

117138
/**
@@ -120,7 +141,7 @@ function instrumentPreparedStatement(statement: PreparedStatement, query: string
120141
function instrumentPreparedStatementQueries(
121142
statement: PreparedStatement,
122143
query: string,
123-
dialect: string,
144+
data: DatabaseSpanData,
124145
): PreparedStatement {
125146
if (patchedStatement.has(statement)) {
126147
return statement;
@@ -130,7 +151,7 @@ function instrumentPreparedStatementQueries(
130151
statement.get = new Proxy(statement.get, {
131152
apply(target, thisArg, args: Parameters<typeof statement.get>) {
132153
return startSpan(
133-
createStartSpanOptions(query, dialect),
154+
createStartSpanOptions(query, data),
134155
handleSpanStart(() => target.apply(thisArg, args), { query }),
135156
);
136157
},
@@ -140,7 +161,7 @@ function instrumentPreparedStatementQueries(
140161
statement.run = new Proxy(statement.run, {
141162
apply(target, thisArg, args: Parameters<typeof statement.run>) {
142163
return startSpan(
143-
createStartSpanOptions(query, dialect),
164+
createStartSpanOptions(query, data),
144165
handleSpanStart(() => target.apply(thisArg, args), { query }),
145166
);
146167
},
@@ -150,7 +171,7 @@ function instrumentPreparedStatementQueries(
150171
statement.all = new Proxy(statement.all, {
151172
apply(target, thisArg, args: Parameters<typeof statement.all>) {
152173
return startSpan(
153-
createStartSpanOptions(query, dialect),
174+
createStartSpanOptions(query, data),
154175
handleSpanStart(() => target.apply(thisArg, args), { query }),
155176
);
156177
},
@@ -203,14 +224,14 @@ function createBreadcrumb(query: string): void {
203224
/**
204225
* Creates a start span options object.
205226
*/
206-
function createStartSpanOptions(query: string, dialect: string): StartSpanOptions {
227+
function createStartSpanOptions(query: string, data: DatabaseSpanData): StartSpanOptions {
207228
return {
208229
name: query,
209230
attributes: {
210-
'db.system.name': dialect,
211231
'db.query.text': query,
212232
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN,
213233
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.query',
234+
...data,
214235
},
215236
};
216237
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { ConnectorName } from 'db0';
2+
import type { DatabaseConnectionConfig as DatabaseConfig } from 'nitropack/types';
3+
4+
interface DatabaseSpanData {
5+
[key: string]: string | number | undefined;
6+
}
7+
8+
/**
9+
* Extracts span attributes from the database configuration.
10+
*/
11+
export function getDatabaseSpanData(config?: DatabaseConfig): Partial<DatabaseSpanData> {
12+
try {
13+
if (!config?.connector) {
14+
// Default to SQLite if no connector is configured
15+
return {
16+
'db.namespace': 'db.sqlite',
17+
};
18+
}
19+
20+
if (config.connector === 'postgresql' || config.connector === 'mysql2') {
21+
return {
22+
'server.address': config.options?.host,
23+
'server.port': config.options?.port,
24+
};
25+
}
26+
27+
if (config.connector === 'pglite') {
28+
return {
29+
'db.namespace': config.options?.dataDir,
30+
};
31+
}
32+
33+
if ((['better-sqlite3', 'bun', 'sqlite', 'sqlite3'] as ConnectorName[]).includes(config.connector)) {
34+
return {
35+
// DB is the default file name in nitro for sqlite-like connectors
36+
'db.namespace': `${config.options?.name ?? 'db'}.sqlite`,
37+
};
38+
}
39+
40+
return {};
41+
} catch {
42+
// This is a best effort to get some attributes, so it is not an absolute must
43+
// Since the user can configure invalid options, we should not fail the whole instrumentation.
44+
return {};
45+
}
46+
}

packages/nuxt/src/vite/databaseConfig.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ export function addDatabaseInstrumentation(nitro: NitroConfig): void {
2121
/**
2222
* This is a different option than the one in `experimental.database`, this configures multiple database instances.
2323
* keys represent database names to be passed to `useDatabase(name?)`.
24+
* We also use the config to populate database span attributes.
2425
* https://nitro.build/guide/database#configuration
2526
*/
26-
const databaseInstances = Object.keys(nitro.database || { default: {} });
27+
const databaseConfig = nitro.database || { default: {} };
2728

2829
// Create a virtual module to pass this data to runtime
2930
addServerTemplate({
3031
filename: '#sentry/database-config.mjs',
3132
getContents: () => {
32-
return `export const databaseInstances = ${JSON.stringify(databaseInstances)};`;
33+
return `export const databaseConfig = ${JSON.stringify(databaseConfig)};`;
3334
},
3435
});
3536

0 commit comments

Comments
 (0)