autotel-drizzle 0.0.1

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,533 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { SpanKind, trace } from '@opentelemetry/api';
3
+ import {
4
+ SEMATTRS_DB_NAME,
5
+ SEMATTRS_DB_OPERATION,
6
+ SEMATTRS_DB_STATEMENT,
7
+ SEMATTRS_DB_SYSTEM,
8
+ SEMATTRS_NET_PEER_NAME,
9
+ SEMATTRS_NET_PEER_PORT,
10
+ } from '../common/constants';
11
+ import { finalizeSpan, runWithSpan } from 'autotel/trace-helpers';
12
+
13
+ const DEFAULT_TRACER_NAME = 'autotel-plugins/drizzle';
14
+ const DEFAULT_DB_SYSTEM = 'postgresql';
15
+ const INSTRUMENTED_FLAG = '__autotelDrizzleInstrumented' as const;
16
+ const PREPARED_QUERY_METHODS = [
17
+ 'all',
18
+ 'execute',
19
+ 'get',
20
+ 'run',
21
+ 'values',
22
+ ] as const;
23
+
24
+ type QueryCallback = (error: unknown, result: unknown) => void;
25
+ type QueryFunction = (...args: any[]) => any;
26
+ type AttributeValue = string | number | boolean;
27
+ type AttributeMap = Record<string, AttributeValue>;
28
+
29
+ interface InstrumentableObject {
30
+ [key: string]: any;
31
+ [INSTRUMENTED_FLAG]?: true;
32
+ }
33
+
34
+ interface DrizzleClientLike extends InstrumentableObject {
35
+ query?: QueryFunction;
36
+ execute?: QueryFunction;
37
+ }
38
+
39
+ interface DrizzleSessionLike extends InstrumentableObject {
40
+ query?: QueryFunction;
41
+ execute?: QueryFunction;
42
+ prepareQuery?: QueryFunction;
43
+ transaction?: QueryFunction;
44
+ }
45
+
46
+ interface DrizzleDbLike extends InstrumentableObject {
47
+ $client?: DrizzleClientLike;
48
+ session?: DrizzleSessionLike;
49
+ _?: {
50
+ session?: DrizzleSessionLike;
51
+ [key: string]: any;
52
+ };
53
+ }
54
+
55
+ export interface InstrumentDrizzleConfig {
56
+ tracerName?: string;
57
+ dbSystem?: string;
58
+ dbName?: string;
59
+ captureQueryText?: boolean;
60
+ maxQueryTextLength?: number;
61
+ peerName?: string;
62
+ peerPort?: number;
63
+ }
64
+
65
+ interface ResolvedConfig {
66
+ tracerName: string;
67
+ dbSystem: string;
68
+ dbName?: string;
69
+ captureQueryText: boolean;
70
+ maxQueryTextLength: number;
71
+ peerName?: string;
72
+ peerPort?: number;
73
+ }
74
+
75
+ interface InstrumentationState {
76
+ tracer: ReturnType<typeof trace.getTracer>;
77
+ config: ResolvedConfig;
78
+ }
79
+
80
+ interface MethodInstrumentationOptions {
81
+ flagSuffix: string;
82
+ queryText: (args: any[]) => string | undefined;
83
+ callbackStyle?: 'last-arg';
84
+ extraAttributes?: AttributeMap;
85
+ }
86
+
87
+ function resolveConfig(config?: InstrumentDrizzleConfig): ResolvedConfig {
88
+ return {
89
+ tracerName: config?.tracerName ?? DEFAULT_TRACER_NAME,
90
+ dbSystem: config?.dbSystem ?? DEFAULT_DB_SYSTEM,
91
+ dbName: config?.dbName,
92
+ captureQueryText: config?.captureQueryText ?? true,
93
+ maxQueryTextLength: config?.maxQueryTextLength ?? 1000,
94
+ peerName: config?.peerName,
95
+ peerPort: config?.peerPort,
96
+ };
97
+ }
98
+
99
+ function getState(config?: InstrumentDrizzleConfig): InstrumentationState {
100
+ const resolved = resolveConfig(config);
101
+ return {
102
+ config: resolved,
103
+ tracer: trace.getTracer(resolved.tracerName),
104
+ };
105
+ }
106
+
107
+ function getFlagKey(suffix: string): string {
108
+ return `${INSTRUMENTED_FLAG}:${suffix}`;
109
+ }
110
+
111
+ function isObject(value: unknown): value is InstrumentableObject {
112
+ return value !== null && typeof value === 'object';
113
+ }
114
+
115
+ function isPromiseLike<T>(value: T | PromiseLike<T>): value is PromiseLike<T> {
116
+ return (
117
+ typeof value === 'object' &&
118
+ value !== null &&
119
+ typeof (value as PromiseLike<T>).then === 'function'
120
+ );
121
+ }
122
+
123
+ function extractQueryText(queryArg: unknown): string | undefined {
124
+ if (typeof queryArg === 'string') {
125
+ return queryArg;
126
+ }
127
+
128
+ if (!isObject(queryArg)) {
129
+ return undefined;
130
+ }
131
+
132
+ if (typeof queryArg.sql === 'string') {
133
+ return queryArg.sql;
134
+ }
135
+
136
+ if (typeof queryArg.text === 'string') {
137
+ return queryArg.text;
138
+ }
139
+
140
+ if (typeof queryArg.queryString === 'string') {
141
+ return queryArg.queryString;
142
+ }
143
+
144
+ if (
145
+ isObject(queryArg.queryChunks) &&
146
+ typeof (queryArg as Record<string, unknown>).sql === 'string'
147
+ ) {
148
+ return queryArg.sql as string;
149
+ }
150
+
151
+ return undefined;
152
+ }
153
+
154
+ function sanitizeQueryText(queryText: string, maxLength: number): string {
155
+ if (queryText.length <= maxLength) {
156
+ return queryText;
157
+ }
158
+
159
+ return `${queryText.slice(0, Math.max(0, maxLength))}...`;
160
+ }
161
+
162
+ function extractOperation(queryText: string): string | undefined {
163
+ const trimmed = queryText.trimStart();
164
+ const match = /^(?<operation>\w+)/u.exec(trimmed);
165
+ return match?.groups?.operation?.toUpperCase();
166
+ }
167
+
168
+ function buildSpan(
169
+ state: InstrumentationState,
170
+ queryText: string | undefined,
171
+ extraAttributes?: AttributeMap,
172
+ ) {
173
+ const operation = queryText ? extractOperation(queryText) : undefined;
174
+ const spanName = operation
175
+ ? `drizzle.${operation.toLowerCase()}`
176
+ : 'drizzle.query';
177
+ const span = state.tracer.startSpan(spanName, { kind: SpanKind.CLIENT });
178
+
179
+ span.setAttribute(SEMATTRS_DB_SYSTEM, state.config.dbSystem);
180
+
181
+ if (operation) {
182
+ span.setAttribute(SEMATTRS_DB_OPERATION, operation);
183
+ }
184
+
185
+ if (state.config.dbName !== undefined) {
186
+ span.setAttribute(SEMATTRS_DB_NAME, state.config.dbName);
187
+ }
188
+
189
+ if (state.config.captureQueryText && queryText !== undefined) {
190
+ span.setAttribute(
191
+ SEMATTRS_DB_STATEMENT,
192
+ sanitizeQueryText(queryText, state.config.maxQueryTextLength),
193
+ );
194
+ }
195
+
196
+ if (state.config.peerName !== undefined) {
197
+ span.setAttribute(SEMATTRS_NET_PEER_NAME, state.config.peerName);
198
+ }
199
+
200
+ if (state.config.peerPort !== undefined) {
201
+ span.setAttribute(SEMATTRS_NET_PEER_PORT, state.config.peerPort);
202
+ }
203
+
204
+ if (extraAttributes) {
205
+ for (const [key, value] of Object.entries(extraAttributes)) {
206
+ span.setAttribute(key, value);
207
+ }
208
+ }
209
+
210
+ return span;
211
+ }
212
+
213
+ function executeWithSpan<T>(span: any, fn: () => T): T {
214
+ return runWithSpan(span, () => {
215
+ try {
216
+ const result = fn();
217
+
218
+ if (isPromiseLike(result)) {
219
+ return result.then(
220
+ (value) => {
221
+ finalizeSpan(span);
222
+ return value;
223
+ },
224
+ (error) => {
225
+ finalizeSpan(span, error);
226
+ throw error;
227
+ },
228
+ ) as T;
229
+ }
230
+
231
+ finalizeSpan(span);
232
+ return result;
233
+ } catch (error) {
234
+ finalizeSpan(span, error);
235
+ throw error;
236
+ }
237
+ });
238
+ }
239
+
240
+ function instrumentMethod(
241
+ target: InstrumentableObject,
242
+ methodName: string,
243
+ state: InstrumentationState,
244
+ options: MethodInstrumentationOptions,
245
+ ): boolean {
246
+ if (typeof target[methodName] !== 'function') {
247
+ return false;
248
+ }
249
+
250
+ const flagKey = getFlagKey(options.flagSuffix);
251
+ if (target[flagKey]) {
252
+ return false;
253
+ }
254
+
255
+ const originalMethod = target[methodName] as QueryFunction;
256
+
257
+ target[methodName] = function instrumentedMethod(
258
+ this: any,
259
+ ...incomingArgs: any[]
260
+ ) {
261
+ const args = [...incomingArgs];
262
+ const callback =
263
+ options.callbackStyle === 'last-arg' && typeof args.at(-1) === 'function'
264
+ ? (args.pop() as QueryCallback)
265
+ : undefined;
266
+ const span = buildSpan(
267
+ state,
268
+ options.queryText(args),
269
+ options.extraAttributes,
270
+ );
271
+
272
+ if (callback) {
273
+ return runWithSpan(span, () => {
274
+ const wrappedCallback: QueryCallback = (error, result) => {
275
+ finalizeSpan(span, error);
276
+ callback(error, result);
277
+ };
278
+
279
+ try {
280
+ return Reflect.apply(originalMethod, this, [
281
+ ...args,
282
+ wrappedCallback,
283
+ ]);
284
+ } catch (error) {
285
+ finalizeSpan(span, error);
286
+ throw error;
287
+ }
288
+ });
289
+ }
290
+
291
+ return executeWithSpan(span, () =>
292
+ Reflect.apply(originalMethod, this, args),
293
+ );
294
+ };
295
+
296
+ target[flagKey] = true;
297
+ return true;
298
+ }
299
+
300
+ function instrumentPreparedQuery(
301
+ prepared: unknown,
302
+ state: InstrumentationState,
303
+ querySource: unknown,
304
+ extraAttributes?: AttributeMap,
305
+ ): boolean {
306
+ if (!isObject(prepared)) {
307
+ return false;
308
+ }
309
+
310
+ let instrumented = false;
311
+ const queryText = extractQueryText(querySource);
312
+
313
+ for (const methodName of PREPARED_QUERY_METHODS) {
314
+ instrumented =
315
+ instrumentMethod(prepared, methodName, state, {
316
+ flagSuffix: `prepared:${methodName}`,
317
+ queryText: () => queryText,
318
+ extraAttributes,
319
+ }) || instrumented;
320
+ }
321
+
322
+ return instrumented;
323
+ }
324
+
325
+ function instrumentPrepareQuery(
326
+ target: DrizzleSessionLike,
327
+ state: InstrumentationState,
328
+ extraAttributes?: AttributeMap,
329
+ ): boolean {
330
+ if (typeof target.prepareQuery !== 'function') {
331
+ return false;
332
+ }
333
+
334
+ const flagKey = getFlagKey('prepareQuery');
335
+ if (target[flagKey]) {
336
+ return false;
337
+ }
338
+
339
+ const originalPrepareQuery = target.prepareQuery;
340
+
341
+ target.prepareQuery = function instrumentedPrepareQuery(
342
+ this: any,
343
+ ...prepareArgs: any[]
344
+ ) {
345
+ const prepared = Reflect.apply(originalPrepareQuery, this, prepareArgs);
346
+ instrumentPreparedQuery(prepared, state, prepareArgs[0], extraAttributes);
347
+ return prepared;
348
+ };
349
+
350
+ target[flagKey] = true;
351
+ return true;
352
+ }
353
+
354
+ function instrumentTransactionTarget(
355
+ target: unknown,
356
+ state: InstrumentationState,
357
+ ): boolean {
358
+ if (!isObject(target)) {
359
+ return false;
360
+ }
361
+
362
+ const transactionAttributes = { 'db.transaction': true };
363
+ let instrumented = false;
364
+
365
+ instrumented =
366
+ instrumentMethod(target, 'query', state, {
367
+ flagSuffix: 'transaction:query',
368
+ queryText: (args) => extractQueryText(args[0]),
369
+ callbackStyle: 'last-arg',
370
+ extraAttributes: transactionAttributes,
371
+ }) || instrumented;
372
+
373
+ instrumented =
374
+ instrumentMethod(target, 'execute', state, {
375
+ flagSuffix: 'transaction:execute',
376
+ queryText: (args) => extractQueryText(args[0]),
377
+ callbackStyle: 'last-arg',
378
+ extraAttributes: transactionAttributes,
379
+ }) || instrumented;
380
+
381
+ instrumented =
382
+ instrumentPrepareQuery(
383
+ target as DrizzleSessionLike,
384
+ state,
385
+ transactionAttributes,
386
+ ) || instrumented;
387
+
388
+ if (isObject(target.session)) {
389
+ instrumented =
390
+ instrumentTransactionTarget(target.session, state) || instrumented;
391
+ }
392
+
393
+ if (isObject(target._?.session)) {
394
+ instrumented =
395
+ instrumentTransactionTarget(target._.session, state) || instrumented;
396
+ }
397
+
398
+ return instrumented;
399
+ }
400
+
401
+ function instrumentSession(
402
+ session: DrizzleSessionLike,
403
+ state: InstrumentationState,
404
+ ): boolean {
405
+ let instrumented = false;
406
+
407
+ instrumented =
408
+ instrumentMethod(session, 'query', state, {
409
+ flagSuffix: 'session:query',
410
+ queryText: (args) => extractQueryText(args[0]),
411
+ callbackStyle: 'last-arg',
412
+ }) || instrumented;
413
+
414
+ instrumented =
415
+ instrumentMethod(session, 'execute', state, {
416
+ flagSuffix: 'session:execute',
417
+ queryText: (args) => extractQueryText(args[0]),
418
+ callbackStyle: 'last-arg',
419
+ }) || instrumented;
420
+
421
+ instrumented = instrumentPrepareQuery(session, state) || instrumented;
422
+
423
+ if (typeof session.transaction === 'function') {
424
+ const flagKey = getFlagKey('session:transaction');
425
+
426
+ if (!session[flagKey]) {
427
+ const originalTransaction = session.transaction;
428
+
429
+ session.transaction = function instrumentedTransaction(
430
+ this: any,
431
+ callback: QueryFunction,
432
+ ...restArgs: any[]
433
+ ) {
434
+ if (typeof callback !== 'function') {
435
+ return Reflect.apply(originalTransaction, this, [
436
+ callback,
437
+ ...restArgs,
438
+ ]);
439
+ }
440
+
441
+ const wrappedCallback = (tx: unknown, ...callbackArgs: any[]) => {
442
+ instrumentTransactionTarget(tx, state);
443
+ return Reflect.apply(callback, this, [tx, ...callbackArgs]);
444
+ };
445
+
446
+ return Reflect.apply(originalTransaction, this, [
447
+ wrappedCallback,
448
+ ...restArgs,
449
+ ]);
450
+ };
451
+
452
+ session[flagKey] = true;
453
+ instrumented = true;
454
+ }
455
+ }
456
+
457
+ if (instrumented) {
458
+ session[INSTRUMENTED_FLAG] = true;
459
+ }
460
+
461
+ return instrumented;
462
+ }
463
+
464
+ export function instrumentDrizzle<TClient extends DrizzleClientLike>(
465
+ client: TClient,
466
+ config?: InstrumentDrizzleConfig,
467
+ ): TClient {
468
+ if (!client || !isObject(client)) {
469
+ return client;
470
+ }
471
+
472
+ const state = getState(config);
473
+ let instrumented = false;
474
+
475
+ instrumented =
476
+ instrumentMethod(client, 'query', state, {
477
+ flagSuffix: 'client:query',
478
+ queryText: (args) => extractQueryText(args[0]),
479
+ callbackStyle: 'last-arg',
480
+ }) || instrumented;
481
+
482
+ instrumented =
483
+ instrumentMethod(client, 'execute', state, {
484
+ flagSuffix: 'client:execute',
485
+ queryText: (args) => extractQueryText(args[0]),
486
+ callbackStyle: 'last-arg',
487
+ }) || instrumented;
488
+
489
+ if (instrumented) {
490
+ client[INSTRUMENTED_FLAG] = true;
491
+ }
492
+
493
+ return client;
494
+ }
495
+
496
+ export function instrumentDrizzleClient<TDb extends DrizzleDbLike>(
497
+ db: TDb,
498
+ config?: InstrumentDrizzleConfig,
499
+ ): TDb {
500
+ if (!db || !isObject(db)) {
501
+ return db;
502
+ }
503
+
504
+ if (db[INSTRUMENTED_FLAG]) {
505
+ return db;
506
+ }
507
+
508
+ const state = getState(config);
509
+ let instrumented = false;
510
+
511
+ instrumented =
512
+ instrumentSession(db as unknown as DrizzleSessionLike, state) ||
513
+ instrumented;
514
+
515
+ if (isObject(db.session)) {
516
+ instrumented = instrumentSession(db.session, state) || instrumented;
517
+ }
518
+
519
+ if (isObject(db._?.session)) {
520
+ instrumented = instrumentSession(db._.session, state) || instrumented;
521
+ }
522
+
523
+ if (isObject(db.$client)) {
524
+ instrumentDrizzle(db.$client, config);
525
+ instrumented = Boolean(db.$client[INSTRUMENTED_FLAG]) || instrumented;
526
+ }
527
+
528
+ if (instrumented) {
529
+ db[INSTRUMENTED_FLAG] = true;
530
+ }
531
+
532
+ return db;
533
+ }
package/src/index.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Autotel Drizzle - OpenTelemetry instrumentation for Drizzle ORM
3
+ *
4
+ * This package provides instrumentation for Drizzle ORM.
5
+ *
6
+ * Philosophy:
7
+ * Only include plugins for libraries that either:
8
+ * 1. Have NO official instrumentation (e.g., Drizzle ORM)
9
+ * 2. Have BROKEN official instrumentation (e.g., Mongoose in ESM+tsx)
10
+ * 3. Add SIGNIFICANT value beyond official packages (e.g., Kafka processing spans)
11
+ *
12
+ * For databases/ORMs with working official instrumentation, use those directly with the --import pattern:
13
+ * - MongoDB: @opentelemetry/instrumentation-mongodb
14
+ * - PostgreSQL: @opentelemetry/instrumentation-pg
15
+ * - MySQL: @opentelemetry/instrumentation-mysql2
16
+ * - Redis: @opentelemetry/instrumentation-redis
17
+ * - Kafka: @opentelemetry/instrumentation-kafkajs (use with autotel-plugins/kafka for processing spans)
18
+ *
19
+ * See: https://github.com/open-telemetry/opentelemetry-js-contrib
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Drizzle manual instrumentation
24
+ * import { instrumentDrizzleClient } from 'autotel-drizzle';
25
+ * import { drizzle } from 'drizzle-orm/node-postgres';
26
+ *
27
+ * const db = instrumentDrizzleClient(drizzle(pool));
28
+ * ```
29
+ *
30
+ * @packageDocumentation
31
+ */
32
+
33
+ // Re-export common semantic conventions
34
+ export {
35
+ SEMATTRS_DB_SYSTEM,
36
+ SEMATTRS_DB_SYSTEM_NAME,
37
+ SEMATTRS_DB_OPERATION,
38
+ SEMATTRS_DB_OPERATION_NAME,
39
+ SEMATTRS_DB_STATEMENT,
40
+ SEMATTRS_DB_NAME,
41
+ SEMATTRS_DB_NAMESPACE,
42
+ SEMATTRS_DB_COLLECTION_NAME,
43
+ SEMATTRS_DB_QUERY_TEXT,
44
+ SEMATTRS_DB_QUERY_SUMMARY,
45
+ SEMATTRS_NET_PEER_NAME,
46
+ SEMATTRS_NET_PEER_PORT,
47
+ SEMATTRS_GCP_BIGQUERY_JOB_ID,
48
+ SEMATTRS_GCP_BIGQUERY_JOB_LOCATION,
49
+ SEMATTRS_GCP_BIGQUERY_PROJECT_ID,
50
+ SEMATTRS_GCP_BIGQUERY_DESTINATION_TABLE,
51
+ SEMATTRS_GCP_BIGQUERY_SOURCE_TABLES,
52
+ SEMATTRS_GCP_BIGQUERY_STATEMENT_TYPE,
53
+ SEMATTRS_GCP_BIGQUERY_QUERY_HASH,
54
+ SEMATTRS_GCP_BIGQUERY_ROWS_AFFECTED,
55
+ SEMATTRS_GCP_BIGQUERY_ROWS_RETURNED,
56
+ SEMATTRS_GCP_BIGQUERY_SCHEMA_FIELDS,
57
+ } from './common/constants';
58
+
59
+ // Re-export Drizzle plugin
60
+ export {
61
+ instrumentDrizzle,
62
+ instrumentDrizzleClient,
63
+ type InstrumentDrizzleConfig,
64
+ } from './drizzle';