update docs

This commit is contained in:
Alex Holovach
2025-10-05 08:19:58 -05:00
parent e77c63390a
commit 7b7dc83f7d
2 changed files with 447 additions and 293 deletions

View File

@@ -42,118 +42,149 @@ Works with any observability platform that supports OpenTelemetry including:
## Usage ## Usage
There are two ways to instrument Drizzle ORM with OpenTelemetry: ### Instrument Your Drizzle Database (Recommended)
### Option 1: Instrument the Connection Pool (Recommended) Use `instrumentDrizzleClient()` to add tracing to your Drizzle database instance. This is the simplest and most straightforward approach:
Wrap your database connection pool with `instrumentDrizzle()` before passing it to Drizzle:
```typescript ```typescript
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/postgres-js";
import { Pool } from "pg"; import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
import { instrumentDrizzle } from "@kubiks/otel-drizzle";
const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Create your Drizzle database instance as usual
const instrumentedPool = instrumentDrizzle(pool); const db = drizzle(process.env.DATABASE_URL!);
const db = drizzle(instrumentedPool);
// Add instrumentation with a single line
instrumentDrizzleClient(db);
// That's it! All queries are now traced automatically // That's it! All queries are now traced automatically
const users = await db.select().from(usersTable); const users = await db.select().from(usersTable);
``` ```
### Option 2: Instrument an Existing Drizzle Client ### Database-Specific Examples
If you already have a Drizzle database instance or don't have access to the underlying pool, use `instrumentDrizzleClient()`. This method instruments the database at the session level, capturing all query operations: #### PostgreSQL
```typescript ```typescript
// Works with postgres-js (Postgres.js) // PostgreSQL with postgres.js (recommended for serverless)
import { drizzle } from "drizzle-orm/postgres-js"; import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { instrumentDrizzleClient } from "@kubiks/otel-drizzle"; import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
import * as schema from "./schema";
const db = drizzle(process.env.DATABASE_URL!, { schema }); // Using connection string directly
const db = drizzle(process.env.DATABASE_URL!);
instrumentDrizzleClient(db, { dbSystem: "postgresql" });
// Instrument the existing database instance // Or with a client instance
instrumentDrizzleClient(db); const queryClient = postgres(process.env.DATABASE_URL!);
const db = drizzle({ client: queryClient });
// All queries are now traced automatically
const users = await db.select().from(schema.users);
// Direct execute calls are also traced
await db.execute("SELECT * FROM users");
// Transactions are also traced
await db.transaction(async (tx) => {
await tx.insert(schema.users).values({ name: "John" });
});
```
### Optional Configuration
Both instrumentation methods accept the same configuration options:
```typescript
// Option 1: Instrument the pool
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const instrumentedPool = instrumentDrizzle(pool, {
dbSystem: "postgresql", // Database type (default: 'postgresql')
dbName: "myapp", // Database name for spans
captureQueryText: true, // Include SQL in traces (default: true)
maxQueryTextLength: 1000, // Max SQL length (default: 1000)
peerName: "db.example.com", // Database server hostname
peerPort: 5432, // Database server port
});
const db = drizzle(instrumentedPool);
// Option 2: Instrument the Drizzle client
const db = drizzle(pool, { schema });
instrumentDrizzleClient(db, { instrumentDrizzleClient(db, {
dbSystem: "postgresql", dbSystem: "postgresql",
dbName: "myapp", dbName: "myapp",
captureQueryText: true,
peerName: "db.example.com", peerName: "db.example.com",
peerPort: 5432, peerPort: 5432,
}); });
``` ```
### Works with All Drizzle-Supported Databases
This package automatically detects and instruments **all databases that Drizzle ORM supports**. It works by detecting whether your database driver uses a `query` or `execute` method and instrumenting it appropriately. This includes:
- **PostgreSQL** (node-postgres, postgres.js, Neon, Vercel Postgres, etc.)
- **MySQL** (mysql2, PlanetScale, TiDB, etc.)
- **SQLite** (better-sqlite3, LibSQL/Turso, Cloudflare D1, etc.)
- **And any other Drizzle-supported database**
```typescript ```typescript
// PostgreSQL with postgres-js (Postgres.js) - use instrumentDrizzleClient // PostgreSQL with node-postgres (pg)
import { drizzle } from "drizzle-orm/postgres-js";
const db = drizzle(process.env.DATABASE_URL!);
instrumentDrizzleClient(db);
// PostgreSQL with node-postgres (pg) - use instrumentDrizzle on pool
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg"; import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL }); import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
const db = drizzle(instrumentDrizzle(pool));
// MySQL with mysql2 (uses 'execute' or 'query' method) // Using connection string directly
const db = drizzle(process.env.DATABASE_URL!);
instrumentDrizzleClient(db, { dbSystem: "postgresql" });
// Or with a pool instance
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle({ client: pool });
instrumentDrizzleClient(db, { dbSystem: "postgresql" });
```
#### MySQL
```typescript
// MySQL with mysql2
import { drizzle } from "drizzle-orm/mysql2"; import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise"; import mysql from "mysql2/promise";
const connection = await mysql.createConnection(process.env.DATABASE_URL); import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
const db = drizzle(instrumentDrizzle(connection, { dbSystem: "mysql" }));
// Using connection string directly
const db = drizzle(process.env.DATABASE_URL!);
instrumentDrizzleClient(db, { dbSystem: "mysql" });
// Or with a connection instance
const connection = await mysql.createConnection({
host: "localhost",
user: "root",
database: "mydb",
// ... other connection options
});
const db = drizzle({ client: connection });
instrumentDrizzleClient(db, {
dbSystem: "mysql",
dbName: "mydb",
peerName: "localhost",
peerPort: 3306,
});
```
#### SQLite
```typescript
// SQLite with better-sqlite3 // SQLite with better-sqlite3
import { drizzle } from "drizzle-orm/better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
const sqlite = new Database("database.db"); import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
const db = drizzle(instrumentDrizzle(sqlite, { dbSystem: "sqlite" }));
// LibSQL/Turso (uses 'execute' method) // Using file path directly
const db = drizzle("sqlite.db");
instrumentDrizzleClient(db, { dbSystem: "sqlite" });
// Or with a Database instance
const sqlite = new Database("sqlite.db");
const db = drizzle({ client: sqlite });
instrumentDrizzleClient(db, { dbSystem: "sqlite" });
```
```typescript
// SQLite with LibSQL/Turso
import { drizzle } from "drizzle-orm/libsql"; import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client"; import { createClient } from "@libsql/client";
const client = createClient({ url: "...", authToken: "..." }); import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
const db = drizzle(instrumentDrizzle(client, { dbSystem: "sqlite" }));
// Using connection config directly
const db = drizzle({
connection: {
url: process.env.DATABASE_URL!,
authToken: process.env.DATABASE_AUTH_TOKEN,
}
});
instrumentDrizzleClient(db, { dbSystem: "sqlite" });
// Or with a client instance
const client = createClient({
url: process.env.DATABASE_URL!,
authToken: process.env.DATABASE_AUTH_TOKEN,
});
const db = drizzle({ client });
instrumentDrizzleClient(db, { dbSystem: "sqlite" });
``` ```
### Configuration Options
```typescript
instrumentDrizzleClient(db, {
dbSystem: "postgresql", // Database type: 'postgresql' | 'mysql' | 'sqlite' (default: 'postgresql')
dbName: "myapp", // Database name for spans
captureQueryText: true, // Include SQL in traces (default: true)
maxQueryTextLength: 1000, // Max SQL length (default: 1000)
peerName: "db.example.com", // Database server hostname
peerPort: 5432, // Database server port
});
```
## What You Get ## What You Get
Each database query automatically creates a span with rich telemetry data: Each database query automatically creates a span with rich telemetry data:

