Relevant source files
The following files were used as context for generating this wiki page:
- drizzle-orm/src/tracing.ts
- drizzle-orm/src/sqlite-proxy/session.ts
- drizzle-orm/src/gel-core/dialect.ts
- drizzle-orm/src/libsql/session.ts
- drizzle-orm/src/neon-serverless/session.ts
- drizzle-orm/src/pg-core/session.ts
- drizzle-orm/src/gel-core/session.ts
- drizzle-orm/src/singlestore/session.ts
- drizzle-orm/src/pg-proxy/session.ts
- drizzle-orm/src/bun-sql/session.ts
- drizzle-orm/src/node-postgres/session.ts
- drizzle-orm/src/pg-core/query-builders/insert.ts
- drizzle-orm/src/pg-core/dialect.ts
- drizzle-orm/src/neon-http/session.ts
- drizzle-orm/src/planetscale-serverless/session.ts
- drizzle-orm/src/tidb-serverless/session.ts
- drizzle-orm/src/aws-data-api/pg/session.ts
- drizzle-orm/src/mysql2/session.ts
- drizzle-orm/src/singlestore-core/session.ts
- drizzle-orm/src/gel-core/query-builders/insert.ts
- drizzle-orm/src/logger.ts
- drizzle-orm/src/tracing-utils.ts
- drizzle-orm/src/op-sqlite/session.ts
- drizzle-orm/src/singlestore-proxy/session.ts
- drizzle-orm/src/pg-core/query-builders/query.ts
- drizzle-orm/src/sqlite-core/session.ts
- drizzle-orm/src/query-promise.ts
- drizzle-orm/src/prisma/pg/session.ts
- drizzle-orm/src/singlestore-core/query-builders/delete.ts
- drizzle-orm/src/mysql-core/session.ts
Overview
Drizzle ORM provides an integrated diagnostic infrastructure combining OpenTelemetry tracing and structured query logging to observe and monitor query execution lifecycle events. The tracing subsystem uses the @opentelemetry/api package to wrap operations in active spans like drizzle.operation, drizzle.prepareQuery, drizzle.buildSQL, drizzle.execute, drizzle.driver.execute, and drizzle.mapResponse, automatically capturing error statuses and ending spans when execution completes. Concurrently, session-level logging dispatches query strings and parameters through logger implementations such as DefaultLogger and ConsoleLogWriter, formatting parameters with JSON stringification. This dual architecture allows applications to track performance metrics, inspect parameterized query statements across various driver runtimes, and handle diagnostic errors during failed database interactions.
Sources: drizzle-orm/src/tracing.ts:1-55, drizzle-orm/src/logger.ts:1-39, drizzle-orm/src/pg-core/session.ts:195-207, drizzle-orm/src/pg-proxy/session.ts:114-154
OpenTelemetry tracing subsystem architecture
Tracer Configuration and Module Integration
The OpenTelemetry tracing integration centers around the internal tracer object exported from drizzle-orm/src/tracing.ts. The module declares optional references to the @opentelemetry/api package and a raw tracer instance. When active spans are requested, the subsystem lazy-loads or references the tracer via otel.trace.getTracer('drizzle-orm', npmVersion), utilizing the package version from ~/version.ts. If @opentelemetry/api is not present in the runtime environment, span wrapping primitives bypass OpenTelemetry entirely and execute the provided callback directly.
import type { Span, Tracer } from '@opentelemetry/api';
import { iife } from '~/tracing-utils.ts';
import { npmVersion } from '~/version.ts';
let otel: typeof import('@opentelemetry/api') | undefined;
let rawTracer: Tracer | undefined;
// try {
// otel = await import('@opentelemetry/api');
// } catch (err: any) {
// if (err.code !== 'MODULE_NOT_FOUND' && err.code !== 'ERR_MODULE_NOT_FOUND') {
// throw err;
// }
// }
type SpanName =
| 'drizzle.operation'
| 'drizzle.prepareQuery'
| 'drizzle.buildSQL'
| 'drizzle.execute'
| 'drizzle.driver.execute'
| 'drizzle.mapResponse';
/** @internal */
export const tracer = {
startActiveSpan<F extends (span?: Span) => unknown>(name: SpanName, fn: F): ReturnType<F> {
if (!otel) {
return fn() as ReturnType<F>;
}
if (!rawTracer) {
rawTracer = otel.trace.getTracer('drizzle-orm', npmVersion);
}Sources: drizzle-orm/src/tracing.ts:1-32
Note
If @opentelemetry/api is missing from the environment, tracer.startActiveSpan skips span creation and transparently executes the workload function without throwing module resolution errors.
Span Names and Wrapping Primitives
The tracing subsystem defines a strict set of permitted operation names managed by the SpanName type. These identifiers cover the end-to-end query lifecycle from high-level operations down to driver-specific execution and response mapping.
| Span Name | Description |
|---|---|
drizzle.operation | Top-level query builder or transactional operation wrapper |
drizzle.prepareQuery | Statement preparation and parameter binding compilation |
drizzle.buildSQL | SQL string generation and dialect operator construction |
drizzle.execute | Query execution boundary across database sessions |
drizzle.driver.execute | Low-level driver communication and transport layer invocation |
drizzle.mapResponse | Raw database response parsing and entity mapping |
type SpanName =
| 'drizzle.operation'
| 'drizzle.prepareQuery'
| 'drizzle.buildSQL'
| 'drizzle.execute'
| 'drizzle.driver.execute'
| 'drizzle.mapResponse';Sources: drizzle-orm/src/tracing.ts:15-21
Span wrapping is executed via the startActiveSpan method, which delegates to an immediately invoked function expression helper (iife) defined in drizzle-orm/src/tracing-utils.ts.
export function iife<T extends unknown[], U>(fn: (...args: T) => U, ...args: T): U {
return fn(...args);
}Sources: drizzle-orm/src/tracing-utils.ts:1-3
The span wrapper wraps the callback execution inside a try...catch...finally block. If an exception occurs, the span status is explicitly set to otel.SpanStatusCode.ERROR with the error message extracted from the thrown instance, before rethrowing the error and ending the span.
return iife(
(otel, rawTracer) =>
rawTracer.startActiveSpan(
name,
((span: Span) => {
try {
return fn(span);
} catch (e) {
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: e instanceof Error ? e.message : 'Unknown error', // eslint-disable-line no-instanceof/no-instanceof
});
throw e;
} finally {
span.end();
}
}) as F,
),
otel,
rawTracer,
);
},Sources: drizzle-orm/src/tracing.ts:34-55
Query builder span instrumentation
Relational and Core Query Builder Spans
Query builder methods integrate directly with the tracing subsystem by wrapping statement preparation and query execution inside active spans. In PostgreSQL relational query builders (PgRelationalQuery), the _prepare method wraps statement construction and relational mapping within a drizzle.prepareQuery span.
_prepare(name?: string): PgPreparedQuery<PreparedQueryConfig & { execute: TResult }> {
return tracer.startActiveSpan('drizzle.prepareQuery', () => {
const { query, builtQuery } = this._toSQL();
return this.session.prepareQuery<PreparedQueryConfig & { execute: TResult }>(
builtQuery,
undefined,
name,
true,
(rawRows, mapColumnValue) => {
const rows = rawRows.map((row) =>
mapRelationalRow(this.schema, this.tableConfig, row, query.selection, mapColumnValue)
);
if (this.mode === 'first') {
return rows[0] as TResult;
}
return rows as TResult;
},
);
});
}Sources: drizzle-orm/src/pg-core/query-builders/query.ts:90-110
When a relational query is executed via execute(), the operation is wrapped inside a drizzle.operation span before invoking the underlying prepared statement.
override execute(): Promise<TResult> {
return tracer.startActiveSpan('drizzle.operation', () => {
return this._prepare().execute(undefined, this.authToken);
});
}Sources: drizzle-orm/src/pg-core/query-builders/query.ts:152-156
Note
Relational execution relies on _prepare() internally, causing a nested trace structure where drizzle.operation invokes drizzle.prepareQuery during statement compilation.
Dialect Compilation and Execution Flow
Query building across different dialects separates syntax compilation from session execution. For instance, SingleStore delete query builders (SingleStoreDeleteBase) invoke dialect builders like buildDeleteQuery and sqlToQuery during getSQL and prepare calls, passing table metadata and returning clauses directly to the session.
getSQL(): SQL {
return this.dialect.buildDeleteQuery(this.config);
}
toSQL(): Query {
const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL());
return rest;
}
prepare(): SingleStoreDeletePrepare<this> {
return this.session.prepareQuery(
this.dialect.sqlToQuery(this.getSQL()),
this.config.returning,
undefined,
undefined,
undefined,
{
type: 'delete',
tables: extractUsedTable(this.config.table),
},
) as SingleStoreDeletePrepare<this>;
}Sources: drizzle-orm/src/singlestore-core/query-builders/delete.ts:176-197
Logger interface and default writers
Logger Contracts and Writers
The logging subsystem defines two core abstraction contracts: Logger, which exposes a logQuery method accepting a SQL query string and an array of unknown parameters, and LogWriter, which defines a simple write method for delivering text messages.
export interface Logger {
logQuery(query: string, params: unknown[]): void;
}
export interface LogWriter {
write(message: string): void;
}Sources: drizzle-orm/src/logger.ts:3-9
The built-in output implementation is ConsoleLogWriter, which implements LogWriter by forwarding messages directly to console.log. Both ConsoleLogWriter and DefaultLogger define static entityKind metadata identifiers for runtime type inspection.
export class ConsoleLogWriter implements LogWriter {
static readonly [entityKind]: string = 'ConsoleLogWriter';
write(message: string) {
console.log(message);
}
}Sources: drizzle-orm/src/logger.ts:11-17
DefaultLogger and Formatting Logic
DefaultLogger implements the Logger interface and accepts an optional configuration object containing a custom LogWriter. If no writer is supplied during instantiation, it defaults to a new instance of ConsoleLogWriter.
export class DefaultLogger implements Logger {
static readonly [entityKind]: string = 'DefaultLogger';
readonly writer: LogWriter;
constructor(config?: { writer: LogWriter }) {
this.writer = config?.writer ?? new ConsoleLogWriter();
}Sources: drizzle-orm/src/logger.ts:19-26
When logQuery is invoked, DefaultLogger serializes each query parameter into a JSON string using JSON.stringify. If serialization fails due to circular references or unsupported types, it falls back to standard String(p) coercion. The formatted parameter array is appended to the query string under the suffix -- params: [...] only when parameters are present, and the resulting payload is dispatched to the configured writer.
logQuery(query: string, params: unknown[]): void {
const stringifiedParams = params.map((p) => {
try {
return JSON.stringify(p);
} catch {
return String(p);
}
});
const paramsStr = stringifiedParams.length ? ` -- params: [${stringifiedParams.join(', ')}]` : '';
this.writer.write(`Query: ${query}${paramsStr}`);
}
}Sources: drizzle-orm/src/logger.ts:28-39
Tip
If JSON.stringify throws an error on complex parameter structures inside DefaultLogger.logQuery, the parameter falls back to String(p) rather than aborting query logging.
Additionally, NoopLogger provides a null-pattern implementation of Logger where logQuery performs no operations.
export class NoopLogger implements Logger {
static readonly [entityKind]: string = 'NoopLogger';
logQuery(): void {
// noop
}
}Sources: drizzle-orm/src/logger.ts:41-47
Session level query logging integration
Logger Dispatch Across Drivers
Database sessions and prepared query implementations across drivers coordinate with the logging subsystem by invoking logQuery on the configured Logger instance right before executing any statement. Each driver binds placeholders via fillPlaceholders and dispatches the interpolated query text and parameter array. For example, RemotePreparedQuery in the SQLite proxy driver invokes this.logger.logQuery(this.query.sql, params) across its run, all, get, and values execution methods prior to invoking the remote client callback.
async run(placeholderValues?: Record<string, unknown>): Promise<SqliteRemoteResult> {
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
this.logger.logQuery(this.query.sql, params);
return await this.queryWithCache(this.query.sql, params, async () => {
return await (this.client as AsyncRemoteCallback)(this.query.sql, params, 'run');
});
}Sources: drizzle-orm/src/sqlite-proxy/session.ts:177-183
Similarly, LibSQLPreparedQuery dispatches logger.logQuery(query.sql, params) across execution branches before executing statements through the underlying LibSQL client or active transaction.
async run(placeholderValues?: Record<string, unknown>): Promise<ResultSet> {
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
this.logger.logQuery(this.query.sql, params);
return await this.queryWithCache(this.query.sql, params, async () => {
const stmt: InStatement = { sql: this.query.sql, args: params as InArgs };
return this.tx ? this.tx.execute(stmt) : this.client.execute(stmt);
});
}Sources: drizzle-orm/src/libsql/session.ts:194-201
PostgreSQL and MySQL drivers follow the same dispatch contract. In NeonPreparedQuery, BunSQLPreparedQuery, NeonHttpPreparedQuery, TiDBServerlessPreparedQuery, and AwsDataApiPreparedQuery, parameter binding precedes logger.logQuery invocation. AwsDataApiPreparedQuery extracts parameters from the raw query configuration input parameters before logging.
async values(placeholderValues: Record<string, unknown> = {}): Promise<T['values']> {
const params = fillPlaceholders(this.params, placeholderValues ?? {});
this.rawQuery.input.parameters = params.map((param, index) => ({
name: `${index + 1}`,
...toValueParam(param, this.typings[index]),
}));
this.options.logger?.logQuery(this.rawQuery.input.sql!, this.rawQuery.input.parameters);
Sources: drizzle-orm/src/aws-data-api/pg/session.ts:110-119
Session-level fallback helpers also integrate logging directly. In TiDBServerlessSession, direct all queries compute the query structure via the dialect, invoke this.logger.logQuery(querySql.sql, querySql.params), and dispatch to the client.
override all<T = unknown>(query: SQL): Promise<T[]> {
const querySql = this.dialect.sqlToQuery(query);
this.logger.logQuery(querySql.sql, querySql.params);
return this.client.execute(querySql.sql, querySql.params) as Promise<T[]>;
}Sources: drizzle-orm/src/tidb-serverless/session.ts:159-163
Driver Logging Integration Matrix
| Driver Module | Prepared Query Class | Session Class | Default Logger Fallback |
|---|---|---|---|
sqlite-proxy | RemotePreparedQuery | SQLiteProxySession | NoopLogger |
libsql | LibSQLPreparedQuery | LibSQLSession | NoopLogger |
neon-serverless | NeonPreparedQuery | NeonSession | NoopLogger |
bun-sql | BunSQLPreparedQuery | BunSQLSession | NoopLogger |
neon-http | NeonHttpPreparedQuery | NeonHttpSession | NoopLogger |
tidb-serverless | TiDBServerlessPreparedQuery | TiDBServerlessSession | NoopLogger |
aws-data-api/pg | AwsDataApiPreparedQuery | AwsDataApiSession | NoopLogger |
op-sqlite | OPSQLitePreparedQuery | OPSQLiteSession | NoopLogger |
singlestore-proxy | PreparedQuery | SingleStoreRemoteSession | NoopLogger |
prisma/pg | PrismaPgPreparedQuery | PrismaPgSession | NoopLogger |
Sources: drizzle-orm/src/sqlite-proxy/session.ts:143-153, drizzle-orm/src/libsql/session.ts:165-174, drizzle-orm/src/neon-serverless/session.ts:27-38, drizzle-orm/src/bun-sql/session.ts:19-28, drizzle-orm/src/neon-http/session.ts:29-37, drizzle-orm/src/tidb-serverless/session.ts:26-34, drizzle-orm/src/aws-data-api/pg/session.ts:30-42, drizzle-orm/src/op-sqlite/session.ts:28-46, drizzle-orm/src/singlestore-proxy/session.ts:29-45, drizzle-orm/src/prisma/pg/session.ts:16-25
Important
When options omit a logger during session initialization (such as in BunSQLSession, TiDBServerlessSession, and OPSQLiteSession), sessions instantiate a NoopLogger to ensure safe method invocation without requiring explicit null checks across prepared queries.
Sources: drizzle-orm/src/bun-sql/session.ts:128-129, drizzle-orm/src/tidb-serverless/session.ts:128-129, drizzle-orm/src/op-sqlite/session.ts:44-45
Diagnostics and tracing error handling
Active Span Exception Capture
startActiveSpan<F extends (span?: Span) => unknown>(name: SpanName, fn: F): ReturnType<F> {
if (!otel) {
return fn() as ReturnType<F>;
}
if (!rawTracer) {
rawTracer = otel.trace.getTracer('drizzle-orm', npmVersion);
}
return iife(
(otel, rawTracer) =>
rawTracer.startActiveSpan(
name,
((span: Span) => {
try {
return fn(span);
} catch (e) {
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: e instanceof Error ? e.message : 'Unknown error', // eslint-disable-line no-instanceof/no-instanceof
});
throw e;
} finally {
span.end();
}
}) as F,
),
otel,
rawTracer,
);
},Sources: drizzle-orm/src/tracing.ts:25-55
The tracing subsystem intercepts exceptions during active span execution via the startActiveSpan wrapper. When an error is thrown within the callback function fn(span), the execution catches the exception in a try...catch block, marks the OpenTelemetry span status as an error, and rethrows the original error.
Warning
If OpenTelemetry (otel) is not imported or available in the runtime environment, startActiveSpan bypasses span creation entirely and directly executes the callback function without error capture or status reporting.
Sources: drizzle-orm/src/tracing.ts:26-28
Span Status and Error Properties
| Condition | Span Status Code | Error Message Extraction Strategy |
|---|---|---|
Caught value is an Error instance | otel.SpanStatusCode.ERROR | e.message |
Caught value is not an Error instance | otel.SpanStatusCode.ERROR | 'Unknown error' |
Sources: drizzle-orm/src/tracing.ts:42-45
Important
The span lifecycle guarantees that span.end() is always invoked within a finally block, ensuring spans are closed even when query execution or response mapping throws an exception.
Sources: drizzle-orm/src/tracing.ts:47-49
Related
On This Page
OverviewOpenTelemetry tracing subsystem architectureTracer Configuration and Module IntegrationSpan Names and Wrapping PrimitivesQuery builder span instrumentationRelational and Core Query Builder SpansDialect Compilation and Execution FlowLogger interface and default writersLogger Contracts and WritersDefaultLogger and Formatting LogicSession level query logging integrationLogger Dispatch Across DriversDriver Logging Integration MatrixDiagnostics and tracing error handlingActive Span Exception CaptureSpan Status and Error PropertiesRelated