autotel-drizzle 0.2.6 → 0.3.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
@@ -156,8 +156,8 @@ individually slow. Group a trace's spans by it:
156
156
 
157
157
  | `db.statement.hash` | `db.collection.name` | Spans |
158
158
  | ------------------- | -------------------- | ----: |
159
- | `6b863b41773053ba` | `comments` | 200 |
160
- | `53ee56739d371a4b` | `posts` | 1 |
159
+ | `16f2c9a04be7d1` | `comments` | 200 |
160
+ | `0a5e83d1c96b47` | `posts` | 1 |
161
161
 
162
162
  One statement, run 200 times. Asking which span was slowest gives you a 1.7%
163
163
  answer and sends you off to index a table that was never the problem. The
@@ -205,24 +205,28 @@ instrumentDrizzleClient(db, {
205
205
 
206
206
  The same query, before and after `CREATE INDEX`:
207
207
 
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) |
208
+ | Attribute | Before | After |
209
+ | ------------------------ | ---------------- | -------------------------------- |
210
+ | `db.statement.hash` | `0c4a9f1e2b7d33` | `0c4a9f1e2b7d33` |
211
+ | `db.plan.hash` | `1d07e5a9c4b2f8` | `08f3b6e1a7c5d2` |
212
+ | `db.plan.node` | `Seq Scan` | `Bitmap Heap Scan` |
213
+ | `db.plan.indexes` | | `idx_comments_post_id` |
214
+ | `db.plan.full_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) |
221
221
 
222
222
  An unchanged statement hash beside a changed plan hash is a planner decision.
223
223
  Grouping `db.plan.indexes` by `db.statement.hash` answers which queries an index
224
224
  is serving.
225
225
 
226
+ The attribute names and the hash come from `autotel-db`, which `autotel-mongodb`
227
+ uses too, so a Postgres `Seq Scan` and a MongoDB `COLLSCAN` both read as
228
+ `db.plan.full_scan: true` and one query can group them.
229
+
226
230
  `rows_examined` counts at the leaves of the plan, and counts the rows a scan
227
231
  read and discarded. A parent node reports the rows its children handed up, so
228
232
  summing every level counts one row per level; and the rows an index saves you
@@ -237,15 +241,50 @@ second time to measure it. Both are off by default. Use them in development, in
237
241
  CI, or behind a sample of production traffic.
238
242
 
239
243
  - `'analyze'` runs on read-only statements only, so a traced insert never runs
240
- twice. A leading `WITH` qualifies only when no writing keyword appears
241
- anywhere in the statement, because `WITH gone AS (DELETE ... RETURNING *)
242
- SELECT` opens exactly like a read.
243
- - The plan is collected before the span opens, so the extra round trip stays out
244
- of the duration the span reports.
245
- - A failed `EXPLAIN` is swallowed. The query still runs and still gets its span.
246
- - Ignored unless `dbSystem` is `'postgresql'`.
247
- - Inside a transaction the plan is taken on the transaction's own connection, so
248
- it sees the rows the query will see.
244
+ twice. A write is planned with a plain `EXPLAIN` instead, which runs nothing,
245
+ and its span records `db.plan.mode: plan`. A leading `WITH` counts as a read
246
+ only when no writing keyword appears anywhere in the statement, because
247
+ `WITH gone AS (DELETE ... RETURNING *) SELECT` opens exactly like a read.
248
+ - Outside a transaction, the plan is collected before the span opens, so the
249
+ extra round trip stays out of the duration the span reports.
250
+ - Only statements Postgres can explain are sent (`SELECT`, `WITH`, `INSERT`,
251
+ `UPDATE`, `DELETE`, `MERGE`, `VALUES`, `TABLE`). DDL and the rest record
252
+ `db.plan.status: unsupported` and send nothing.
253
+ - **Inside `db.transaction()`, nothing is ever sent on the transaction's own
254
+ connection, and nothing the application sends is held back.** An `EXPLAIN`
255
+ there could abort the transaction, and waiting for a plan before a
256
+ statement reorders the transaction's statements against each other and
257
+ against its `COMMIT` or `ROLLBACK`. So each statement goes out exactly when
258
+ it would without instrumentation, and its plan is fetched alongside it from
259
+ a separate pooled connection, inside a transaction that is always rolled
260
+ back, with a 100ms lock timeout (the application's transaction may hold
261
+ locks the `EXPLAIN` needs). The span ends at the statement's own end time.
262
+ - `drizzle(pool)` (node-postgres `Pool`): plans are captured this way. A
263
+ statement whose table the transaction has locked records
264
+ `db.plan.status: failed` with the lock timeout, and is not slowed.
265
+ - `drizzle(client)`, an embedded database, or any other driver: there is no
266
+ separate connection, so statements inside a transaction record
267
+ `db.plan.status: unsupported`. The same statements outside a transaction
268
+ are planned as usual.
269
+ - A transaction is recognised by its connection, not by which session issued
270
+ the statement, so this holds for `drizzle(client)` (where the transaction
271
+ reuses the session it was given) and for nested transactions. Open
272
+ transactions through `db.transaction()` when explain is on.
273
+ - Explains outside a transaction through a node-postgres `Pool` check a
274
+ connection out and hand it back clean, so a failing `EXPLAIN` (a missing
275
+ privilege) never costs a pooled connection.
276
+ - A failed `EXPLAIN` never fails the query. The span records
277
+ `db.plan.status: failed` and the reason in `db.plan.error`, with any quoted
278
+ values, documents and key details stripped from it.
279
+ - Postgres only. With another `dbSystem`, spans record
280
+ `db.plan.status: unsupported` rather than reading as if explain were off.
281
+ - Plans also carry `db.plan.stages`, `db.plan.mode`, and
282
+ `db.plan.blocking_sort` (a `Sort` node).
283
+ - Inside a transaction the plan comes from a separate pooled connection, so it
284
+ cannot see the transaction's uncommitted changes: rows it has inserted or
285
+ deleted, tables it has created. The plan describes the query against the
286
+ committed data, and a statement on a table the transaction created records
287
+ `db.plan.status: failed`.
249
288
 
250
289
  ## Security Considerations
251
290
 
@@ -1,6 +1,6 @@
1
- import { createHash } from "node:crypto";
2
- import { SpanKind, trace } from "@opentelemetry/api";
1
+ import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
3
2
  import { finalizeSpan, runWithSpan } from "autotel/trace-helpers";
3
+ import { hashStatement, planAttributes, planUnavailableAttributes } from "autotel-db";
4
4
  //#region src/common/constants.ts
5
5
  const SEMATTRS_DB_SYSTEM = "db.system";
6
6
  const SEMATTRS_DB_SYSTEM_NAME = "db.system.name";
@@ -37,6 +37,32 @@ function hasMethod(target, name) {
37
37
  function asFunction(value) {
38
38
  return typeof value === "function" ? value : void 0;
39
39
  }
40
+ function isPgPool(value) {
41
+ return asFunction(value.connect) !== void 0 && typeof value.totalCount === "number";
42
+ }
43
+ function isolatedExplainClient(value) {
44
+ if (!isDriverObject(value) || !isPgPool(value)) return void 0;
45
+ const connect = asFunction(value.connect);
46
+ return { query: async (statement, params) => {
47
+ const connection = asDriverObject(await connect.call(value));
48
+ const run = connection && asFunction(connection.query);
49
+ if (connection === void 0 || run === void 0) throw new Error("the pool handed back no client");
50
+ const release = asFunction(connection.release);
51
+ try {
52
+ await run.call(connection, "BEGIN", []);
53
+ await run.call(connection, "SET LOCAL lock_timeout = '100ms'", []);
54
+ await run.call(connection, "SET LOCAL statement_timeout = '5s'", []);
55
+ return await run.call(connection, statement, params);
56
+ } finally {
57
+ try {
58
+ await run.call(connection, "ROLLBACK", []);
59
+ release?.call(connection);
60
+ } catch (error) {
61
+ release?.call(connection, error instanceof Error ? error.message : String(error));
62
+ }
63
+ }
64
+ } };
65
+ }
40
66
  function isDriverObject(value) {
41
67
  return value !== null && typeof value === "object";
42
68
  }
@@ -74,6 +100,17 @@ function asQueryClient(value) {
74
100
  if (!isDriverObject(value)) return;
75
101
  const run = asFunction(value.query);
76
102
  if (run === void 0) return;
103
+ const connect = asFunction(value.connect);
104
+ if (connect !== void 0 && isPgPool(value)) return { query: async (statement, params) => {
105
+ const connection = asDriverObject(await connect.call(value));
106
+ const checkedOut = connection && asFunction(connection.query);
107
+ if (connection === void 0 || checkedOut === void 0) throw new Error("the pool handed back no client");
108
+ try {
109
+ return await checkedOut.call(connection, statement, params);
110
+ } finally {
111
+ asFunction(connection.release)?.call(connection);
112
+ }
113
+ } };
77
114
  return { query: async (statement, params) => run.call(value, statement, params) };
78
115
  }
79
116
  function toCount(value) {
@@ -130,16 +167,6 @@ function parseExplainResponse(result) {
130
167
  }
131
168
  //#endregion
132
169
  //#region src/drizzle/index.ts
133
- const SEMATTRS_DB_PLAN_NODE = "db.plan.node";
134
- const SEMATTRS_DB_PLAN_INDEXES = "db.plan.indexes";
135
- const SEMATTRS_DB_PLAN_COST = "db.plan.cost";
136
- const SEMATTRS_DB_PLAN_ROWS_ESTIMATED = "db.plan.rows_estimated";
137
- const SEMATTRS_DB_PLAN_ROWS_EXAMINED = "db.plan.rows_examined";
138
- const SEMATTRS_DB_PLAN_ROWS_RETURNED = "db.plan.rows_returned";
139
- const SEMATTRS_DB_PLAN_BLOCKS = "db.plan.blocks";
140
- const SEMATTRS_DB_PLAN_EXECUTION_MS = "db.plan.execution_ms";
141
- const SEMATTRS_DB_PLAN_SEQ_SCAN = "db.plan.seq_scan";
142
- const SEMATTRS_DB_PLAN_HASH = "db.plan.hash";
143
170
  const TRANSACTION_ATTRIBUTE = "db.transaction";
144
171
  const REENTRY_FLAG = `__autotelDrizzleInstrumented:running`;
145
172
  const DEFAULT_TRACER_NAME = "autotel-plugins/drizzle";
@@ -190,7 +217,8 @@ function resolveConfig(config) {
190
217
  semconv: config?.semconv ?? semconvFromEnvironment(),
191
218
  spanNaming: config?.spanNaming ?? "drizzle",
192
219
  traceTransactions: config?.traceTransactions ?? true,
193
- explain: (config?.dbSystem ?? DEFAULT_DB_SYSTEM) === "postgresql" ? config?.explain ?? false : false
220
+ explain: (config?.dbSystem ?? DEFAULT_DB_SYSTEM) === "postgresql" ? config?.explain ?? false : false,
221
+ explainRequested: config?.explain ?? false
194
222
  };
195
223
  }
196
224
  function getState(config) {
@@ -235,9 +263,6 @@ function sanitizeQueryText(queryText, maxLength) {
235
263
  if (queryText.length <= maxLength) return queryText;
236
264
  return `${queryText.slice(0, Math.max(0, maxLength))}...`;
237
265
  }
238
- function hashQueryText(queryText) {
239
- return createHash("sha1").update(queryText).digest("hex").slice(0, 16);
240
- }
241
266
  function extractOperation(queryText) {
242
267
  const trimmed = queryText.trimStart();
243
268
  return /^(?<operation>\w+)/u.exec(trimmed)?.groups?.operation?.toUpperCase();
@@ -252,21 +277,28 @@ const WRITE_KEYWORDS = /\b(?:INSERT|UPDATE|DELETE|MERGE|TRUNCATE|DROP|ALTER|CREA
252
277
  function buildExplainStatement(mode, queryText) {
253
278
  return `EXPLAIN (${mode === "analyze" ? "ANALYZE, BUFFERS, FORMAT JSON" : "FORMAT JSON"}) ${queryText}`;
254
279
  }
255
- function planAttributes(payload, mode) {
256
- const attributes = {};
280
+ const SORT_NODES = new Set(["Sort", "Incremental Sort"]);
281
+ function planAttributes$1(payload, mode) {
257
282
  const [root] = payload.nodes;
258
- if (root === void 0) return attributes;
259
- const nodeTypes = [];
260
- const indexes = [];
283
+ if (root === void 0) return {};
284
+ const plan = {
285
+ nodes: [],
286
+ fullScan: false,
287
+ blockingSort: false,
288
+ mode,
289
+ indexes: [],
290
+ cost: root.totalCost,
291
+ rowsEstimated: root.planRows
292
+ };
261
293
  let rowsExamined = 0;
262
294
  let blocks = 0;
263
- let sawSeqScan = false;
264
295
  for (const node of payload.nodes) {
265
296
  if (node.nodeType !== void 0) {
266
- nodeTypes.push(node.nodeType);
267
- sawSeqScan ||= node.nodeType === "Seq Scan";
297
+ plan.nodes.push(node.nodeType);
298
+ plan.fullScan ||= node.nodeType === "Seq Scan";
299
+ plan.blockingSort ||= SORT_NODES.has(node.nodeType);
268
300
  }
269
- if (node.indexName !== void 0 && !indexes.includes(node.indexName)) indexes.push(node.indexName);
301
+ if (node.indexName !== void 0 && !plan.indexes.includes(node.indexName)) plan.indexes.push(node.indexName);
270
302
  if (node.isLeaf) {
271
303
  rowsExamined += node.actualRows;
272
304
  rowsExamined += node.rowsRemovedByFilter;
@@ -275,33 +307,86 @@ function planAttributes(payload, mode) {
275
307
  blocks += node.sharedHitBlocks;
276
308
  blocks += node.sharedReadBlocks;
277
309
  }
278
- if (root.nodeType !== void 0) attributes[SEMATTRS_DB_PLAN_NODE] = root.nodeType;
279
- if (root.totalCost !== void 0) attributes[SEMATTRS_DB_PLAN_COST] = root.totalCost;
280
- if (root.planRows !== void 0) attributes[SEMATTRS_DB_PLAN_ROWS_ESTIMATED] = root.planRows;
281
- if (indexes.length > 0) attributes[SEMATTRS_DB_PLAN_INDEXES] = indexes.join(",");
282
- attributes[SEMATTRS_DB_PLAN_SEQ_SCAN] = sawSeqScan;
283
- attributes[SEMATTRS_DB_PLAN_HASH] = hashQueryText(nodeTypes.join(">"));
284
310
  if (mode === "analyze") {
285
- attributes[SEMATTRS_DB_PLAN_ROWS_EXAMINED] = rowsExamined;
286
- attributes[SEMATTRS_DB_PLAN_ROWS_RETURNED] = root.actualRows;
287
- attributes[SEMATTRS_DB_PLAN_BLOCKS] = blocks;
288
- if (payload.executionMs !== void 0) attributes[SEMATTRS_DB_PLAN_EXECUTION_MS] = payload.executionMs;
311
+ plan.rowsExamined = rowsExamined;
312
+ plan.rowsReturned = root.actualRows;
313
+ plan.blocks = blocks;
314
+ plan.executionMs = payload.executionMs;
289
315
  }
290
- return attributes;
316
+ return planAttributes(plan);
291
317
  }
292
318
  function resolveExplainClient(target, state) {
293
- return readQueryClient(target) ?? state.explainClient;
319
+ const own = readQueryClient(target);
320
+ if (own && isDriverObject(target)) return {
321
+ client: own,
322
+ connection: readObject(target, "client")
323
+ };
324
+ return state.explainClient ? { client: state.explainClient } : void 0;
294
325
  }
295
- async function collectPlan(state, client, queryText, params) {
296
- const mode = state.config.explain;
297
- if (!mode || client === void 0) return;
298
- if (mode === "analyze" && !isReadOnlyStatement(queryText)) return;
326
+ const openTransactions = new WeakMap();
327
+ function transactionConnections(tx) {
328
+ if (!isDriverObject(tx)) return [];
329
+ const found = [];
330
+ for (const holder of [tx, ...readSessions(tx)]) {
331
+ const connection = readObject(holder, "client");
332
+ if (connection !== void 0 && !found.includes(connection)) found.push(connection);
333
+ }
334
+ return found;
335
+ }
336
+ function openTransaction(connections) {
337
+ for (const connection of connections) openTransactions.set(connection, (openTransactions.get(connection) ?? 0) + 1);
338
+ return () => {
339
+ for (const connection of connections) {
340
+ const depth = (openTransactions.get(connection) ?? 1) - 1;
341
+ if (depth > 0) openTransactions.set(connection, depth);
342
+ else openTransactions.delete(connection);
343
+ }
344
+ };
345
+ }
346
+ function thenAfter(run, after) {
347
+ let result;
348
+ try {
349
+ result = run();
350
+ } catch (error) {
351
+ after();
352
+ throw error;
353
+ }
354
+ if (isThenable(result)) return Promise.resolve(result).finally(after);
355
+ after();
356
+ return result;
357
+ }
358
+ const EXPLAINABLE = new Set([
359
+ "SELECT",
360
+ "WITH",
361
+ "INSERT",
362
+ "UPDATE",
363
+ "DELETE",
364
+ "MERGE",
365
+ "VALUES",
366
+ "TABLE"
367
+ ]);
368
+ async function collectPlan(state, explainClient, queryText, params, inTransaction) {
369
+ const requested = state.config.explain;
370
+ if (!requested) return;
371
+ if (!EXPLAINABLE.has(extractOperation(queryText) ?? "")) return planUnavailableAttributes("unsupported", { mode: requested });
372
+ const mode = requested === "analyze" && !isReadOnlyStatement(queryText) ? "plan" : requested;
373
+ const client = inTransaction ? state.isolatedExplainClient : explainClient?.client;
374
+ if (client === void 0) return inTransaction ? planUnavailableAttributes("unsupported", { mode }) : planUnavailableAttributes("failed", {
375
+ mode,
376
+ error: "no client to run EXPLAIN on"
377
+ });
299
378
  try {
300
379
  const payload = parseExplainResponse(await client.query(buildExplainStatement(mode, queryText), readParams(params)));
301
- if (payload === void 0) return;
302
- return planAttributes(payload, mode);
303
- } catch {
304
- return;
380
+ if (payload === void 0) return planUnavailableAttributes("failed", {
381
+ mode,
382
+ error: "EXPLAIN returned no plan autotel-drizzle reads"
383
+ });
384
+ return planAttributes$1(payload, mode);
385
+ } catch (error) {
386
+ return planUnavailableAttributes("failed", {
387
+ mode,
388
+ error: error instanceof Error ? error.message : String(error)
389
+ });
305
390
  }
306
391
  }
307
392
  function buildSpanName(mode, operation, table) {
@@ -327,7 +412,7 @@ function buildSpan(state, queryText, extraAttributes) {
327
412
  if (operation) setRenamed(SEMATTRS_DB_OPERATION, SEMATTRS_DB_OPERATION_NAME, operation);
328
413
  if (state.config.dbName !== void 0) setRenamed(SEMATTRS_DB_NAME, SEMATTRS_DB_NAMESPACE, state.config.dbName);
329
414
  if (queryText !== void 0) {
330
- span.setAttribute(SEMATTRS_DB_STATEMENT_HASH, hashQueryText(queryText));
415
+ span.setAttribute(SEMATTRS_DB_STATEMENT_HASH, hashStatement(queryText));
331
416
  if (table !== void 0) span.setAttribute(SEMATTRS_DB_COLLECTION_NAME, table);
332
417
  }
333
418
  if (state.config.captureQueryText && queryText !== void 0) {
@@ -339,6 +424,37 @@ function buildSpan(state, queryText, extraAttributes) {
339
424
  if (extraAttributes) for (const [key, value] of Object.entries(extraAttributes)) span.setAttribute(key, value);
340
425
  return span;
341
426
  }
427
+ function runAlongsidePlan(span, planned, fn) {
428
+ const end = (error) => {
429
+ const endTime = performance.timeOrigin + performance.now();
430
+ planned.then((attributes) => {
431
+ if (attributes) span.setAttributes(attributes);
432
+ }).finally(() => {
433
+ if (error) {
434
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
435
+ span.setStatus({ code: SpanStatusCode.ERROR });
436
+ } else span.setStatus({ code: SpanStatusCode.OK });
437
+ span.end(endTime);
438
+ });
439
+ };
440
+ return runWithSpan(span, () => {
441
+ try {
442
+ const result = fn();
443
+ if (isThenable(result)) return result.then((value) => {
444
+ end();
445
+ return value;
446
+ }, (error) => {
447
+ end(error);
448
+ throw error;
449
+ });
450
+ end();
451
+ return result;
452
+ } catch (error) {
453
+ end(error);
454
+ throw error;
455
+ }
456
+ });
457
+ }
342
458
  function executeWithSpan(span, fn) {
343
459
  return runWithSpan(span, () => {
344
460
  try {
@@ -369,11 +485,20 @@ function instrumentMethod(target, methodName, state, options) {
369
485
  const callback = trailing === void 0 ? void 0 : (args.pop(), trailing);
370
486
  if (options.guardReentry && target[REENTRY_FLAG]) return originalMethod.call(this, ...incomingArgs);
371
487
  const queryText = options.queryText(args);
372
- if (state.config.explain && queryText !== void 0 && !callback) return collectPlan(state, resolveExplainClient(readSelf(this), state), queryText, options.explainParams?.(args)).then((planAttributes) => executeWithSpan(buildSpan(state, queryText, {
488
+ if (state.config.explain && queryText !== void 0 && !callback) {
489
+ const explainClient = resolveExplainClient(readSelf(this), state);
490
+ const inTransaction = options.extraAttributes?.[TRANSACTION_ATTRIBUTE] === true || explainClient?.connection !== void 0 && openTransactions.has(explainClient.connection);
491
+ if (inTransaction) return runAlongsidePlan(buildSpan(state, queryText, options.extraAttributes), collectPlan(state, explainClient, queryText, options.explainParams?.(args), true), () => originalMethod.call(this, ...args));
492
+ return collectPlan(state, explainClient, queryText, options.explainParams?.(args), inTransaction).then((planAttributes) => executeWithSpan(buildSpan(state, queryText, {
493
+ ...options.extraAttributes,
494
+ ...planAttributes
495
+ }), () => originalMethod.call(this, ...args)));
496
+ }
497
+ const unexplained = state.config.explainRequested && queryText !== void 0 ? planUnavailableAttributes("unsupported", { mode: state.config.explainRequested }) : void 0;
498
+ const span = buildSpan(state, queryText, {
373
499
  ...options.extraAttributes,
374
- ...planAttributes
375
- }), () => originalMethod.call(this, ...args)));
376
- const span = buildSpan(state, queryText, options.extraAttributes);
500
+ ...unexplained
501
+ });
377
502
  if (callback) return runWithSpan(span, () => {
378
503
  const wrappedCallback = (error, result) => {
379
504
  finalizeSpan(span, error);
@@ -479,11 +604,15 @@ function instrumentSession(session, state, allowDirectMethods = true) {
479
604
  const [rawCallback, ...restArgs] = transactionArgs;
480
605
  const callback = asFunction(rawCallback);
481
606
  if (callback === void 0) return originalTransaction.call(this, ...transactionArgs);
607
+ const closers = [];
482
608
  const wrappedCallback = (tx, ...callbackArgs) => {
483
609
  instrumentTransactionTarget(tx, state);
610
+ closers.push(openTransaction(transactionConnections(tx)));
484
611
  return callback.call(this, tx, ...callbackArgs);
485
612
  };
486
- const runTransaction = () => originalTransaction.call(this, wrappedCallback, ...restArgs);
613
+ const runTransaction = () => thenAfter(() => originalTransaction.call(this, wrappedCallback, ...restArgs), () => {
614
+ for (const close of closers) close();
615
+ });
487
616
  if (!state.config.traceTransactions) return runTransaction();
488
617
  const span = state.tracer.startSpan("drizzle.transaction", { kind: SpanKind.CLIENT });
489
618
  span.setAttribute(TRANSACTION_ATTRIBUTE, true);
@@ -523,6 +652,7 @@ function instrumentDrizzleClient(db, config) {
523
652
  if (readFlag(target, INSTRUMENTED_FLAG)) return db;
524
653
  const state = getState(config);
525
654
  state.explainClient = asQueryClient(target.$client);
655
+ state.isolatedExplainClient = isolatedExplainClient(target.$client);
526
656
  let instrumented = false;
527
657
  let sessionInstrumented = false;
528
658
  for (const session of readSessions(target)) {
@@ -1,6 +1,6 @@
1
- let node_crypto = require("node:crypto");
2
1
  let _opentelemetry_api = require("@opentelemetry/api");
3
2
  let autotel_trace_helpers = require("autotel/trace-helpers");
3
+ let autotel_db = require("autotel-db");
4
4
  //#region src/common/constants.ts
5
5
  const SEMATTRS_DB_SYSTEM = "db.system";
6
6
  const SEMATTRS_DB_SYSTEM_NAME = "db.system.name";
@@ -37,6 +37,32 @@ function hasMethod(target, name) {
37
37
  function asFunction(value) {
38
38
  return typeof value === "function" ? value : void 0;
39
39
  }
40
+ function isPgPool(value) {
41
+ return asFunction(value.connect) !== void 0 && typeof value.totalCount === "number";
42
+ }
43
+ function isolatedExplainClient(value) {
44
+ if (!isDriverObject(value) || !isPgPool(value)) return void 0;
45
+ const connect = asFunction(value.connect);
46
+ return { query: async (statement, params) => {
47
+ const connection = asDriverObject(await connect.call(value));
48
+ const run = connection && asFunction(connection.query);
49
+ if (connection === void 0 || run === void 0) throw new Error("the pool handed back no client");
50
+ const release = asFunction(connection.release);
51
+ try {
52
+ await run.call(connection, "BEGIN", []);
53
+ await run.call(connection, "SET LOCAL lock_timeout = '100ms'", []);
54
+ await run.call(connection, "SET LOCAL statement_timeout = '5s'", []);
55
+ return await run.call(connection, statement, params);
56
+ } finally {
57
+ try {
58
+ await run.call(connection, "ROLLBACK", []);
59
+ release?.call(connection);
60
+ } catch (error) {
61
+ release?.call(connection, error instanceof Error ? error.message : String(error));
62
+ }
63
+ }
64
+ } };
65
+ }
40
66
  function isDriverObject(value) {
41
67
  return value !== null && typeof value === "object";
42
68
  }
@@ -74,6 +100,17 @@ function asQueryClient(value) {
74
100
  if (!isDriverObject(value)) return;
75
101
  const run = asFunction(value.query);
76
102
  if (run === void 0) return;
103
+ const connect = asFunction(value.connect);
104
+ if (connect !== void 0 && isPgPool(value)) return { query: async (statement, params) => {
105
+ const connection = asDriverObject(await connect.call(value));
106
+ const checkedOut = connection && asFunction(connection.query);
107
+ if (connection === void 0 || checkedOut === void 0) throw new Error("the pool handed back no client");
108
+ try {
109
+ return await checkedOut.call(connection, statement, params);
110
+ } finally {
111
+ asFunction(connection.release)?.call(connection);
112
+ }
113
+ } };
77
114
  return { query: async (statement, params) => run.call(value, statement, params) };
78
115
  }
79
116
  function toCount(value) {
@@ -130,16 +167,6 @@ function parseExplainResponse(result) {
130
167
  }
131
168
  //#endregion
132
169
  //#region src/drizzle/index.ts
133
- const SEMATTRS_DB_PLAN_NODE = "db.plan.node";
134
- const SEMATTRS_DB_PLAN_INDEXES = "db.plan.indexes";
135
- const SEMATTRS_DB_PLAN_COST = "db.plan.cost";
136
- const SEMATTRS_DB_PLAN_ROWS_ESTIMATED = "db.plan.rows_estimated";
137
- const SEMATTRS_DB_PLAN_ROWS_EXAMINED = "db.plan.rows_examined";
138
- const SEMATTRS_DB_PLAN_ROWS_RETURNED = "db.plan.rows_returned";
139
- const SEMATTRS_DB_PLAN_BLOCKS = "db.plan.blocks";
140
- const SEMATTRS_DB_PLAN_EXECUTION_MS = "db.plan.execution_ms";
141
- const SEMATTRS_DB_PLAN_SEQ_SCAN = "db.plan.seq_scan";
142
- const SEMATTRS_DB_PLAN_HASH = "db.plan.hash";
143
170
  const TRANSACTION_ATTRIBUTE = "db.transaction";
144
171
  const REENTRY_FLAG = `__autotelDrizzleInstrumented:running`;
145
172
  const DEFAULT_TRACER_NAME = "autotel-plugins/drizzle";
@@ -190,7 +217,8 @@ function resolveConfig(config) {
190
217
  semconv: config?.semconv ?? semconvFromEnvironment(),
191
218
  spanNaming: config?.spanNaming ?? "drizzle",
192
219
  traceTransactions: config?.traceTransactions ?? true,
193
- explain: (config?.dbSystem ?? DEFAULT_DB_SYSTEM) === "postgresql" ? config?.explain ?? false : false
220
+ explain: (config?.dbSystem ?? DEFAULT_DB_SYSTEM) === "postgresql" ? config?.explain ?? false : false,
221
+ explainRequested: config?.explain ?? false
194
222
  };
195
223
  }
196
224
  function getState(config) {
@@ -235,9 +263,6 @@ function sanitizeQueryText(queryText, maxLength) {
235
263
  if (queryText.length <= maxLength) return queryText;
236
264
  return `${queryText.slice(0, Math.max(0, maxLength))}...`;
237
265
  }
238
- function hashQueryText(queryText) {
239
- return (0, node_crypto.createHash)("sha1").update(queryText).digest("hex").slice(0, 16);
240
- }
241
266
  function extractOperation(queryText) {
242
267
  const trimmed = queryText.trimStart();
243
268
  return /^(?<operation>\w+)/u.exec(trimmed)?.groups?.operation?.toUpperCase();
@@ -252,21 +277,28 @@ const WRITE_KEYWORDS = /\b(?:INSERT|UPDATE|DELETE|MERGE|TRUNCATE|DROP|ALTER|CREA
252
277
  function buildExplainStatement(mode, queryText) {
253
278
  return `EXPLAIN (${mode === "analyze" ? "ANALYZE, BUFFERS, FORMAT JSON" : "FORMAT JSON"}) ${queryText}`;
254
279
  }
280
+ const SORT_NODES = new Set(["Sort", "Incremental Sort"]);
255
281
  function planAttributes(payload, mode) {
256
- const attributes = {};
257
282
  const [root] = payload.nodes;
258
- if (root === void 0) return attributes;
259
- const nodeTypes = [];
260
- const indexes = [];
283
+ if (root === void 0) return {};
284
+ const plan = {
285
+ nodes: [],
286
+ fullScan: false,
287
+ blockingSort: false,
288
+ mode,
289
+ indexes: [],
290
+ cost: root.totalCost,
291
+ rowsEstimated: root.planRows
292
+ };
261
293
  let rowsExamined = 0;
262
294
  let blocks = 0;
263
- let sawSeqScan = false;
264
295
  for (const node of payload.nodes) {
265
296
  if (node.nodeType !== void 0) {
266
- nodeTypes.push(node.nodeType);
267
- sawSeqScan ||= node.nodeType === "Seq Scan";
297
+ plan.nodes.push(node.nodeType);
298
+ plan.fullScan ||= node.nodeType === "Seq Scan";
299
+ plan.blockingSort ||= SORT_NODES.has(node.nodeType);
268
300
  }
269
- if (node.indexName !== void 0 && !indexes.includes(node.indexName)) indexes.push(node.indexName);
301
+ if (node.indexName !== void 0 && !plan.indexes.includes(node.indexName)) plan.indexes.push(node.indexName);
270
302
  if (node.isLeaf) {
271
303
  rowsExamined += node.actualRows;
272
304
  rowsExamined += node.rowsRemovedByFilter;
@@ -275,33 +307,86 @@ function planAttributes(payload, mode) {
275
307
  blocks += node.sharedHitBlocks;
276
308
  blocks += node.sharedReadBlocks;
277
309
  }
278
- if (root.nodeType !== void 0) attributes[SEMATTRS_DB_PLAN_NODE] = root.nodeType;
279
- if (root.totalCost !== void 0) attributes[SEMATTRS_DB_PLAN_COST] = root.totalCost;
280
- if (root.planRows !== void 0) attributes[SEMATTRS_DB_PLAN_ROWS_ESTIMATED] = root.planRows;
281
- if (indexes.length > 0) attributes[SEMATTRS_DB_PLAN_INDEXES] = indexes.join(",");
282
- attributes[SEMATTRS_DB_PLAN_SEQ_SCAN] = sawSeqScan;
283
- attributes[SEMATTRS_DB_PLAN_HASH] = hashQueryText(nodeTypes.join(">"));
284
310
  if (mode === "analyze") {
285
- attributes[SEMATTRS_DB_PLAN_ROWS_EXAMINED] = rowsExamined;
286
- attributes[SEMATTRS_DB_PLAN_ROWS_RETURNED] = root.actualRows;
287
- attributes[SEMATTRS_DB_PLAN_BLOCKS] = blocks;
288
- if (payload.executionMs !== void 0) attributes[SEMATTRS_DB_PLAN_EXECUTION_MS] = payload.executionMs;
311
+ plan.rowsExamined = rowsExamined;
312
+ plan.rowsReturned = root.actualRows;
313
+ plan.blocks = blocks;
314
+ plan.executionMs = payload.executionMs;
289
315
  }
290
- return attributes;
316
+ return (0, autotel_db.planAttributes)(plan);
291
317
  }
292
318
  function resolveExplainClient(target, state) {
293
- return readQueryClient(target) ?? state.explainClient;
319
+ const own = readQueryClient(target);
320
+ if (own && isDriverObject(target)) return {
321
+ client: own,
322
+ connection: readObject(target, "client")
323
+ };
324
+ return state.explainClient ? { client: state.explainClient } : void 0;
294
325
  }
295
- async function collectPlan(state, client, queryText, params) {
296
- const mode = state.config.explain;
297
- if (!mode || client === void 0) return;
298
- if (mode === "analyze" && !isReadOnlyStatement(queryText)) return;
326
+ const openTransactions = new WeakMap();
327
+ function transactionConnections(tx) {
328
+ if (!isDriverObject(tx)) return [];
329
+ const found = [];
330
+ for (const holder of [tx, ...readSessions(tx)]) {
331
+ const connection = readObject(holder, "client");
332
+ if (connection !== void 0 && !found.includes(connection)) found.push(connection);
333
+ }
334
+ return found;
335
+ }
336
+ function openTransaction(connections) {
337
+ for (const connection of connections) openTransactions.set(connection, (openTransactions.get(connection) ?? 0) + 1);
338
+ return () => {
339
+ for (const connection of connections) {
340
+ const depth = (openTransactions.get(connection) ?? 1) - 1;
341
+ if (depth > 0) openTransactions.set(connection, depth);
342
+ else openTransactions.delete(connection);
343
+ }
344
+ };
345
+ }
346
+ function thenAfter(run, after) {
347
+ let result;
348
+ try {
349
+ result = run();
350
+ } catch (error) {
351
+ after();
352
+ throw error;
353
+ }
354
+ if (isThenable(result)) return Promise.resolve(result).finally(after);
355
+ after();
356
+ return result;
357
+ }
358
+ const EXPLAINABLE = new Set([
359
+ "SELECT",
360
+ "WITH",
361
+ "INSERT",
362
+ "UPDATE",
363
+ "DELETE",
364
+ "MERGE",
365
+ "VALUES",
366
+ "TABLE"
367
+ ]);
368
+ async function collectPlan(state, explainClient, queryText, params, inTransaction) {
369
+ const requested = state.config.explain;
370
+ if (!requested) return;
371
+ if (!EXPLAINABLE.has(extractOperation(queryText) ?? "")) return (0, autotel_db.planUnavailableAttributes)("unsupported", { mode: requested });
372
+ const mode = requested === "analyze" && !isReadOnlyStatement(queryText) ? "plan" : requested;
373
+ const client = inTransaction ? state.isolatedExplainClient : explainClient?.client;
374
+ if (client === void 0) return inTransaction ? (0, autotel_db.planUnavailableAttributes)("unsupported", { mode }) : (0, autotel_db.planUnavailableAttributes)("failed", {
375
+ mode,
376
+ error: "no client to run EXPLAIN on"
377
+ });
299
378
  try {
300
379
  const payload = parseExplainResponse(await client.query(buildExplainStatement(mode, queryText), readParams(params)));
301
- if (payload === void 0) return;
380
+ if (payload === void 0) return (0, autotel_db.planUnavailableAttributes)("failed", {
381
+ mode,
382
+ error: "EXPLAIN returned no plan autotel-drizzle reads"
383
+ });
302
384
  return planAttributes(payload, mode);
303
- } catch {
304
- return;
385
+ } catch (error) {
386
+ return (0, autotel_db.planUnavailableAttributes)("failed", {
387
+ mode,
388
+ error: error instanceof Error ? error.message : String(error)
389
+ });
305
390
  }
306
391
  }
307
392
  function buildSpanName(mode, operation, table) {
@@ -327,7 +412,7 @@ function buildSpan(state, queryText, extraAttributes) {
327
412
  if (operation) setRenamed(SEMATTRS_DB_OPERATION, SEMATTRS_DB_OPERATION_NAME, operation);
328
413
  if (state.config.dbName !== void 0) setRenamed(SEMATTRS_DB_NAME, SEMATTRS_DB_NAMESPACE, state.config.dbName);
329
414
  if (queryText !== void 0) {
330
- span.setAttribute(SEMATTRS_DB_STATEMENT_HASH, hashQueryText(queryText));
415
+ span.setAttribute(SEMATTRS_DB_STATEMENT_HASH, (0, autotel_db.hashStatement)(queryText));
331
416
  if (table !== void 0) span.setAttribute(SEMATTRS_DB_COLLECTION_NAME, table);
332
417
  }
333
418
  if (state.config.captureQueryText && queryText !== void 0) {
@@ -339,6 +424,37 @@ function buildSpan(state, queryText, extraAttributes) {
339
424
  if (extraAttributes) for (const [key, value] of Object.entries(extraAttributes)) span.setAttribute(key, value);
340
425
  return span;
341
426
  }
427
+ function runAlongsidePlan(span, planned, fn) {
428
+ const end = (error) => {
429
+ const endTime = performance.timeOrigin + performance.now();
430
+ planned.then((attributes) => {
431
+ if (attributes) span.setAttributes(attributes);
432
+ }).finally(() => {
433
+ if (error) {
434
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
435
+ span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
436
+ } else span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK });
437
+ span.end(endTime);
438
+ });
439
+ };
440
+ return (0, autotel_trace_helpers.runWithSpan)(span, () => {
441
+ try {
442
+ const result = fn();
443
+ if (isThenable(result)) return result.then((value) => {
444
+ end();
445
+ return value;
446
+ }, (error) => {
447
+ end(error);
448
+ throw error;
449
+ });
450
+ end();
451
+ return result;
452
+ } catch (error) {
453
+ end(error);
454
+ throw error;
455
+ }
456
+ });
457
+ }
342
458
  function executeWithSpan(span, fn) {
343
459
  return (0, autotel_trace_helpers.runWithSpan)(span, () => {
344
460
  try {
@@ -369,11 +485,20 @@ function instrumentMethod(target, methodName, state, options) {
369
485
  const callback = trailing === void 0 ? void 0 : (args.pop(), trailing);
370
486
  if (options.guardReentry && target[REENTRY_FLAG]) return originalMethod.call(this, ...incomingArgs);
371
487
  const queryText = options.queryText(args);
372
- if (state.config.explain && queryText !== void 0 && !callback) return collectPlan(state, resolveExplainClient(readSelf(this), state), queryText, options.explainParams?.(args)).then((planAttributes) => executeWithSpan(buildSpan(state, queryText, {
488
+ if (state.config.explain && queryText !== void 0 && !callback) {
489
+ const explainClient = resolveExplainClient(readSelf(this), state);
490
+ const inTransaction = options.extraAttributes?.[TRANSACTION_ATTRIBUTE] === true || explainClient?.connection !== void 0 && openTransactions.has(explainClient.connection);
491
+ if (inTransaction) return runAlongsidePlan(buildSpan(state, queryText, options.extraAttributes), collectPlan(state, explainClient, queryText, options.explainParams?.(args), true), () => originalMethod.call(this, ...args));
492
+ return collectPlan(state, explainClient, queryText, options.explainParams?.(args), inTransaction).then((planAttributes) => executeWithSpan(buildSpan(state, queryText, {
493
+ ...options.extraAttributes,
494
+ ...planAttributes
495
+ }), () => originalMethod.call(this, ...args)));
496
+ }
497
+ const unexplained = state.config.explainRequested && queryText !== void 0 ? (0, autotel_db.planUnavailableAttributes)("unsupported", { mode: state.config.explainRequested }) : void 0;
498
+ const span = buildSpan(state, queryText, {
373
499
  ...options.extraAttributes,
374
- ...planAttributes
375
- }), () => originalMethod.call(this, ...args)));
376
- const span = buildSpan(state, queryText, options.extraAttributes);
500
+ ...unexplained
501
+ });
377
502
  if (callback) return (0, autotel_trace_helpers.runWithSpan)(span, () => {
378
503
  const wrappedCallback = (error, result) => {
379
504
  (0, autotel_trace_helpers.finalizeSpan)(span, error);
@@ -479,11 +604,15 @@ function instrumentSession(session, state, allowDirectMethods = true) {
479
604
  const [rawCallback, ...restArgs] = transactionArgs;
480
605
  const callback = asFunction(rawCallback);
481
606
  if (callback === void 0) return originalTransaction.call(this, ...transactionArgs);
607
+ const closers = [];
482
608
  const wrappedCallback = (tx, ...callbackArgs) => {
483
609
  instrumentTransactionTarget(tx, state);
610
+ closers.push(openTransaction(transactionConnections(tx)));
484
611
  return callback.call(this, tx, ...callbackArgs);
485
612
  };
486
- const runTransaction = () => originalTransaction.call(this, wrappedCallback, ...restArgs);
613
+ const runTransaction = () => thenAfter(() => originalTransaction.call(this, wrappedCallback, ...restArgs), () => {
614
+ for (const close of closers) close();
615
+ });
487
616
  if (!state.config.traceTransactions) return runTransaction();
488
617
  const span = state.tracer.startSpan("drizzle.transaction", { kind: _opentelemetry_api.SpanKind.CLIENT });
489
618
  span.setAttribute(TRANSACTION_ATTRIBUTE, true);
@@ -523,6 +652,7 @@ function instrumentDrizzleClient(db, config) {
523
652
  if (readFlag(target, INSTRUMENTED_FLAG)) return db;
524
653
  const state = getState(config);
525
654
  state.explainClient = asQueryClient(target.$client);
655
+ state.isolatedExplainClient = isolatedExplainClient(target.$client);
526
656
  let instrumented = false;
527
657
  let sessionInstrumented = false;
528
658
  for (const session of readSessions(target)) {
package/dist/drizzle.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_drizzle = require("./drizzle-BK9eu10E.cjs");
2
+ const require_drizzle = require("./drizzle-BeUip3cx.cjs");
3
3
  exports.instrumentDrizzle = require_drizzle.instrumentDrizzle;
4
4
  exports.instrumentDrizzleClient = require_drizzle.instrumentDrizzleClient;
@@ -13,7 +13,7 @@ import { TracerProvider } from "@opentelemetry/api";
13
13
  * Both modes double the round trips to the database. Turn them on in
14
14
  * development, in CI, or behind a sample of production traffic.
15
15
  */
16
- type ExplainMode = 'plan' | 'analyze';
16
+ export type ExplainMode = 'plan' | 'analyze';
17
17
  /**
18
18
  * Which OpenTelemetry database attribute names to emit.
19
19
  *
@@ -29,7 +29,7 @@ type ExplainMode = 'plan' | 'analyze';
29
29
  * `OTEL_SEMCONV_STABILITY_OPT_IN`: `database` selects `'stable'` and
30
30
  * `database/dup` selects `'dup'`.
31
31
  */
32
- type SemconvMode = 'legacy' | 'stable' | 'dup';
32
+ export type SemconvMode = 'legacy' | 'stable' | 'dup';
33
33
  /**
34
34
  * How a query span is named.
35
35
  *
@@ -37,8 +37,8 @@ type SemconvMode = 'legacy' | 'stable' | 'dup';
37
37
  * - `'semconv'` follows OpenTelemetry's database span convention,
38
38
  * `{db.operation.name} {target}`, so `SELECT comments`.
39
39
  */
40
- type SpanNamingMode = 'drizzle' | 'semconv';
41
- interface InstrumentDrizzleConfig {
40
+ export type SpanNamingMode = 'drizzle' | 'semconv';
41
+ export interface InstrumentDrizzleConfig {
42
42
  tracerName?: string;
43
43
  dbSystem?: string;
44
44
  dbName?: string;
@@ -78,7 +78,6 @@ interface InstrumentDrizzleConfig {
78
78
  */
79
79
  traceTransactions?: boolean;
80
80
  }
81
- declare function instrumentDrizzle<TClient>(client: TClient, config?: InstrumentDrizzleConfig): TClient;
82
- declare function instrumentDrizzleClient<TDb>(db: TDb, config?: InstrumentDrizzleConfig): TDb;
83
- //#endregion
84
- export { ExplainMode, InstrumentDrizzleConfig, SemconvMode, SpanNamingMode, instrumentDrizzle, instrumentDrizzleClient };
81
+ export declare function instrumentDrizzle<TClient>(client: TClient, config?: InstrumentDrizzleConfig): TClient;
82
+ export declare function instrumentDrizzleClient<TDb>(db: TDb, config?: InstrumentDrizzleConfig): TDb;
83
+ //#endregion
package/dist/drizzle.d.ts CHANGED
@@ -13,7 +13,7 @@ import { TracerProvider } from "@opentelemetry/api";
13
13
  * Both modes double the round trips to the database. Turn them on in
14
14
  * development, in CI, or behind a sample of production traffic.
15
15
  */
16
- type ExplainMode = 'plan' | 'analyze';
16
+ export type ExplainMode = 'plan' | 'analyze';
17
17
  /**
18
18
  * Which OpenTelemetry database attribute names to emit.
19
19
  *
@@ -29,7 +29,7 @@ type ExplainMode = 'plan' | 'analyze';
29
29
  * `OTEL_SEMCONV_STABILITY_OPT_IN`: `database` selects `'stable'` and
30
30
  * `database/dup` selects `'dup'`.
31
31
  */
32
- type SemconvMode = 'legacy' | 'stable' | 'dup';
32
+ export type SemconvMode = 'legacy' | 'stable' | 'dup';
33
33
  /**
34
34
  * How a query span is named.
35
35
  *
@@ -37,8 +37,8 @@ type SemconvMode = 'legacy' | 'stable' | 'dup';
37
37
  * - `'semconv'` follows OpenTelemetry's database span convention,
38
38
  * `{db.operation.name} {target}`, so `SELECT comments`.
39
39
  */
40
- type SpanNamingMode = 'drizzle' | 'semconv';
41
- interface InstrumentDrizzleConfig {
40
+ export type SpanNamingMode = 'drizzle' | 'semconv';
41
+ export interface InstrumentDrizzleConfig {
42
42
  tracerName?: string;
43
43
  dbSystem?: string;
44
44
  dbName?: string;
@@ -78,7 +78,6 @@ interface InstrumentDrizzleConfig {
78
78
  */
79
79
  traceTransactions?: boolean;
80
80
  }
81
- declare function instrumentDrizzle<TClient>(client: TClient, config?: InstrumentDrizzleConfig): TClient;
82
- declare function instrumentDrizzleClient<TDb>(db: TDb, config?: InstrumentDrizzleConfig): TDb;
83
- //#endregion
84
- export { ExplainMode, InstrumentDrizzleConfig, SemconvMode, SpanNamingMode, instrumentDrizzle, instrumentDrizzleClient };
81
+ export declare function instrumentDrizzle<TClient>(client: TClient, config?: InstrumentDrizzleConfig): TClient;
82
+ export declare function instrumentDrizzleClient<TDb>(db: TDb, config?: InstrumentDrizzleConfig): TDb;
83
+ //#endregion
package/dist/drizzle.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as instrumentDrizzleClient, t as instrumentDrizzle } from "./drizzle-BiA8DdAa.js";
1
+ import { n as instrumentDrizzleClient, t as instrumentDrizzle } from "./drizzle-BAxEDkHv.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-BK9eu10E.cjs");
2
+ const require_drizzle = require("./drizzle-BeUip3cx.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.d.cts CHANGED
@@ -4,27 +4,27 @@ import { ExplainMode, InstrumentDrizzleConfig, instrumentDrizzle, instrumentDriz
4
4
  * OpenTelemetry semantic conventions for database operations.
5
5
  * These constants are shared across all plugins.
6
6
  */
7
- declare const SEMATTRS_DB_SYSTEM: "db.system";
8
- declare const SEMATTRS_DB_SYSTEM_NAME: "db.system.name";
9
- declare const SEMATTRS_DB_OPERATION: "db.operation";
10
- declare const SEMATTRS_DB_STATEMENT: "db.statement";
11
- declare const SEMATTRS_DB_NAME: "db.name";
12
- declare const SEMATTRS_DB_NAMESPACE: "db.namespace";
13
- declare const SEMATTRS_DB_COLLECTION_NAME: "db.collection.name";
14
- declare const SEMATTRS_DB_OPERATION_NAME: "db.operation.name";
15
- declare const SEMATTRS_DB_QUERY_TEXT: "db.query.text";
16
- declare const SEMATTRS_DB_QUERY_SUMMARY: "db.query.summary";
17
- declare const SEMATTRS_NET_PEER_NAME: "net.peer.name";
18
- declare const SEMATTRS_NET_PEER_PORT: "net.peer.port";
19
- declare const SEMATTRS_GCP_BIGQUERY_JOB_ID: "gcp.bigquery.job.id";
20
- declare const SEMATTRS_GCP_BIGQUERY_JOB_LOCATION: "gcp.bigquery.job.location";
21
- declare const SEMATTRS_GCP_BIGQUERY_PROJECT_ID: "gcp.bigquery.project.id";
22
- declare const SEMATTRS_GCP_BIGQUERY_DESTINATION_TABLE: "gcp.bigquery.destination.table";
23
- declare const SEMATTRS_GCP_BIGQUERY_SOURCE_TABLES: "gcp.bigquery.source.tables";
24
- declare const SEMATTRS_GCP_BIGQUERY_STATEMENT_TYPE: "gcp.bigquery.statement_type";
25
- declare const SEMATTRS_GCP_BIGQUERY_QUERY_HASH: "gcp.bigquery.query.hash";
26
- declare const SEMATTRS_GCP_BIGQUERY_ROWS_AFFECTED: "gcp.bigquery.rows.affected";
27
- declare const SEMATTRS_GCP_BIGQUERY_ROWS_RETURNED: "gcp.bigquery.rows.returned";
28
- declare const SEMATTRS_GCP_BIGQUERY_SCHEMA_FIELDS: "gcp.bigquery.schema.fields";
7
+ export declare const SEMATTRS_DB_SYSTEM: 'db.system';
8
+ export declare const SEMATTRS_DB_SYSTEM_NAME: 'db.system.name';
9
+ export declare const SEMATTRS_DB_OPERATION: 'db.operation';
10
+ export declare const SEMATTRS_DB_STATEMENT: 'db.statement';
11
+ export declare const SEMATTRS_DB_NAME: 'db.name';
12
+ export declare const SEMATTRS_DB_NAMESPACE: 'db.namespace';
13
+ export declare const SEMATTRS_DB_COLLECTION_NAME: 'db.collection.name';
14
+ export declare const SEMATTRS_DB_OPERATION_NAME: 'db.operation.name';
15
+ export declare const SEMATTRS_DB_QUERY_TEXT: 'db.query.text';
16
+ export declare const SEMATTRS_DB_QUERY_SUMMARY: 'db.query.summary';
17
+ export declare const SEMATTRS_NET_PEER_NAME: 'net.peer.name';
18
+ export declare const SEMATTRS_NET_PEER_PORT: 'net.peer.port';
19
+ export declare const SEMATTRS_GCP_BIGQUERY_JOB_ID: 'gcp.bigquery.job.id';
20
+ export declare const SEMATTRS_GCP_BIGQUERY_JOB_LOCATION: 'gcp.bigquery.job.location';
21
+ export declare const SEMATTRS_GCP_BIGQUERY_PROJECT_ID: 'gcp.bigquery.project.id';
22
+ export declare const SEMATTRS_GCP_BIGQUERY_DESTINATION_TABLE: 'gcp.bigquery.destination.table';
23
+ export declare const SEMATTRS_GCP_BIGQUERY_SOURCE_TABLES: 'gcp.bigquery.source.tables';
24
+ export declare const SEMATTRS_GCP_BIGQUERY_STATEMENT_TYPE: 'gcp.bigquery.statement_type';
25
+ export declare const SEMATTRS_GCP_BIGQUERY_QUERY_HASH: 'gcp.bigquery.query.hash';
26
+ export declare const SEMATTRS_GCP_BIGQUERY_ROWS_AFFECTED: 'gcp.bigquery.rows.affected';
27
+ export declare const SEMATTRS_GCP_BIGQUERY_ROWS_RETURNED: 'gcp.bigquery.rows.returned';
28
+ export declare const SEMATTRS_GCP_BIGQUERY_SCHEMA_FIELDS: 'gcp.bigquery.schema.fields';
29
29
  //#endregion
30
- export { type ExplainMode, type InstrumentDrizzleConfig, 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 };
30
+ export { type ExplainMode, type InstrumentDrizzleConfig, instrumentDrizzle, instrumentDrizzleClient };
package/dist/index.d.ts CHANGED
@@ -4,27 +4,27 @@ import { ExplainMode, InstrumentDrizzleConfig, instrumentDrizzle, instrumentDriz
4
4
  * OpenTelemetry semantic conventions for database operations.
5
5
  * These constants are shared across all plugins.
6
6
  */
7
- declare const SEMATTRS_DB_SYSTEM: "db.system";
8
- declare const SEMATTRS_DB_SYSTEM_NAME: "db.system.name";
9
- declare const SEMATTRS_DB_OPERATION: "db.operation";
10
- declare const SEMATTRS_DB_STATEMENT: "db.statement";
11
- declare const SEMATTRS_DB_NAME: "db.name";
12
- declare const SEMATTRS_DB_NAMESPACE: "db.namespace";
13
- declare const SEMATTRS_DB_COLLECTION_NAME: "db.collection.name";
14
- declare const SEMATTRS_DB_OPERATION_NAME: "db.operation.name";
15
- declare const SEMATTRS_DB_QUERY_TEXT: "db.query.text";
16
- declare const SEMATTRS_DB_QUERY_SUMMARY: "db.query.summary";
17
- declare const SEMATTRS_NET_PEER_NAME: "net.peer.name";
18
- declare const SEMATTRS_NET_PEER_PORT: "net.peer.port";
19
- declare const SEMATTRS_GCP_BIGQUERY_JOB_ID: "gcp.bigquery.job.id";
20
- declare const SEMATTRS_GCP_BIGQUERY_JOB_LOCATION: "gcp.bigquery.job.location";
21
- declare const SEMATTRS_GCP_BIGQUERY_PROJECT_ID: "gcp.bigquery.project.id";
22
- declare const SEMATTRS_GCP_BIGQUERY_DESTINATION_TABLE: "gcp.bigquery.destination.table";
23
- declare const SEMATTRS_GCP_BIGQUERY_SOURCE_TABLES: "gcp.bigquery.source.tables";
24
- declare const SEMATTRS_GCP_BIGQUERY_STATEMENT_TYPE: "gcp.bigquery.statement_type";
25
- declare const SEMATTRS_GCP_BIGQUERY_QUERY_HASH: "gcp.bigquery.query.hash";
26
- declare const SEMATTRS_GCP_BIGQUERY_ROWS_AFFECTED: "gcp.bigquery.rows.affected";
27
- declare const SEMATTRS_GCP_BIGQUERY_ROWS_RETURNED: "gcp.bigquery.rows.returned";
28
- declare const SEMATTRS_GCP_BIGQUERY_SCHEMA_FIELDS: "gcp.bigquery.schema.fields";
7
+ export declare const SEMATTRS_DB_SYSTEM: 'db.system';
8
+ export declare const SEMATTRS_DB_SYSTEM_NAME: 'db.system.name';
9
+ export declare const SEMATTRS_DB_OPERATION: 'db.operation';
10
+ export declare const SEMATTRS_DB_STATEMENT: 'db.statement';
11
+ export declare const SEMATTRS_DB_NAME: 'db.name';
12
+ export declare const SEMATTRS_DB_NAMESPACE: 'db.namespace';
13
+ export declare const SEMATTRS_DB_COLLECTION_NAME: 'db.collection.name';
14
+ export declare const SEMATTRS_DB_OPERATION_NAME: 'db.operation.name';
15
+ export declare const SEMATTRS_DB_QUERY_TEXT: 'db.query.text';
16
+ export declare const SEMATTRS_DB_QUERY_SUMMARY: 'db.query.summary';
17
+ export declare const SEMATTRS_NET_PEER_NAME: 'net.peer.name';
18
+ export declare const SEMATTRS_NET_PEER_PORT: 'net.peer.port';
19
+ export declare const SEMATTRS_GCP_BIGQUERY_JOB_ID: 'gcp.bigquery.job.id';
20
+ export declare const SEMATTRS_GCP_BIGQUERY_JOB_LOCATION: 'gcp.bigquery.job.location';
21
+ export declare const SEMATTRS_GCP_BIGQUERY_PROJECT_ID: 'gcp.bigquery.project.id';
22
+ export declare const SEMATTRS_GCP_BIGQUERY_DESTINATION_TABLE: 'gcp.bigquery.destination.table';
23
+ export declare const SEMATTRS_GCP_BIGQUERY_SOURCE_TABLES: 'gcp.bigquery.source.tables';
24
+ export declare const SEMATTRS_GCP_BIGQUERY_STATEMENT_TYPE: 'gcp.bigquery.statement_type';
25
+ export declare const SEMATTRS_GCP_BIGQUERY_QUERY_HASH: 'gcp.bigquery.query.hash';
26
+ export declare const SEMATTRS_GCP_BIGQUERY_ROWS_AFFECTED: 'gcp.bigquery.rows.affected';
27
+ export declare const SEMATTRS_GCP_BIGQUERY_ROWS_RETURNED: 'gcp.bigquery.rows.returned';
28
+ export declare const SEMATTRS_GCP_BIGQUERY_SCHEMA_FIELDS: 'gcp.bigquery.schema.fields';
29
29
  //#endregion
30
- export { type ExplainMode, type InstrumentDrizzleConfig, 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 };
30
+ export { type ExplainMode, type InstrumentDrizzleConfig, instrumentDrizzle, instrumentDrizzleClient };
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-BiA8DdAa.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-BAxEDkHv.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.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "OpenTelemetry instrumentation for Drizzle ORM",
5
5
  "type": "module",
6
6
  "main": "./dist/drizzle.js",
@@ -37,23 +37,20 @@
37
37
  "@opentelemetry/api": "^1.9.1",
38
38
  "@opentelemetry/instrumentation": "^0.222.0",
39
39
  "@opentelemetry/semantic-conventions": "^1.43.0",
40
- "autotel": "7.9.0"
40
+ "autotel": "7.10.0",
41
+ "autotel-db": "0.1.0"
41
42
  },
42
43
  "peerDependencies": {
43
- "drizzle-orm": ">=0.45.2"
44
+ "drizzle-orm": ">=0.45.3"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@arethetypeswrong/cli": "^0.18.5",
47
48
  "@opentelemetry/core": "^2.11.0",
48
- "@types/node": "^26.1.2",
49
- "@typescript-eslint/eslint-plugin": "^8.65.0",
50
- "@typescript-eslint/parser": "^8.65.0",
51
- "drizzle-orm": "^0.45.2",
52
- "eslint-config-prettier": "^10.1.8",
53
- "eslint-plugin-unicorn": "^64.0.0",
54
- "tsdown": "^0.22.14",
55
- "typescript": "^6.0.3",
56
- "vitest": "^4.1.10"
49
+ "@types/node": "^26.6.3",
50
+ "drizzle-orm": "^0.45.3",
51
+ "tsdown": "^0.23.0",
52
+ "typescript": "^7.0.2",
53
+ "vitest": "^5.0.2"
57
54
  },
58
55
  "repository": {
59
56
  "type": "git",
@@ -68,8 +65,8 @@
68
65
  "build": "tsdown",
69
66
  "ci": "npm run build && npm run check-format && npm run check-exports && npm run lint && npm run test",
70
67
  "dev": "tsdown --watch",
71
- "lint": "eslint src/**/*.ts",
72
- "lint:fix": "eslint src/**/*.ts --fix",
68
+ "lint": "oxlint -c oxlint.json --disable-nested-config src",
69
+ "lint:fix": "pnpm lint --fix",
73
70
  "format:check": "prettier --check src/**/*.ts",
74
71
  "type-check": "tsc --noEmit",
75
72
  "test": "vitest run --config vitest.unit.config.ts",