View File

@@ -143,16 +143,15 @@ function finalizeSpan(span: Span, error?: unknown): void {
} }
/** /**
* Instruments a database connection pool or client with OpenTelemetry tracing. * Instruments a database connection pool/client with OpenTelemetry tracing.
* *
* This function wraps the connection's `query` or `execute` method to automatically create * This function wraps the connection's `query` and `execute` methods to create spans for each database
* spans for each database operation. It automatically detects which method is available * operation.
* and instruments it appropriately for any database driver. * The instrumentation is idempotent - calling it multiple times on the same connection will only
* The instrumentation is idempotent - calling it multiple times on the same * instrument it once.
* connection will only instrument it once.
* *
* @typeParam TClient - The type of the database connection pool or client * @typeParam TClient - The type of the database connection pool or client
* @param client - The database connection pool or client to instrument (e.g., pg Pool, mysql2 Connection, LibSQL Client) * @param client - The database connection pool or client to instrument
* @param config - Optional configuration for instrumentation behavior * @param config - Optional configuration for instrumentation behavior
* @returns The instrumented pool/client (same instance, modified in place) * @returns The instrumented pool/client (same instance, modified in place)
* *
@@ -164,19 +163,13 @@ function finalizeSpan(span: Span, error?: unknown): void {
* import { instrumentDrizzle } from '@kubiks/otel-drizzle'; * import { instrumentDrizzle } from '@kubiks/otel-drizzle';
* *
* const pool = new Pool({ connectionString: process.env.DATABASE_URL }); * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
* const instrumentedPool = instrumentDrizzle(pool);
* const db = drizzle(instrumentedPool);
*
* // With custom configuration
* const instrumentedPool = instrumentDrizzle(pool, { * const instrumentedPool = instrumentDrizzle(pool, {
* dbSystem: 'postgresql', * dbSystem: 'postgresql',
* dbName: 'myapp', * dbName: 'myapp',
* captureQueryText: true,
* maxQueryTextLength: 1000,
* peerName: 'db.example.com', * peerName: 'db.example.com',
* peerPort: 5432, * peerPort: 5432,
* }); * });
* const db = drizzle(instrumentedPool); * const db = drizzle({ client: instrumentedPool });
* ``` * ```
* *
* @example * @example
@@ -186,8 +179,13 @@ function finalizeSpan(span: Span, error?: unknown): void {
* import mysql from 'mysql2/promise'; * import mysql from 'mysql2/promise';
* import { instrumentDrizzle } from '@kubiks/otel-drizzle'; * import { instrumentDrizzle } from '@kubiks/otel-drizzle';
* *
* const connection = await mysql.createConnection(process.env.DATABASE_URL); * const connection = await mysql.createConnection({
* const db = drizzle(instrumentDrizzle(connection, { dbSystem: 'mysql' })); * host: 'localhost',
* user: 'root',
* database: 'mydb',
* });
* const instrumentedConnection = instrumentDrizzle(connection, { dbSystem: 'mysql' });
* const db = drizzle({ client: instrumentedConnection });
* ``` * ```
* *
* @example * @example
@@ -197,19 +195,24 @@ function finalizeSpan(span: Span, error?: unknown): void {
* import Database from 'better-sqlite3'; * import Database from 'better-sqlite3';
* import { instrumentDrizzle } from '@kubiks/otel-drizzle'; * import { instrumentDrizzle } from '@kubiks/otel-drizzle';
* *
* const sqlite = new Database('database.db'); * const sqlite = new Database('sqlite.db');
* const db = drizzle(instrumentDrizzle(sqlite, { dbSystem: 'sqlite' })); * const instrumentedSqlite = instrumentDrizzle(sqlite, { dbSystem: 'sqlite' });
* const db = drizzle({ client: instrumentedSqlite });
* ``` * ```
* *
* @example * @example
* ```typescript * ```typescript
* // LibSQL/Turso (automatically detects 'execute' method) * // LibSQL/Turso
* import { drizzle } from 'drizzle-orm/libsql'; * import { drizzle } from 'drizzle-orm/libsql';
* import { createClient } from '@libsql/client'; * import { createClient } from '@libsql/client';
* import { instrumentDrizzle } from '@kubiks/otel-drizzle'; * import { instrumentDrizzle } from '@kubiks/otel-drizzle';
* *
* const client = createClient({ url: '...', authToken: '...' }); * const client = createClient({
* const db = drizzle(instrumentDrizzle(client, { dbSystem: 'sqlite' })); * url: process.env.DATABASE_URL!,
* authToken: process.env.DATABASE_AUTH_TOKEN,
* });
* const instrumentedClient = instrumentDrizzle(client, { dbSystem: 'sqlite' });
* const db = drizzle({ client: instrumentedClient });
* ``` * ```
*/ */
export function instrumentDrizzle<TClient extends DrizzleClientLike>( export function instrumentDrizzle<TClient extends DrizzleClientLike>(
@@ -219,11 +222,11 @@ export function instrumentDrizzle<TClient extends DrizzleClientLike>(
if (!client) { if (!client) {
return client; return client;
} }
// Check if client has query or execute method // Check if client has query or execute method
const hasQuery = typeof client.query === "function"; const hasQuery = typeof client.query === "function";
const hasExecute = typeof client.execute === "function"; const hasExecute = typeof client.execute === "function";
if (!hasQuery && !hasExecute) { if (!hasQuery && !hasExecute) {
return client; return client;
} }
@@ -243,7 +246,7 @@ export function instrumentDrizzle<TClient extends DrizzleClientLike>(
} = config ?? {}; } = config ?? {};
const tracer = trace.getTracer(tracerName); const tracer = trace.getTracer(tracerName);
// Store the original method (query or execute) // Store the original method (query or execute)
const methodName = hasQuery ? "query" : "execute"; const methodName = hasQuery ? "query" : "execute";
const originalMethod = hasQuery ? client.query : client.execute; const originalMethod = hasQuery ? client.query : client.execute;
@@ -338,7 +341,7 @@ export function instrumentDrizzle<TClient extends DrizzleClientLike>(
}; };
client[INSTRUMENTED_FLAG] = true; client[INSTRUMENTED_FLAG] = true;
// Replace the original method with the instrumented one // Replace the original method with the instrumented one
if (hasQuery) { if (hasQuery) {
client.query = instrumentedMethod; client.query = instrumentedMethod;
@@ -369,21 +372,10 @@ interface DrizzleDbLike {
} }
/** /**
* Instruments an already created Drizzle database instance with OpenTelemetry tracing. * Instruments a Drizzle database instance with OpenTelemetry tracing.
* *
* This function instruments the database at the session level, intercepting: * This function instruments the database at the session level, automatically tracing all database
* - `session.prepareQuery` - Used by all query builders (select, insert, update, delete) * operations including query builders, direct SQL execution, and transactions.
* - `session.query` - Used for direct SQL execution
* - `$client` methods - As a fallback for underlying connection
*
* This ensures all database operations are traced, whether you use:
* - Query builders: `db.select().from(table)`
* - Direct execution: `db.execute(sql)`
* - Transactions: `db.transaction()`
*
* This is useful when you have a Drizzle database instance created with
* `const db = drizzle(connectionString)` or `const db = drizzle(pool, { schema })`
* and want to add instrumentation to it without having direct access to the underlying pool.
* *
* The instrumentation is idempotent - calling it multiple times on the same * The instrumentation is idempotent - calling it multiple times on the same
* database will only instrument it once. * database will only instrument it once.
@@ -395,39 +387,109 @@ interface DrizzleDbLike {
* *
* @example * @example
* ```typescript * ```typescript
* // When you have a pre-created Drizzle database instance * // PostgreSQL with postgres.js
* import { drizzle } from 'drizzle-orm/node-postgres'; * import { drizzle } from 'drizzle-orm/postgres-js';
* import { Pool } from 'pg'; * import postgres from 'postgres';
* import { instrumentDrizzleClient } from '@kubiks/otel-drizzle'; * import { instrumentDrizzleClient } from '@kubiks/otel-drizzle';
* import * as schema from './schema';
* *
* const pool = new Pool({ connectionString: process.env.DATABASE_URL }); * // Using connection string
* const db = drizzle(pool, { schema }); * const db = drizzle(process.env.DATABASE_URL!);
* instrumentDrizzleClient(db, { dbSystem: 'postgresql' });
* *
* // Instrument the existing db instance * // Or with a client instance
* instrumentDrizzleClient(db, { * const queryClient = postgres(process.env.DATABASE_URL!);
* dbSystem: 'postgresql', * const db = drizzle({ client: queryClient });
* dbName: 'myapp', * instrumentDrizzleClient(db, { dbSystem: 'postgresql' });
* captureQueryText: true,
* peerName: 'db.example.com',
* peerPort: 5432,
* });
*
* // Now all queries through db are traced
* const users = await db.select().from(schema.users);
* ``` * ```
* *
* @example * @example
* ```typescript * ```typescript
* // Works with any Drizzle driver * // PostgreSQL with node-postgres (pg)
* import { drizzle } from 'drizzle-orm/node-postgres';
* import { Pool } from 'pg';
* import { instrumentDrizzleClient } from '@kubiks/otel-drizzle';
*
* // Using connection string
* const db = drizzle(process.env.DATABASE_URL!);
* instrumentDrizzleClient(db, { dbSystem: 'postgresql' });
*
* // Or with a pool
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
* const db = drizzle({ client: pool });
* instrumentDrizzleClient(db, {
* dbSystem: 'postgresql',
* dbName: 'myapp',
* peerName: 'db.example.com',
* peerPort: 5432,
* });
* ```
*
* @example
* ```typescript
* // MySQL with mysql2
* import { drizzle } from 'drizzle-orm/mysql2'; * import { drizzle } from 'drizzle-orm/mysql2';
* import mysql from 'mysql2/promise'; * import mysql from 'mysql2/promise';
* import { instrumentDrizzleClient } from '@kubiks/otel-drizzle'; * import { instrumentDrizzleClient } from '@kubiks/otel-drizzle';
* *
* const connection = await mysql.createConnection(process.env.DATABASE_URL); * // Using connection string
* const db = drizzle(connection); * const db = drizzle(process.env.DATABASE_URL!);
*
* instrumentDrizzleClient(db, { dbSystem: 'mysql' }); * instrumentDrizzleClient(db, { dbSystem: 'mysql' });
*
* // Or with a connection
* const connection = await mysql.createConnection({
* host: 'localhost',
* user: 'root',
* database: 'mydb',
* });
* const db = drizzle({ client: connection });
* instrumentDrizzleClient(db, {
* dbSystem: 'mysql',
* dbName: 'mydb',
* peerName: 'localhost',
* peerPort: 3306,
* });
* ```
*
* @example
* ```typescript
* // SQLite with better-sqlite3
* import { drizzle } from 'drizzle-orm/better-sqlite3';
* import Database from 'better-sqlite3';
* import { instrumentDrizzleClient } from '@kubiks/otel-drizzle';
*
* // Using file path
* const db = drizzle('sqlite.db');
* instrumentDrizzleClient(db, { dbSystem: 'sqlite' });
*
* // Or with a Database instance
* const sqlite = new Database('sqlite.db');
* const db = drizzle({ client: sqlite });
* instrumentDrizzleClient(db, { dbSystem: 'sqlite' });
* ```
*
* @example
* ```typescript
* // SQLite with LibSQL/Turso
* import { drizzle } from 'drizzle-orm/libsql';
* import { createClient } from '@libsql/client';
* import { instrumentDrizzleClient } from '@kubiks/otel-drizzle';
*
* // Using connection config
* const db = drizzle({
* connection: {
* url: process.env.DATABASE_URL!,
* authToken: process.env.DATABASE_AUTH_TOKEN,
* }
* });
* instrumentDrizzleClient(db, { dbSystem: 'sqlite' });
*
* // Or with a client instance
* const client = createClient({
* url: process.env.DATABASE_URL!,
* authToken: process.env.DATABASE_AUTH_TOKEN,
* });
* const db = drizzle({ client });
* instrumentDrizzleClient(db, { dbSystem: 'sqlite' });
* ``` * ```
*/ */
export function instrumentDrizzleClient<TDb extends DrizzleDbLike>( export function instrumentDrizzleClient<TDb extends DrizzleDbLike>(
@@ -460,23 +522,31 @@ export function instrumentDrizzleClient<TDb extends DrizzleDbLike>(
// This is where all queries actually go through // This is where all queries actually go through
if ((db as any).session && !instrumented) { if ((db as any).session && !instrumented) {
const session = (db as any).session; const session = (db as any).session;
// Check if session has prepareQuery method (used by select/insert/update/delete) // Check if session has prepareQuery method (used by select/insert/update/delete)
if (typeof session.prepareQuery === "function" && !session[INSTRUMENTED_FLAG]) { if (
typeof session.prepareQuery === "function" &&
!session[INSTRUMENTED_FLAG]
) {
const originalPrepareQuery = session.prepareQuery; const originalPrepareQuery = session.prepareQuery;
session.prepareQuery = function(...args: any[]) { session.prepareQuery = function (...args: any[]) {
const prepared = originalPrepareQuery.apply(this, args); const prepared = originalPrepareQuery.apply(this, args);
// Wrap the prepared query's execute method // Wrap the prepared query's execute method
if (prepared && typeof prepared.execute === "function") { if (prepared && typeof prepared.execute === "function") {
const originalPreparedExecute = prepared.execute; const originalPreparedExecute = prepared.execute;
prepared.execute = function(this: any, ...executeArgs: any[]) { prepared.execute = function (this: any, ...executeArgs: any[]) {
// Extract query information from the query object // Extract query information from the query object
const queryObj = args[0]; // The query object passed to prepareQuery const queryObj = args[0]; // The query object passed to prepareQuery
const queryText = queryObj?.sql || queryObj?.queryString || extractQueryText(queryObj); const queryText =
const operation = queryText ? extractOperation(queryText) : undefined; queryObj?.sql ||
queryObj?.queryString ||
extractQueryText(queryObj);
const operation = queryText
? extractOperation(queryText)
: undefined;
const spanName = operation const spanName = operation
? `drizzle.${operation.toLowerCase()}` ? `drizzle.${operation.toLowerCase()}`
: "drizzle.query"; : "drizzle.query";
@@ -494,7 +564,10 @@ export function instrumentDrizzleClient<TDb extends DrizzleDbLike>(
} }
if (captureQueryText && queryText !== undefined) { if (captureQueryText && queryText !== undefined) {
const sanitized = sanitizeQueryText(queryText, maxQueryTextLength); const sanitized = sanitizeQueryText(
queryText,
maxQueryTextLength,
);
span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized); span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
} }
@@ -528,20 +601,25 @@ export function instrumentDrizzleClient<TDb extends DrizzleDbLike>(
}); });
}; };
} }
return prepared; return prepared;
}; };
session[INSTRUMENTED_FLAG] = true; session[INSTRUMENTED_FLAG] = true;
instrumented = true; instrumented = true;
} }
// Also instrument direct query method if exists // Also instrument direct query method if exists
if (typeof session.query === "function" && !session[INSTRUMENTED_FLAG + "_query"]) { if (
typeof session.query === "function" &&
!session[INSTRUMENTED_FLAG + "_query"]
) {
const originalQuery = session.query; const originalQuery = session.query;
session.query = function(this: any, queryString: string, params: any[]) { session.query = function (this: any, queryString: string, params: any[]) {
const operation = queryString ? extractOperation(queryString) : undefined; const operation = queryString
? extractOperation(queryString)
: undefined;
const spanName = operation const spanName = operation
? `drizzle.${operation.toLowerCase()}` ? `drizzle.${operation.toLowerCase()}`
: "drizzle.query"; : "drizzle.query";
@@ -592,188 +670,233 @@ export function instrumentDrizzleClient<TDb extends DrizzleDbLike>(
} }
}); });
}; };
session[INSTRUMENTED_FLAG + "_query"] = true; session[INSTRUMENTED_FLAG + "_query"] = true;
instrumented = true; instrumented = true;
} }
// Instrument transaction method to ensure transaction sessions are also instrumented // Instrument transaction method to ensure transaction sessions are also instrumented
if (typeof session.transaction === "function" && !session[INSTRUMENTED_FLAG + "_transaction"]) { if (
typeof session.transaction === "function" &&
!session[INSTRUMENTED_FLAG + "_transaction"]
) {
const originalTransaction = session.transaction; const originalTransaction = session.transaction;
session.transaction = function(this: any, transactionCallback: any, ...restArgs: any[]) { session.transaction = function (
this: any,
transactionCallback: any,
...restArgs: any[]
) {
// Wrap the transaction callback to instrument the tx object // Wrap the transaction callback to instrument the tx object
const wrappedCallback = async function(tx: any) { const wrappedCallback = async function (tx: any) {
// Instrument the transaction's session if it has one // Instrument the transaction's session if it has one
if (tx && (tx.session || tx._?.session || tx)) { if (tx && (tx.session || tx._?.session || tx)) {
const txSession = tx.session || tx._?.session || tx; const txSession = tx.session || tx._?.session || tx;
// Instrument tx.execute if it exists
if (typeof tx.execute === "function" && !tx[INSTRUMENTED_FLAG + "_execute"]) {
const originalTxExecute = tx.execute;
tx.execute = function(this: any, ...executeArgs: any[]) {
const queryText = extractQueryText(executeArgs[0]);
const operation = queryText ? extractOperation(queryText) : undefined;
const spanName = operation
? `drizzle.${operation.toLowerCase()}`
: "drizzle.query";
// Start span // Instrument tx.execute if it exists
const span = tracer.startSpan(spanName, { kind: SpanKind.CLIENT }); if (
span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem); typeof tx.execute === "function" &&
span.setAttribute("db.transaction", true); !tx[INSTRUMENTED_FLAG + "_execute"]
) {
const originalTxExecute = tx.execute;
if (operation) { tx.execute = function (this: any, ...executeArgs: any[]) {
span.setAttribute(SEMATTRS_DB_OPERATION, operation); const queryText = extractQueryText(executeArgs[0]);
} const operation = queryText
? extractOperation(queryText)
: undefined;
const spanName = operation
? `drizzle.${operation.toLowerCase()}`
: "drizzle.query";
if (dbName) { // Start span
span.setAttribute(SEMATTRS_DB_NAME, dbName); const span = tracer.startSpan(spanName, {
} kind: SpanKind.CLIENT,
});
span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
span.setAttribute("db.transaction", true);
if (captureQueryText && queryText !== undefined) { if (operation) {
const sanitized = sanitizeQueryText(queryText, maxQueryTextLength); span.setAttribute(SEMATTRS_DB_OPERATION, operation);
span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized); }
}
if (peerName) { if (dbName) {
span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName); span.setAttribute(SEMATTRS_DB_NAME, dbName);
} }
if (peerPort) { if (captureQueryText && queryText !== undefined) {
span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort); const sanitized = sanitizeQueryText(
} queryText,
maxQueryTextLength,
);
span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
}
const activeContext = trace.setSpan(context.active(), span); if (peerName) {
span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
}
// Execute the query if (peerPort) {
return context.with(activeContext, () => { span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
try { }
const result = originalTxExecute.apply(this, executeArgs);
return Promise.resolve(result)
.then((value) => {
finalizeSpan(span);
return value;
})
.catch((error) => {
finalizeSpan(span, error);
throw error;
});
} catch (error) {
finalizeSpan(span, error);
throw error;
}
});
};
tx[INSTRUMENTED_FLAG + "_execute"] = true;
}
// Also instrument txSession.prepareQuery if it exists
if (typeof txSession.prepareQuery === "function" && !txSession[INSTRUMENTED_FLAG + "_tx"]) {
const originalTxPrepareQuery = txSession.prepareQuery;
txSession.prepareQuery = function(...prepareArgs: any[]) {
const prepared = originalTxPrepareQuery.apply(this, prepareArgs);
// Wrap the prepared query's execute method
if (prepared && typeof prepared.execute === "function") {
const originalPreparedExecute = prepared.execute;
prepared.execute = function(this: any, ...executeArgs: any[]) {
// Extract query information from the query object
const queryObj = prepareArgs[0]; // The query object passed to prepareQuery
const queryText = queryObj?.sql || queryObj?.queryString || extractQueryText(queryObj);
const operation = queryText ? extractOperation(queryText) : undefined;
const spanName = operation
? `drizzle.${operation.toLowerCase()}`
: "drizzle.query";
// Start span const activeContext = trace.setSpan(context.active(), span);
const span = tracer.startSpan(spanName, { kind: SpanKind.CLIENT });
span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
span.setAttribute("db.transaction", true);
if (operation) { // Execute the query
span.setAttribute(SEMATTRS_DB_OPERATION, operation); return context.with(activeContext, () => {
} try {
const result = originalTxExecute.apply(this, executeArgs);
if (dbName) { return Promise.resolve(result)
span.setAttribute(SEMATTRS_DB_NAME, dbName); .then((value) => {
} finalizeSpan(span);
return value;
if (captureQueryText && queryText !== undefined) { })
const sanitized = sanitizeQueryText(queryText, maxQueryTextLength); .catch((error) => {
span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized); finalizeSpan(span, error);
} throw error;
if (peerName) {
span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
}
if (peerPort) {
span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
}
const activeContext = trace.setSpan(context.active(), span);
// Execute the prepared query
return context.with(activeContext, () => {
try {
const result = originalPreparedExecute.apply(this, executeArgs);
return Promise.resolve(result)
.then((value) => {
finalizeSpan(span);
return value;
})
.catch((error) => {
finalizeSpan(span, error);
throw error;
});
} catch (error) {
finalizeSpan(span, error);
throw error;
}
}); });
}; } catch (error) {
finalizeSpan(span, error);
throw error;
} }
});
return prepared; };
};
tx[INSTRUMENTED_FLAG + "_execute"] = true;
txSession[INSTRUMENTED_FLAG + "_tx"] = true;
}
} }
// Call the original callback with the instrumented tx // Also instrument txSession.prepareQuery if it exists
return transactionCallback(tx); if (
}; typeof txSession.prepareQuery === "function" &&
!txSession[INSTRUMENTED_FLAG + "_tx"]
) {
const originalTxPrepareQuery = txSession.prepareQuery;
txSession.prepareQuery = function (...prepareArgs: any[]) {
const prepared = originalTxPrepareQuery.apply(
this,
prepareArgs,
);
// Wrap the prepared query's execute method
if (prepared && typeof prepared.execute === "function") {
const originalPreparedExecute = prepared.execute;
prepared.execute = function (
this: any,
...executeArgs: any[]
) {
// Extract query information from the query object
const queryObj = prepareArgs[0]; // The query object passed to prepareQuery
const queryText =
queryObj?.sql ||
queryObj?.queryString ||
extractQueryText(queryObj);
const operation = queryText
? extractOperation(queryText)
: undefined;
const spanName = operation
? `drizzle.${operation.toLowerCase()}`
: "drizzle.query";
// Start span
const span = tracer.startSpan(spanName, {
kind: SpanKind.CLIENT,
});
span.setAttribute(SEMATTRS_DB_SYSTEM, dbSystem);
span.setAttribute("db.transaction", true);
if (operation) {
span.setAttribute(SEMATTRS_DB_OPERATION, operation);
}
if (dbName) {
span.setAttribute(SEMATTRS_DB_NAME, dbName);
}
if (captureQueryText && queryText !== undefined) {
const sanitized = sanitizeQueryText(
queryText,
maxQueryTextLength,
);
span.setAttribute(SEMATTRS_DB_STATEMENT, sanitized);
}
if (peerName) {
span.setAttribute(SEMATTRS_NET_PEER_NAME, peerName);
}
if (peerPort) {
span.setAttribute(SEMATTRS_NET_PEER_PORT, peerPort);
}
const activeContext = trace.setSpan(context.active(), span);
// Execute the prepared query
return context.with(activeContext, () => {
try {
const result = originalPreparedExecute.apply(
this,
executeArgs,
);
return Promise.resolve(result)
.then((value) => {
finalizeSpan(span);
return value;
})
.catch((error) => {
finalizeSpan(span, error);
throw error;
});
} catch (error) {
finalizeSpan(span, error);
throw error;
}
});
};
}
return prepared;
};
txSession[INSTRUMENTED_FLAG + "_tx"] = true;
}
}
// Call the original callback with the instrumented tx
return transactionCallback(tx);
};
// Call the original transaction with the wrapped callback // Call the original transaction with the wrapped callback
return originalTransaction.apply(this, [wrappedCallback, ...restArgs]); return originalTransaction.apply(this, [wrappedCallback, ...restArgs]);
}; };
session[INSTRUMENTED_FLAG + "_transaction"] = true; session[INSTRUMENTED_FLAG + "_transaction"] = true;
instrumented = true; instrumented = true;
} }
} }
// Second priority: Try to instrument via $client
// This handles the underlying connection pool
if (db.$client && !instrumented) { if (db.$client && !instrumented) {
const client = db.$client; const client = db.$client;
// Check if client has query or execute function // Check if client has query or execute function
if (typeof client.query === "function" || typeof client.execute === "function") { if (
typeof client.query === "function" ||
typeof client.execute === "function"
) {
instrumentDrizzle(client, config); instrumentDrizzle(client, config);
instrumented = true; instrumented = true;
} }
} }
// Third priority: Try to instrument via session.execute as fallback // Third priority: Try to instrument via session.execute as fallback
if (db._ && db._.session && typeof db._.session.execute === "function" && !instrumented) { if (
db._ &&
db._.session &&
typeof db._.session.execute === "function" &&
!instrumented
) {
const session = db._.session; const session = db._.session;
// Check if already instrumented // Check if already instrumented
if (session[INSTRUMENTED_FLAG]) { if (session[INSTRUMENTED_FLAG]) {
return db; return db;