autotel-drizzle 0.0.53 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,33 +110,73 @@ await db.select().from(users).where(eq(users.id, 123));
110
110
  peerName?: string // Database host
111
111
  peerPort?: number // Database port
112
112
  explain?: 'plan' | 'analyze' | false // Capture the postgres query plan (default: false)
113
+ semconv?: 'legacy' | 'stable' | 'dup' // Which attribute names to emit
114
+ spanNaming?: 'drizzle' | 'semconv' // Span name style (default: 'drizzle')
113
115
  }
114
116
  ```
115
117
 
118
+ **Attribute names: `semconv`**
119
+
120
+ OpenTelemetry renamed the database attributes. `semconv` picks which names a span
121
+ carries, and defaults to whatever `OTEL_SEMCONV_STABILITY_OPT_IN` asks for, or
122
+ `'legacy'` when that is unset.
123
+
124
+ | Mode | Emits |
125
+ | ---------- | ---------------------------------------------------------------------- |
126
+ | `'legacy'` | `db.system`, `db.operation`, `db.name`, `db.statement` |
127
+ | `'stable'` | `db.system.name`, `db.operation.name`, `db.namespace`, `db.query.text` |
128
+ | `'dup'` | both, so tools on either side of the rename read the same trace |
129
+
130
+ `'dup'` is what the migration period is for: set it and nothing downstream has to
131
+ move at the same time as you.
132
+
133
+ **Span names: `spanNaming`**
134
+
135
+ `'drizzle'` (the default) names spans `drizzle.select`. `'semconv'` follows the
136
+ OpenTelemetry database convention, `{db.operation.name} {target}`, so
137
+ `SELECT comments`. The default stays put so existing dashboards and saved queries
138
+ keep working.
139
+
116
140
  **Span Attributes:**
117
141
 
118
- - `db.system` - Database type (postgresql, mysql, sqlite)
119
- - `db.operation` - Operation name (SELECT, INSERT, UPDATE, DELETE)
120
- - `db.name` - Database name
121
- - `db.statement` - SQL query text (if `captureQueryText: true`)
142
+ - `db.system` / `db.system.name` - Database type (postgresql, mysql, sqlite)
143
+ - `db.operation` / `db.operation.name` - Operation name (SELECT, INSERT, UPDATE, DELETE)
144
+ - `db.name` / `db.namespace` - Database name
145
+ - `db.statement` / `db.query.text` - SQL query text (if `captureQueryText: true`)
122
146
  - `db.statement.hash` - Stable hash of the statement, present even when the text is suppressed
147
+ - `db.collection.name` - The table the statement is aimed at
123
148
  - `net.peer.name` - Database host
124
149
  - `net.peer.port` - Database port
125
150
 
151
+ ### Finding an N+1
152
+
153
+ `db.statement.hash` is the cheapest N+1 detector you have. A route that loops a
154
+ query emits one span per iteration, all carrying the same hash, and none of them
155
+ individually slow. Group a trace's spans by it:
156
+
157
+ | `db.statement.hash` | `db.collection.name` | Spans |
158
+ | ------------------- | -------------------- | ----: |
159
+ | `6b863b41773053ba` | `comments` | 200 |
160
+ | `53ee56739d371a4b` | `posts` | 1 |
161
+
162
+ One statement, run 200 times. Asking which span was slowest gives you a 1.7%
163
+ answer and sends you off to index a table that was never the problem. The
164
+ `find_repeated_queries` tool in `autotel-mcp` does this grouping for you.
165
+
126
166
  ## Drivers
127
167
 
128
168
  Tested against postgres 16, mysql 8, and sqlite, driving each through drizzle's
129
169
  own query builder, its relational API, and raw `db.execute`. Every driver below
130
170
  emits one span per query, names the operation, and tags `db.collection.name`.
131
171
 
132
- | Driver | Queries | Transaction span | Query plans |
133
- | --- | --- | --- | --- |
134
- | `node-postgres` | yes | yes | yes |
135
- | `postgres.js` | yes | yes | yes |
136
- | `pglite` | yes | yes | yes |
137
- | `mysql2` | yes | yes | postgres only |
138
- | `better-sqlite3` | yes | yes, synchronous | postgres only |
139
- | `libsql` | yes | yes | postgres only |
172
+ | Driver | Queries | Transaction span | Query plans |
173
+ | ---------------- | ------- | ---------------- | ------------- |
174
+ | `node-postgres` | yes | yes | yes |
175
+ | `postgres.js` | yes | yes | yes |
176
+ | `pglite` | yes | yes | yes |
177
+ | `mysql2` | yes | yes | postgres only |
178
+ | `better-sqlite3` | yes | yes, synchronous | postgres only |
179
+ | `libsql` | yes | yes | postgres only |
140
180
 
141
181
  Nothing about the instrumentation is dialect-specific except `explain`, which
142
182
  sends postgres syntax and so turns itself off unless `dbSystem` is
@@ -165,19 +205,19 @@ instrumentDrizzleClient(db, {
165
205
 
166
206
  The same query, before and after `CREATE INDEX`:
167
207
 
168
- | Attribute | Before | After |
169
- | --- | --- | --- |
170
- | `db.statement.hash` | `372269a8881e921a` | `372269a8881e921a` |
171
- | `db.plan.hash` | `0b50591ce9f68f51` | `eb8765b8d1c4af5a` |
172
- | `db.plan.node` | `Seq Scan` | `Bitmap Heap Scan` |
173
- | `db.plan.indexes` | | `idx_comments_post_id` |
174
- | `db.plan.seq_scan` | `true` | `false` |
175
- | `db.plan.rows_examined` | `200199` | `1001` |
176
- | `db.plan.rows_returned` | `1001` | `1001` |
177
- | `db.plan.blocks` | `1861` | `1005` |
178
- | `db.plan.cost` | | planner's total cost estimate |
179
- | `db.plan.rows_estimated` | | rows the planner expected |
180
- | `db.plan.execution_ms` | | measured time (`'analyze'` only) |
208
+ | Attribute | Before | After |
209
+ | ------------------------ | ------------------ | -------------------------------- |
210
+ | `db.statement.hash` | `372269a8881e921a` | `372269a8881e921a` |
211
+ | `db.plan.hash` | `0b50591ce9f68f51` | `eb8765b8d1c4af5a` |
212
+ | `db.plan.node` | `Seq Scan` | `Bitmap Heap Scan` |
213
+ | `db.plan.indexes` | | `idx_comments_post_id` |
214
+ | `db.plan.seq_scan` | `true` | `false` |
215
+ | `db.plan.rows_examined` | `200199` | `1001` |
216
+ | `db.plan.rows_returned` | `1001` | `1001` |
217
+ | `db.plan.blocks` | `1861` | `1005` |
218
+ | `db.plan.cost` | | planner's total cost estimate |
219
+ | `db.plan.rows_estimated` | | rows the planner expected |
220
+ | `db.plan.execution_ms` | | measured time (`'analyze'` only) |
181
221
 
182
222
  An unchanged statement hash beside a changed plan hash is a planner decision.
183
223
  Grouping `db.plan.indexes` by `db.statement.hash` answers which queries an index
@@ -199,7 +239,7 @@ CI, or behind a sample of production traffic.
199
239
  - `'analyze'` runs on read-only statements only, so a traced insert never runs
200
240
  twice. A leading `WITH` qualifies only when no writing keyword appears
201
241
  anywhere in the statement, because `WITH gone AS (DELETE ... RETURNING *)
