Skip to main content
doc0blade47/drizzle-ormdocumentation home
Sign in

Logging and Tracing

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

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 NameDescription
drizzle.operationTop-level query builder or transactional operation wrapper
drizzle.prepareQueryStatement preparation and parameter binding compilation
drizzle.buildSQLSQL string generation and dialect operator construction
drizzle.executeQuery execution boundary across database sessions
drizzle.driver.executeLow-level driver communication and transport layer invocation
drizzle.mapResponseRaw 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 ModulePrepared Query ClassSession ClassDefault Logger Fallback
sqlite-proxyRemotePreparedQuerySQLiteProxySessionNoopLogger
libsqlLibSQLPreparedQueryLibSQLSessionNoopLogger
neon-serverlessNeonPreparedQueryNeonSessionNoopLogger
bun-sqlBunSQLPreparedQueryBunSQLSessionNoopLogger
neon-httpNeonHttpPreparedQueryNeonHttpSessionNoopLogger
tidb-serverlessTiDBServerlessPreparedQueryTiDBServerlessSessionNoopLogger
aws-data-api/pgAwsDataApiPreparedQueryAwsDataApiSessionNoopLogger
op-sqliteOPSQLitePreparedQueryOPSQLiteSessionNoopLogger
singlestore-proxyPreparedQuerySingleStoreRemoteSessionNoopLogger
prisma/pgPrismaPgPreparedQueryPrismaPgSessionNoopLogger

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

ConditionSpan Status CodeError Message Extraction Strategy
Caught value is an Error instanceotel.SpanStatusCode.ERRORe.message
Caught value is not an Error instanceotel.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

Was this page helpful?