autotel-plugins 0.4.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.
@@ -0,0 +1,898 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ // Note: `any` types are necessary for dynamic instrumentation patterns
3
+ // where we need to wrap arbitrary methods and preserve their signatures
4
+ import { SpanKind, trace } from '@opentelemetry/api';
5
+ import {
6
+ SEMATTRS_DB_SYSTEM,
7
+ SEMATTRS_DB_OPERATION,
8
+ SEMATTRS_DB_STATEMENT,
9
+ SEMATTRS_DB_NAME,
10
+ SEMATTRS_NET_PEER_NAME,
11
+ SEMATTRS_NET_PEER_PORT,
12
+ } from '../common/constants';
13
+ import { runWithSpan, finalizeSpan } from 'autotel/trace-helpers';
14
+
15
+ const DEFAULT_TRACER_NAME = 'autotel-plugins/drizzle';
16
+ const DEFAULT_DB_SYSTEM = 'postgresql';
17
+ const INSTRUMENTED_FLAG = '__autotelDrizzleInstrumented' as const;
18
+
19
+ type QueryCallback = (error: unknown, result: unknown) => void;
20
+
21
+ type QueryFunction = (...args: any[]) => any;
22
+
23
+ interface DrizzleClientLike {
24
+ query?: QueryFunction;
25
+ execute?: QueryFunction;
26
+ [INSTRUMENTED_FLAG]?: true;
27
+ [key: string]: any; // Allow other properties
28
+ }
29
+
30
+ /**
31
+ * Configuration options for Drizzle instrumentation.
32
+ */
33
+ export interface InstrumentDrizzleConfig {
34
+ /**
35
+ * Custom tracer name. Defaults to "autotel-plugins/drizzle".
36
+ */
37
+ tracerName?: string;
38
+
39
+ /**
40
+ * Database system identifier (e.g., "postgresql", "mysql", "sqlite").
41
+ * Defaults to "postgresql".
42
+ */
43
+ dbSystem?: string;
44
+
45
+ /**
46
+ * Database name to include in spans.
47
+ */
48
+ dbName?: string;
49
+
50
+ /**
51
+ * Whether to capture full SQL query text in spans.
52
+ * Defaults to true.
53
+ */
54
+ captureQueryText?: boolean;
55
+
56
+ /**
57
+ * Maximum length for captured query text. Queries longer than this
58
+ * will be truncated. Defaults to 1000 characters.
59
+ */
60
+ maxQueryTextLength?: number;
61
+
62
+ /**
63
+ * Remote hostname or IP address of the database server.
64
+ * Example: "db.example.com" or "192.168.1.100"
65
+ */
66
+ peerName?: string;
67
+
68
+ /**
69
+ * Remote port number of the database server.
70
+ * Example: 5432 for PostgreSQL, 3306 for MySQL
71
+ */
72
+ peerPort?: number;
73
+ }
74
+
75
+ /**
76
+ * Extracts SQL query text from various query argument formats.
77
+ */
78
+ function extractQueryText(queryArg: unknown): string | undefined {
79
+ if (typeof queryArg === 'string') {
80
+ return queryArg;
81
+ }
82
+ if (queryArg && typeof queryArg === 'object') {
83
+ // Generic SQL object format (used by LibSQL, MySQL, and others)
84
+ if (typeof (queryArg as { sql?: unknown }).sql === 'string') {
85
+ return (queryArg as { sql: string }).sql;
86
+ }
87
+ // PostgreSQL-style query object
88
+ if (typeof (queryArg as { text?: unknown }).text === 'string') {
89
+ return (queryArg as { text: string }).text;
90
+ }
91
+ // Drizzle SQL object
92
+ if (
93
+ typeof (queryArg as { queryChunks?: unknown }).queryChunks === 'object'
94
+ ) {
95
+ // Drizzle query objects may have complex structure, try to extract meaningful info
96
+ const drizzleQuery = queryArg as Record<string, unknown>;
97
+ if (typeof drizzleQuery.sql === 'string') {
98
+ return drizzleQuery.sql;
99
+ }
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ /**
106
+ * Sanitizes and truncates query text for safe inclusion in spans.
107
+ */
108
+ function sanitizeQueryText(queryText: string, maxLength: number): string {
109
+ if (queryText.length <= maxLength) {
110
+ return queryText;
111
+ }
112
+ return `${queryText.slice(0, Math.max(0, maxLength))}...`;
113
+ }
114
+
115
+ /**
116
+ * Extracts the SQL operation (SELECT, INSERT, etc.) from query text.
117
+ */
118
+ function extractOperation(queryText: string): string | undefined {
119
+ const trimmed = queryText.trimStart();
120
+ const match = /^(?<op>\w+)/u.exec(trimmed);
121
+ return match?.groups?.op?.toUpperCase();
122
+ }
123
+
124
+ /**
125
+ * Instruments a database connection pool/client with OpenTelemetry tracing.
126
+ *
127
+ * This function wraps the connection's `query` and `execute` methods to create spans for each database
128
+ * operation.
129
+ * The instrumentation is idempotent - calling it multiple times on the same connection will only
130
+ * instrument it once.
131
+ *
132
+ * @typeParam TClient - The type of the database connection pool or client
133
+ * @param client - The database connection pool or client to instrument
134
+ * @param config - Optional configuration for instrumentation behavior
135
+ * @returns The instrumented pool/client (same instance, modified in place)
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * // PostgreSQL with node-postgres
140
+ * import { drizzle } from 'drizzle-orm/node-postgres';
141
+ * import { Pool } from 'pg';
142
+ * import { instrumentDrizzle } from 'autotel-plugins/drizzle';
143
+ *
144
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
145
+ * const instrumentedPool = instrumentDrizzle(pool, {
146
+ * dbSystem: 'postgresql',
147
+ * dbName: 'myapp',
148
+ * peerName: 'db.example.com',
149
+ * peerPort: 5432,
150
+ * });
151
+ * const db = drizzle({ client: instrumentedPool });
152
+ * ```
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * // MySQL with mysql2
157
+ * import { drizzle } from 'drizzle-orm/mysql2';
158
+ * import mysql from 'mysql2/promise';
159
+ * import { instrumentDrizzle } from 'autotel-plugins/drizzle';
160
+ *
161
+ * const connection = await mysql.createConnection({
162
+ * host: 'localhost',
163
+ * user: 'root',
164
+ * database: 'mydb',
165
+ * });
166
+ * const instrumentedConnection = instrumentDrizzle(connection, { dbSystem: 'mysql' });
167
+ * const db = drizzle({ client: instrumentedConnection });
168
+ * ```
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * // SQLite with better-sqlite3
173
+ * import { drizzle } from 'drizzle-orm/better-sqlite3';
174
+ * import Database from 'better-sqlite3';
175
+ * import { instrumentDrizzle } from 'autotel-plugins/drizzle';
176
+ *
177
+ * const sqlite = new Database('sqlite.db');
178
+ * const instrumentedSqlite = instrumentDrizzle(sqlite, { dbSystem: 'sqlite' });
179
+ * const db = drizzle({ client: instrumentedSqlite });
180
+ * ```
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * // LibSQL/Turso
185
+ * import { drizzle } from 'drizzle-orm/libsql';
186
+ * import { createClient } from '@libsql/client';
187
+ * import { instrumentDrizzle } from 'autotel-plugins/drizzle';
188
+ *
189
+ * const client = createClient({
190
+ * url: process.env.DATABASE_URL!,
191
+ * authToken: process.env.DATABASE_AUTH_TOKEN,
192
+ * });
193
+ * const instrumentedClient = instrumentDrizzle(client, { dbSystem: 'sqlite' });
194
+ * const db = drizzle({ client: instrumentedClient });
195
+ * ```
196
+ */
197
+ export function instrumentDrizzle<TClient extends DrizzleClientLike>(
198
+ client: TClient,
199
+ config?: InstrumentDrizzleConfig,
200
+ ): TClient {
201
+ if (!client) {
202
+ return client;
203
+ }
204
+
205
+ // Check if client has query or execute method
206
+ const hasQuery = typeof client.query === 'function';
207
+ const hasExecute = typeof client.execute === 'function';
208
+
209
+ if (!hasQuery && !hasExecute) {
210
+ return client;
211
+ }
212
+
213
+ if (client[INSTRUMENTED_FLAG]) {
214
+ return client;
215
+ }
216
+
217
+ const {
218
+ tracerName = DEFAULT_TRACER_NAME,
219
+ dbSystem = DEFAULT_DB_SYSTEM,
220
+ dbName,
221
+ captureQueryText = true,
222
+ maxQueryTextLength = 1000,
223
+ peerName,
224
+ peerPort,
225
+ } = config ?? {};
226
+
227
+ const tracer = trace.getTracer(tracerName);
228
+
229
+ // Store the original method (query or execute)
230
+ const originalMethod = hasQuery ? client.query : client.execute;
231
+
232
+ if (!originalMethod) {
233
+ return client;
234
+ }
235
+
236
+ const instrumentedMethod: QueryFunction = function instrumented(
237
+ this: any,
238
+ ...incomingArgs: any[]
239
+ ) {
240
+ const args = [...incomingArgs];
241
+ let callback: QueryCallback | undefined;
242
+
243
+ // Detect callback pattern
244
+ if (typeof args.at(-1) === 'function') {
245
+ callback = args.pop() as QueryCallback;
246
+ }
247
+
248
+ // Extract query information
249
+ const queryText = extractQueryText(args[0]);
250
+ const operation = queryText ? extractOperation(queryText) : undefined;
251
+ const spanName = operation
252
+ ? `drizzle.${operation.toLowerCase()}`
253
+ : 'drizzle.query';
254
+
255
+ // Start span
256
+ const span = tracer.startSpan(spanName, { kind: SpanKind.CLIENT });
257
+ span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
258
+
259
+ if (operation) {
260
+ span.setAttribute(SEMATTRS_DB_OPERATION, operation);
261
+ }
262
+
263
+ if (dbName) {
264
+ span.setAttribute(SEMATTRS_DB_NAME, dbName);
265
+ }
266
+
267
+ if (captureQueryText && queryText !== undefined) {
268
+ const sanitized = sanitizeQueryText(queryText, maxQueryTextLength);
269
+ span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
270
+ }
271
+
272
+ if (peerName) {
273
+ span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
274
+ }
275
+
276
+ if (peerPort) {
277
+ span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
278
+ }
279
+
280
+ // Callback-based pattern
281
+ if (callback) {
282
+ return runWithSpan(span, () => {
283
+ const wrappedCallback: QueryCallback = (err, result) => {
284
+ finalizeSpan(span, err);
285
+ if (callback) {
286
+ callback(err, result);
287
+ }
288
+ };
289
+
290
+ try {
291
+ return Reflect.apply(originalMethod, this, [
292
+ ...args,
293
+ wrappedCallback,
294
+ ]);
295
+ } catch (error) {
296
+ finalizeSpan(span, error);
297
+ throw error;
298
+ }
299
+ });
300
+ }
301
+
302
+ // Promise-based pattern
303
+ return runWithSpan(span, () => {
304
+ try {
305
+ const result = originalMethod.apply(this, args);
306
+ return Promise.resolve(result)
307
+ .then((value) => {
308
+ finalizeSpan(span);
309
+ return value;
310
+ })
311
+ .catch((error) => {
312
+ finalizeSpan(span, error);
313
+ throw error;
314
+ });
315
+ } catch (error) {
316
+ finalizeSpan(span, error);
317
+ throw error;
318
+ }
319
+ });
320
+ };
321
+
322
+ client[INSTRUMENTED_FLAG] = true;
323
+
324
+ // Replace the original method with the instrumented one
325
+ if (hasQuery) {
326
+ client.query = instrumentedMethod;
327
+ } else {
328
+ client.execute = instrumentedMethod;
329
+ }
330
+
331
+ return client;
332
+ }
333
+
334
+ /**
335
+ * Interface for Drizzle database instances with minimal type requirements.
336
+ */
337
+ interface DrizzleDbLike {
338
+ $client?: DrizzleClientLike | any; // Allow any client type
339
+ execute?: QueryFunction; // Direct execute method on db
340
+ transaction?: QueryFunction; // Transaction method on db
341
+ _?: {
342
+ session?: {
343
+ execute?: QueryFunction;
344
+ [INSTRUMENTED_FLAG]?: true;
345
+ [key: string]: any;
346
+ };
347
+ [key: string]: any;
348
+ };
349
+ [INSTRUMENTED_FLAG]?: true;
350
+ [key: string]: any; // Allow other properties
351
+ }
352
+
353
+ /**
354
+ * Instruments a Drizzle database instance with OpenTelemetry tracing.
355
+ *
356
+ * This function instruments the database at the session level, automatically tracing all database
357
+ * operations including query builders, direct SQL execution, and transactions.
358
+ *
359
+ * The instrumentation is idempotent - calling it multiple times on the same
360
+ * database will only instrument it once.
361
+ *
362
+ * @typeParam TDb - The type of the Drizzle database instance
363
+ * @param db - The Drizzle database instance to instrument
364
+ * @param config - Optional configuration for instrumentation behavior
365
+ * @returns The instrumented database instance (same instance, modified in place)
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * // PostgreSQL with postgres.js
370
+ * import { drizzle } from 'drizzle-orm/postgres-js';
371
+ * import postgres from 'postgres';
372
+ * import { instrumentDrizzleClient } from 'autotel-plugins/drizzle';
373
+ *
374
+ * // Using connection string
375
+ * const db = drizzle(process.env.DATABASE_URL!);
376
+ * instrumentDrizzleClient(db, { dbSystem: 'postgresql' });
377
+ *
378
+ * // Or with a client instance
379
+ * const queryClient = postgres(process.env.DATABASE_URL!);
380
+ * const db = drizzle({ client: queryClient });
381
+ * instrumentDrizzleClient(db, { dbSystem: 'postgresql' });
382
+ * ```
383
+ *
384
+ * @example
385
+ * ```typescript
386
+ * // PostgreSQL with node-postgres (pg)
387
+ * import { drizzle } from 'drizzle-orm/node-postgres';
388
+ * import { Pool } from 'pg';
389
+ * import { instrumentDrizzleClient } from 'autotel-plugins/drizzle';
390
+ *
391
+ * // Using connection string
392
+ * const db = drizzle(process.env.DATABASE_URL!);
393
+ * instrumentDrizzleClient(db, { dbSystem: 'postgresql' });
394
+ *
395
+ * // Or with a pool
396
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
397
+ * const db = drizzle({ client: pool });
398
+ * instrumentDrizzleClient(db, {
399
+ * dbSystem: 'postgresql',
400
+ * dbName: 'myapp',
401
+ * peerName: 'db.example.com',
402
+ * peerPort: 5432,
403
+ * });
404
+ * ```
405
+ */
406
+ export function instrumentDrizzleClient<TDb extends DrizzleDbLike>(
407
+ db: TDb,
408
+ config?: InstrumentDrizzleConfig,
409
+ ): TDb {
410
+ if (!db) {
411
+ return db;
412
+ }
413
+
414
+ // Check if already instrumented
415
+ if (db[INSTRUMENTED_FLAG]) {
416
+ return db;
417
+ }
418
+
419
+ const {
420
+ tracerName = DEFAULT_TRACER_NAME,
421
+ dbSystem = DEFAULT_DB_SYSTEM,
422
+ dbName,
423
+ captureQueryText = true,
424
+ maxQueryTextLength = 1000,
425
+ peerName,
426
+ peerPort,
427
+ } = config ?? {};
428
+
429
+ const tracer = trace.getTracer(tracerName);
430
+ let instrumented = false;
431
+
432
+ // First priority: Instrument the session directly
433
+ // This is where all queries actually go through
434
+ if ((db as any).session && !instrumented) {
435
+ const session = (db as any).session;
436
+
437
+ // Check if session has prepareQuery method (used by select/insert/update/delete)
438
+ if (
439
+ typeof session.prepareQuery === 'function' &&
440
+ !session[INSTRUMENTED_FLAG]
441
+ ) {
442
+ const originalPrepareQuery = session.prepareQuery;
443
+
444
+ session.prepareQuery = function (...args: any[]) {
445
+ const prepared = originalPrepareQuery.apply(this, args);
446
+
447
+ // Wrap the prepared query's execute method
448
+ if (prepared && typeof prepared.execute === 'function') {
449
+ const originalPreparedExecute = prepared.execute;
450
+
451
+ prepared.execute = function (this: any, ...executeArgs: any[]) {
452
+ // Extract query information from the query object
453
+ const queryObj = args[0]; // The query object passed to prepareQuery
454
+ const queryText =
455
+ queryObj?.sql ||
456
+ queryObj?.queryString ||
457
+ extractQueryText(queryObj);
458
+ const operation = queryText
459
+ ? extractOperation(queryText)
460
+ : undefined;
461
+ const spanName = operation
462
+ ? `drizzle.${operation.toLowerCase()}`
463
+ : 'drizzle.query';
464
+
465
+ // Start span
466
+ const span = tracer.startSpan(spanName, { kind: SpanKind.CLIENT });
467
+ span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
468
+
469
+ if (operation) {
470
+ span.setAttribute(SEMATTRS_DB_OPERATION, operation);
471
+ }
472
+
473
+ if (dbName) {
474
+ span.setAttribute(SEMATTRS_DB_NAME, dbName);
475
+ }
476
+
477
+ if (captureQueryText && queryText !== undefined) {
478
+ const sanitized = sanitizeQueryText(
479
+ queryText,
480
+ maxQueryTextLength,
481
+ );
482
+ span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
483
+ }
484
+
485
+ if (peerName) {
486
+ span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
487
+ }
488
+
489
+ if (peerPort) {
490
+ span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
491
+ }
492
+
493
+ // Execute the prepared query
494
+ return runWithSpan(span, () => {
495
+ try {
496
+ const result = originalPreparedExecute.apply(this, executeArgs);
497
+ return Promise.resolve(result)
498
+ .then((value) => {
499
+ finalizeSpan(span);
500
+ return value;
501
+ })
502
+ .catch((error) => {
503
+ finalizeSpan(span, error);
504
+ throw error;
505
+ });
506
+ } catch (error) {
507
+ finalizeSpan(span, error);
508
+ throw error;
509
+ }
510
+ });
511
+ };
512
+ }
513
+
514
+ return prepared;
515
+ };
516
+
517
+ session[INSTRUMENTED_FLAG] = true;
518
+ instrumented = true;
519
+ }
520
+
521
+ // Also instrument direct query method if exists
522
+ if (
523
+ typeof session.query === 'function' &&
524
+ !session[INSTRUMENTED_FLAG + '_query']
525
+ ) {
526
+ const originalQuery = session.query;
527
+
528
+ session.query = function (this: any, queryString: string, params: any[]) {
529
+ const operation = queryString
530
+ ? extractOperation(queryString)
531
+ : undefined;
532
+ const spanName = operation
533
+ ? `drizzle.${operation.toLowerCase()}`
534
+ : 'drizzle.query';
535
+
536
+ // Start span
537
+ const span = tracer.startSpan(spanName, { kind: SpanKind.CLIENT });
538
+ span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
539
+
540
+ if (operation) {
541
+ span.setAttribute(SEMATTRS_DB_OPERATION, operation);
542
+ }
543
+
544
+ if (dbName) {
545
+ span.setAttribute(SEMATTRS_DB_NAME, dbName);
546
+ }
547
+
548
+ if (captureQueryText && queryString !== undefined) {
549
+ const sanitized = sanitizeQueryText(queryString, maxQueryTextLength);
550
+ span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
551
+ }
552
+
553
+ if (peerName) {
554
+ span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
555
+ }
556
+
557
+ if (peerPort) {
558
+ span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
559
+ }
560
+
561
+ // Execute the query
562
+ return runWithSpan(span, () => {
563
+ try {
564
+ const result = Reflect.apply(originalQuery, this, [
565
+ queryString,
566
+ params,
567
+ ]);
568
+ return Promise.resolve(result)
569
+ .then((value) => {
570
+ finalizeSpan(span);
571
+ return value;
572
+ })
573
+ .catch((error) => {
574
+ finalizeSpan(span, error);
575
+ throw error;
576
+ });
577
+ } catch (error) {
578
+ finalizeSpan(span, error);
579
+ throw error;
580
+ }
581
+ });
582
+ };
583
+
584
+ session[INSTRUMENTED_FLAG + '_query'] = true;
585
+ instrumented = true;
586
+ }
587
+
588
+ // Instrument transaction method to ensure transaction sessions are also instrumented
589
+ if (
590
+ typeof session.transaction === 'function' &&
591
+ !session[INSTRUMENTED_FLAG + '_transaction']
592
+ ) {
593
+ const originalTransaction = session.transaction;
594
+
595
+ session.transaction = function (
596
+ this: any,
597
+ transactionCallback: any,
598
+ ...restArgs: any[]
599
+ ) {
600
+ // Wrap the transaction callback to instrument the tx object
601
+ const wrappedCallback = async function (tx: any) {
602
+ // Instrument the transaction's session if it has one
603
+ if (tx && (tx.session || tx._?.session || tx)) {
604
+ const txSession = tx.session || tx._?.session || tx;
605
+
606
+ // Instrument tx.execute if it exists
607
+ if (
608
+ typeof tx.execute === 'function' &&
609
+ !tx[INSTRUMENTED_FLAG + '_execute']
610
+ ) {
611
+ const originalTxExecute = tx.execute;
612
+
613
+ tx.execute = function (this: any, ...executeArgs: any[]) {
614
+ const queryText = extractQueryText(executeArgs[0]);
615
+ const operation = queryText
616
+ ? extractOperation(queryText)
617
+ : undefined;
618
+ const spanName = operation
619
+ ? `drizzle.${operation.toLowerCase()}`
620
+ : 'drizzle.query';
621
+
622
+ // Start span
623
+ const span = tracer.startSpan(spanName, {
624
+ kind: SpanKind.CLIENT,
625
+ });
626
+ span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
627
+ span.setAttribute('db.transaction', true);
628
+
629
+ if (operation) {
630
+ span.setAttribute(SEMATTRS_DB_OPERATION, operation);
631
+ }
632
+
633
+ if (dbName) {
634
+ span.setAttribute(SEMATTRS_DB_NAME, dbName);
635
+ }
636
+
637
+ if (captureQueryText && queryText !== undefined) {
638
+ const sanitized = sanitizeQueryText(
639
+ queryText,
640
+ maxQueryTextLength,
641
+ );
642
+ span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
643
+ }
644
+
645
+ if (peerName) {
646
+ span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
647
+ }
648
+
649
+ if (peerPort) {
650
+ span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
651
+ }
652
+
653
+ // Execute the query
654
+ return runWithSpan(span, () => {
655
+ try {
656
+ const result = originalTxExecute.apply(this, executeArgs);
657
+ return Promise.resolve(result)
658
+ .then((value) => {
659
+ finalizeSpan(span);
660
+ return value;
661
+ })
662
+ .catch((error) => {
663
+ finalizeSpan(span, error);
664
+ throw error;
665
+ });
666
+ } catch (error) {
667
+ finalizeSpan(span, error);
668
+ throw error;
669
+ }
670
+ });
671
+ };
672
+
673
+ tx[INSTRUMENTED_FLAG + '_execute'] = true;
674
+ }
675
+
676
+ // Also instrument txSession.prepareQuery if it exists
677
+ if (
678
+ typeof txSession.prepareQuery === 'function' &&
679
+ !txSession[INSTRUMENTED_FLAG + '_tx']
680
+ ) {
681
+ const originalTxPrepareQuery = txSession.prepareQuery;
682
+
683
+ txSession.prepareQuery = function (...prepareArgs: any[]) {
684
+ const prepared = originalTxPrepareQuery.apply(
685
+ this,
686
+ prepareArgs,
687
+ );
688
+
689
+ // Wrap the prepared query's execute method
690
+ if (prepared && typeof prepared.execute === 'function') {
691
+ const originalPreparedExecute = prepared.execute;
692
+
693
+ prepared.execute = function (
694
+ this: any,
695
+ ...executeArgs: any[]
696
+ ) {
697
+ // Extract query information from the query object
698
+ const queryObj = prepareArgs[0]; // The query object passed to prepareQuery
699
+ const queryText =
700
+ queryObj?.sql ||
701
+ queryObj?.queryString ||
702
+ extractQueryText(queryObj);
703
+ const operation = queryText
704
+ ? extractOperation(queryText)
705
+ : undefined;
706
+ const spanName = operation
707
+ ? `drizzle.${operation.toLowerCase()}`
708
+ : 'drizzle.query';
709
+
710
+ // Start span
711
+ const span = tracer.startSpan(spanName, {
712
+ kind: SpanKind.CLIENT,
713
+ });
714
+ span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
715
+ span.setAttribute('db.transaction', true);
716
+
717
+ if (operation) {
718
+ span.setAttribute(SEMATTRS_DB_OPERATION, operation);
719
+ }
720
+
721
+ if (dbName) {
722
+ span.setAttribute(SEMATTRS_DB_NAME, dbName);
723
+ }
724
+
725
+ if (captureQueryText && queryText !== undefined) {
726
+ const sanitized = sanitizeQueryText(
727
+ queryText,
728
+ maxQueryTextLength,
729
+ );
730
+ span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
731
+ }
732
+
733
+ if (peerName) {
734
+ span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
735
+ }
736
+
737
+ if (peerPort) {
738
+ span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
739
+ }
740
+
741
+ // Execute the prepared query
742
+ return runWithSpan(span, () => {
743
+ try {
744
+ const result = originalPreparedExecute.apply(
745
+ this,
746
+ executeArgs,
747
+ );
748
+ return Promise.resolve(result)
749
+ .then((value) => {
750
+ finalizeSpan(span);
751
+ return value;
752
+ })
753
+ .catch((error) => {
754
+ finalizeSpan(span, error);
755
+ throw error;
756
+ });
757
+ } catch (error) {
758
+ finalizeSpan(span, error);
759
+ throw error;
760
+ }
761
+ });
762
+ };
763
+ }
764
+
765
+ return prepared;
766
+ };
767
+
768
+ txSession[INSTRUMENTED_FLAG + '_tx'] = true;
769
+ }
770
+ }
771
+
772
+ // Call the original callback with the instrumented tx
773
+ return transactionCallback(tx);
774
+ };
775
+
776
+ // Call the original transaction with the wrapped callback
777
+ return Reflect.apply(originalTransaction, this, [
778
+ wrappedCallback,
779
+ ...restArgs,
780
+ ]);
781
+ };
782
+
783
+ session[INSTRUMENTED_FLAG + '_transaction'] = true;
784
+ instrumented = true;
785
+ }
786
+ }
787
+
788
+ if (db.$client && !instrumented) {
789
+ const client = db.$client;
790
+ // Check if client has query or execute function
791
+ if (
792
+ typeof client.query === 'function' ||
793
+ typeof client.execute === 'function'
794
+ ) {
795
+ instrumentDrizzle(client, config);
796
+ instrumented = true;
797
+ }
798
+ }
799
+
800
+ // Third priority: Try to instrument via session.execute as fallback
801
+ if (
802
+ db._ &&
803
+ db._.session &&
804
+ typeof db._.session.execute === 'function' &&
805
+ !instrumented
806
+ ) {
807
+ const session = db._.session;
808
+
809
+ // Check if already instrumented
810
+ if (session[INSTRUMENTED_FLAG]) {
811
+ return db;
812
+ }
813
+
814
+ const {
815
+ tracerName = DEFAULT_TRACER_NAME,
816
+ dbSystem = DEFAULT_DB_SYSTEM,
817
+ dbName,
818
+ captureQueryText = true,
819
+ maxQueryTextLength = 1000,
820
+ peerName,
821
+ peerPort,
822
+ } = config ?? {};
823
+
824
+ const tracer = trace.getTracer(tracerName);
825
+ const originalExecute = session.execute;
826
+
827
+ if (!originalExecute) {
828
+ return db;
829
+ }
830
+
831
+ const instrumentedExecute: QueryFunction = function instrumented(
832
+ this: any,
833
+ ...args: any[]
834
+ ) {
835
+ // Extract query information
836
+ const queryText = extractQueryText(args[0]);
837
+ const operation = queryText ? extractOperation(queryText) : undefined;
838
+ const spanName = operation
839
+ ? `drizzle.${operation.toLowerCase()}`
840
+ : 'drizzle.query';
841
+
842
+ // Start span
843
+ const span = tracer.startSpan(spanName, { kind: SpanKind.CLIENT });
844
+ span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
845
+
846
+ if (operation) {
847
+ span.setAttribute(SEMATTRS_DB_OPERATION, operation);
848
+ }
849
+
850
+ if (dbName) {
851
+ span.setAttribute(SEMATTRS_DB_NAME, dbName);
852
+ }
853
+
854
+ if (captureQueryText && queryText !== undefined) {
855
+ const sanitized = sanitizeQueryText(queryText, maxQueryTextLength);
856
+ span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
857
+ }
858
+
859
+ if (peerName) {
860
+ span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
861
+ }
862
+
863
+ if (peerPort) {
864
+ span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
865
+ }
866
+
867
+ // Promise-based pattern (session.execute is typically promise-based)
868
+ return runWithSpan(span, () => {
869
+ try {
870
+ const result = originalExecute.apply(this, args);
871
+ return Promise.resolve(result)
872
+ .then((value) => {
873
+ finalizeSpan(span);
874
+ return value;
875
+ })
876
+ .catch((error) => {
877
+ finalizeSpan(span, error);
878
+ throw error;
879
+ });
880
+ } catch (error) {
881
+ finalizeSpan(span, error);
882
+ throw error;
883
+ }
884
+ });
885
+ };
886
+
887
+ session[INSTRUMENTED_FLAG] = true;
888
+ session.execute = instrumentedExecute;
889
+ instrumented = true;
890
+ }
891
+
892
+ // Mark the db as instrumented if we instrumented anything
893
+ if (instrumented) {
894
+ db[INSTRUMENTED_FLAG] = true;
895
+ }
896
+
897
+ return db;
898
+ }