202
- SELECT` opens exactly like a read.
242
+ SELECT` opens exactly like a read.
203
243
  - The plan is collected before the span opens, so the extra round trip stays out
204
244
  of the duration the span reports.
205
245
  - A failed `EXPLAIN` is swallowed. The query still runs and still gets its span.
@@ -188,6 +188,7 @@ function resolveConfig(config) {
188
188
  peerPort: config?.peerPort,
189
189
  tracerProvider: config?.tracerProvider,
190
190
  semconv: config?.semconv ?? semconvFromEnvironment(),
191
+ spanNaming: config?.spanNaming ?? "drizzle",
191
192
  traceTransactions: config?.traceTransactions ?? true,
192
193
  explain: (config?.dbSystem ?? DEFAULT_DB_SYSTEM) === "postgresql" ? config?.explain ?? false : false
193
194
  };
@@ -303,9 +304,17 @@ async function collectPlan(state, client, queryText, params) {
303
304
  return;
304
305
  }
305
306
  }
307
+ function buildSpanName(mode, operation, table) {
308
+ if (mode === "semconv") {
309
+ if (operation === void 0) return "query";
310
+ return table === void 0 ? operation : `${operation} ${table}`;
311
+ }
312
+ return operation ? `drizzle.${operation.toLowerCase()}` : "drizzle.query";
313
+ }
306
314
  function buildSpan(state, queryText, extraAttributes) {
307
315
  const operation = queryText ? extractOperation(queryText) : void 0;
308
- const spanName = operation ? `drizzle.${operation.toLowerCase()}` : "drizzle.query";
316
+ const table = queryText ? extractTableName(queryText) : void 0;
317
+ const spanName = buildSpanName(state.config.spanNaming, operation, table);
309
318
  const span = state.tracer.startSpan(spanName, { kind: SpanKind.CLIENT });
310
319
  const { semconv } = state.config;
311
320
  const legacy = semconv !== "stable";
@@ -319,7 +328,6 @@ function buildSpan(state, queryText, extraAttributes) {
319
328
  if (state.config.dbName !== void 0) setRenamed(SEMATTRS_DB_NAME, SEMATTRS_DB_NAMESPACE, state.config.dbName);
320
329
  if (queryText !== void 0) {
321
330
  span.setAttribute(SEMATTRS_DB_STATEMENT_HASH, hashQueryText(queryText));
322
- const table = extractTableName(queryText);
323
331
  if (table !== void 0) span.setAttribute(SEMATTRS_DB_COLLECTION_NAME, table);
324
332
  }
325
333
  if (state.config.captureQueryText && queryText !== void 0) setRenamed(SEMATTRS_DB_STATEMENT, SEMATTRS_DB_QUERY_TEXT, sanitizeQueryText(queryText, state.config.maxQueryTextLength));
@@ -188,6 +188,7 @@ function resolveConfig(config) {
188
188
  peerPort: config?.peerPort,
189
189
  tracerProvider: config?.tracerProvider,
190
190
  semconv: config?.semconv ?? semconvFromEnvironment(),
191
+ spanNaming: config?.spanNaming ?? "drizzle",
191
192
  traceTransactions: config?.traceTransactions ?? true,
192
193
  explain: (config?.dbSystem ?? DEFAULT_DB_SYSTEM) === "postgresql" ? config?.explain ?? false : false
193
194
  };
@@ -303,9 +304,17 @@ async function collectPlan(state, client, queryText, params) {
303
304
  return;
304
305
  }
305
306
  }
307
+ function buildSpanName(mode, operation, table) {
308
+ if (mode === "semconv") {
309
+ if (operation === void 0) return "query";
310
+ return table === void 0 ? operation : `${operation} ${table}`;
311
+ }
312
+ return operation ? `drizzle.${operation.toLowerCase()}` : "drizzle.query";
313
+ }
306
314
  function buildSpan(state, queryText, extraAttributes) {
307
315
  const operation = queryText ? extractOperation(queryText) : void 0;
308
- const spanName = operation ? `drizzle.${operation.toLowerCase()}` : "drizzle.query";
316
+ const table = queryText ? extractTableName(queryText) : void 0;
317
+ const spanName = buildSpanName(state.config.spanNaming, operation, table);
309
318
  const span = state.tracer.startSpan(spanName, { kind: _opentelemetry_api.SpanKind.CLIENT });
310
319
  const { semconv } = state.config;
311
320
  const legacy = semconv !== "stable";
@@ -319,7 +328,6 @@ function buildSpan(state, queryText, extraAttributes) {
319
328
  if (state.config.dbName !== void 0) setRenamed(SEMATTRS_DB_NAME, SEMATTRS_DB_NAMESPACE, state.config.dbName);
320
329
  if (queryText !== void 0) {
321
330
  span.setAttribute(SEMATTRS_DB_STATEMENT_HASH, hashQueryText(queryText));
322
- const table = extractTableName(queryText);
323
331
  if (table !== void 0) span.setAttribute(SEMATTRS_DB_COLLECTION_NAME, table);
324
332
  }
325
333
  if (state.config.captureQueryText && queryText !== void 0) setRenamed(SEMATTRS_DB_STATEMENT, SEMATTRS_DB_QUERY_TEXT, sanitizeQueryText(queryText, state.config.maxQueryTextLength));
package/dist/drizzle.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_drizzle = require("./drizzle-BzeOQKBq.cjs");
2
+ const require_drizzle = require("./drizzle-xrFydm_L.cjs");
3
3
  exports.instrumentDrizzle = require_drizzle.instrumentDrizzle;
4
4
  exports.instrumentDrizzleClient = require_drizzle.instrumentDrizzleClient;
@@ -30,6 +30,14 @@ type ExplainMode = 'plan' | 'analyze';
30
30
  * `database/dup` selects `'dup'`.
31
31
  */
32
32
  type SemconvMode = 'legacy' | 'stable' | 'dup';
33
+ /**
34
+ * How a query span is named.
35
+ *
36
+ * - `'drizzle'` names it after the library, `drizzle.select`.
37
+ * - `'semconv'` follows OpenTelemetry's database span convention,
38
+ * `{db.operation.name} {target}`, so `SELECT comments`.
39
+ */
40
+ type SpanNamingMode = 'drizzle' | 'semconv';
33
41
  interface InstrumentDrizzleConfig {
34
42
  tracerName?: string;
35
43
  dbSystem?: string;
@@ -50,6 +58,12 @@ interface InstrumentDrizzleConfig {
50
58
  * unset.
51
59
  */
52
60
  semconv?: SemconvMode;
61
+ /**
62
+ * How query spans are named. Defaults to `'drizzle'`, which keeps the
63
+ * `drizzle.select` names existing dashboards and saved queries are built on.
64
+ * `'semconv'` switches to the OpenTelemetry convention, `SELECT comments`.
65
+ */
66
+ spanNaming?: SpanNamingMode;
53
67
  /**
54
68
  * Where spans are created. Defaults to the globally registered provider,
55
69
  * which is what an application wants. Pass one to send this client's spans
@@ -67,4 +81,4 @@ interface InstrumentDrizzleConfig {
67
81
  declare function instrumentDrizzle<TClient>(client: TClient, config?: InstrumentDrizzleConfig): TClient;
68
82
  declare function instrumentDrizzleClient<TDb>(db: TDb, config?: InstrumentDrizzleConfig): TDb;
69
83
  //#endregion
70
- export { ExplainMode, InstrumentDrizzleConfig, SemconvMode, instrumentDrizzle, instrumentDrizzleClient };
84
+ export { ExplainMode, InstrumentDrizzleConfig, SemconvMode, SpanNamingMode, instrumentDrizzle, instrumentDrizzleClient };
package/dist/drizzle.d.ts CHANGED
@@ -30,6 +30,14 @@ type ExplainMode = 'plan' | 'analyze';
30
30
  * `database/dup` selects `'dup'`.
31
31
  */
32
32
  type SemconvMode = 'legacy' | 'stable' | 'dup';
33
+ /**
34
+ * How a query span is named.
35
+ *
36
+ * - `'drizzle'` names it after the library, `drizzle.select`.
37
+ * - `'semconv'` follows OpenTelemetry's database span convention,
38
+ * `{db.operation.name} {target}`, so `SELECT comments`.
39
+ */
40
+ type SpanNamingMode = 'drizzle' | 'semconv';
33
41
  interface InstrumentDrizzleConfig {
34
42
  tracerName?: string;
35
43
  dbSystem?: string;
@@ -50,6 +58,12 @@ interface InstrumentDrizzleConfig {
50
58
  * unset.
51
59
  */
52
60
  semconv?: SemconvMode;
61
+ /**
62
+ * How query spans are named. Defaults to `'drizzle'`, which keeps the
63
+ * `drizzle.select` names existing dashboards and saved queries are built on.
64
+ * `'semconv'` switches to the OpenTelemetry convention, `SELECT comments`.
65
+ */
66
+ spanNaming?: SpanNamingMode;
53
67
  /**
54
68
  * Where spans are created. Defaults to the globally registered provider,
55
69
  * which is what an application wants. Pass one to send this client's spans
@@ -67,4 +81,4 @@ interface InstrumentDrizzleConfig {
67
81
  declare function instrumentDrizzle<TClient>(client: TClient, config?: InstrumentDrizzleConfig): TClient;
68
82
  declare function instrumentDrizzleClient<TDb>(db: TDb, config?: InstrumentDrizzleConfig): TDb;
69
83
  //#endregion
70
- export { ExplainMode, InstrumentDrizzleConfig, SemconvMode, instrumentDrizzle, instrumentDrizzleClient };
84
+ export { ExplainMode, InstrumentDrizzleConfig, SemconvMode, SpanNamingMode, instrumentDrizzle, instrumentDrizzleClient };
package/dist/drizzle.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as instrumentDrizzleClient, t as instrumentDrizzle } from "./drizzle-C3xQFzy2.js";
1
+ import { n as instrumentDrizzleClient, t as instrumentDrizzle } from "./drizzle-BPYatG7J.js";
2
2
  export { instrumentDrizzle, instrumentDrizzleClient };
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_drizzle = require("./drizzle-BzeOQKBq.cjs");
2
+ const require_drizzle = require("./drizzle-xrFydm_L.cjs");
3
3
  exports.SEMATTRS_DB_COLLECTION_NAME = require_drizzle.SEMATTRS_DB_COLLECTION_NAME;
4
4
  exports.SEMATTRS_DB_NAME = require_drizzle.SEMATTRS_DB_NAME;
5
5
  exports.SEMATTRS_DB_NAMESPACE = require_drizzle.SEMATTRS_DB_NAMESPACE;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as SEMATTRS_NET_PEER_NAME, S as SEMATTRS_GCP_BIGQUERY_STATEMENT_TYPE, _ as SEMATTRS_GCP_BIGQUERY_QUERY_HASH, a as SEMATTRS_DB_NAMESPACE, b as SEMATTRS_GCP_BIGQUERY_SCHEMA_FIELDS, c as SEMATTRS_DB_QUERY_SUMMARY, d as SEMATTRS_DB_SYSTEM, f as SEMATTRS_DB_SYSTEM_NAME, g as SEMATTRS_GCP_BIGQUERY_PROJECT_ID, h as SEMATTRS_GCP_BIGQUERY_JOB_LOCATION, i as SEMATTRS_DB_NAME, l as SEMATTRS_DB_QUERY_TEXT, m as SEMATTRS_GCP_BIGQUERY_JOB_ID, n as instrumentDrizzleClient, o as SEMATTRS_DB_OPERATION, p as SEMATTRS_GCP_BIGQUERY_DESTINATION_TABLE, r as SEMATTRS_DB_COLLECTION_NAME, s as SEMATTRS_DB_OPERATION_NAME, t as instrumentDrizzle, u as SEMATTRS_DB_STATEMENT, v as SEMATTRS_GCP_BIGQUERY_ROWS_AFFECTED, w as SEMATTRS_NET_PEER_PORT, x as SEMATTRS_GCP_BIGQUERY_SOURCE_TABLES, y as SEMATTRS_GCP_BIGQUERY_ROWS_RETURNED } from "./drizzle-C3xQFzy2.js";
1
+ import { C as SEMATTRS_NET_PEER_NAME, S as SEMATTRS_GCP_BIGQUERY_STATEMENT_TYPE, _ as SEMATTRS_GCP_BIGQUERY_QUERY_HASH, a as SEMATTRS_DB_NAMESPACE, b as SEMATTRS_GCP_BIGQUERY_SCHEMA_FIELDS, c as SEMATTRS_DB_QUERY_SUMMARY, d as SEMATTRS_DB_SYSTEM, f as SEMATTRS_DB_SYSTEM_NAME, g as SEMATTRS_GCP_BIGQUERY_PROJECT_ID, h as SEMATTRS_GCP_BIGQUERY_JOB_LOCATION, i as SEMATTRS_DB_NAME, l as SEMATTRS_DB_QUERY_TEXT, m as SEMATTRS_GCP_BIGQUERY_JOB_ID, n as instrumentDrizzleClient, o as SEMATTRS_DB_OPERATION, p as SEMATTRS_GCP_BIGQUERY_DESTINATION_TABLE, r as SEMATTRS_DB_COLLECTION_NAME, s as SEMATTRS_DB_OPERATION_NAME, t as instrumentDrizzle, u as SEMATTRS_DB_STATEMENT, v as SEMATTRS_GCP_BIGQUERY_ROWS_AFFECTED, w as SEMATTRS_NET_PEER_PORT, x as SEMATTRS_GCP_BIGQUERY_SOURCE_TABLES, y as SEMATTRS_GCP_BIGQUERY_ROWS_RETURNED } from "./drizzle-BPYatG7J.js";
2
2
  export { SEMATTRS_DB_COLLECTION_NAME, SEMATTRS_DB_NAME, SEMATTRS_DB_NAMESPACE, SEMATTRS_DB_OPERATION, SEMATTRS_DB_OPERATION_NAME, SEMATTRS_DB_QUERY_SUMMARY, SEMATTRS_DB_QUERY_TEXT, SEMATTRS_DB_STATEMENT, SEMATTRS_DB_SYSTEM, SEMATTRS_DB_SYSTEM_NAME, SEMATTRS_GCP_BIGQUERY_DESTINATION_TABLE, SEMATTRS_GCP_BIGQUERY_JOB_ID, SEMATTRS_GCP_BIGQUERY_JOB_LOCATION, SEMATTRS_GCP_BIGQUERY_PROJECT_ID, SEMATTRS_GCP_BIGQUERY_QUERY_HASH, SEMATTRS_GCP_BIGQUERY_ROWS_AFFECTED, SEMATTRS_GCP_BIGQUERY_ROWS_RETURNED, SEMATTRS_GCP_BIGQUERY_SCHEMA_FIELDS, SEMATTRS_GCP_BIGQUERY_SOURCE_TABLES, SEMATTRS_GCP_BIGQUERY_STATEMENT_TYPE, SEMATTRS_NET_PEER_NAME, SEMATTRS_NET_PEER_PORT, instrumentDrizzle, instrumentDrizzleClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-drizzle",
3
- "version": "0.0.53",
3
+ "version": "0.1.0",
4
4
  "description": "OpenTelemetry instrumentation for Drizzle ORM",
5
5
  "type": "module",
6
6
  "main": "./dist/drizzle.js",
@@ -35,7 +35,7 @@
35
35
  "@opentelemetry/api": "^1.9.1",
36
36
  "@opentelemetry/instrumentation": "^0.221.0",
37
37
  "@opentelemetry/semantic-conventions": "^1.43.0",
38
- "autotel": "7.0.1"
38
+ "autotel": "7.2.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "drizzle-orm": ">=0.45.2"