TL;DR

Drizzle’s built-in logger only runs before queries execute, so it can’t record execution time or results. Numeric’s engineer used Node.js AsyncLocalStorage to carry per-query context across the async call stack, letting the pre-execution logger populate SQL and params and finalizing the log after the query completes.

What happened

Numeric runs Postgres queries with Drizzle, but found Drizzle’s single pre-execution logger insufficient for their observability needs: it exposes only the SQL and args before the query runs, preventing capture of execution time and row counts. Instead of resorting to fragile prototype hacks against node-postgres internals, the team applied Node.js’s AsyncLocalStorage to carry query metadata through the async call chain. The pattern has three parts: create an AsyncLocalStorage-backed context (the author calls the wrapper function wrapQuery) with a query key and start time; let Drizzle’s pre-execution logger write SQL text and parameters into that context; and, after the query returns, read the stored context to compute elapsed time, include row counts and emit a complete structured log line. The approach preserves type safety, avoids touching library internals, and mirrors patterns used by tracing and error-monitoring libraries.

Why it matters

  • Restores comprehensive per-query logs (unique key, execution time, SQL, params, row count) needed for debugging and monitoring.
  • Avoids brittle prototype manipulation and tight coupling to driver implementation details.
  • Uses built-in Node.js primitives to propagate context across async operations without manual argument passing.
  • Aligns with established patterns for distributed tracing and error context propagation used by other tools.

Key facts

  • Numeric uses Drizzle as a fully-typed Postgres query builder and needed richer query logs for monitoring and optimization.
  • Required canonical log elements: unique query key, execution time, sanitized SQL, argument count/values, and row count.
  • Drizzle’s logger API invokes a single handler before execution and only provides access to the query and arguments.
  • A common alternative on GitHub involves prototype manipulation of node-postgres internals, which the author deemed fragile.
  • AsyncLocalStorage maintains a context tied to the async call chain and can be accessed anywhere within that chain.
  • Implementation breakdown: create a context (wrapQuery), have Drizzle’s logger populate SQL/params, then finalize and emit the log after completion.
  • This flow produces complete, structured log lines without modifying library internals.
  • Author reports type safety and no additional runtime overhead beyond normal logging costs.
  • The author notes that OpenTelemetry and Sentry use similar AsyncLocalStorage-based patterns for trace and error context propagation.

What to watch next

  • Whether Drizzle will add built-in post-execution logging hooks (not confirmed in the source).
  • Performance behavior and resource use of AsyncLocalStorage at very high query volumes (not confirmed in the source).
  • Adoption of this pattern in Drizzle examples or community plugins (not confirmed in the source).

Quick glossary

  • AsyncLocalStorage: A Node.js API that associates and propagates a store of data across an asynchronous call chain, similar to thread-local storage for async operations.
  • Drizzle (DrizzleORM): A TypeScript-friendly SQL query builder for Postgres that aims to produce readable SQL while providing type safety.
  • Query logging: Recording details about database queries (such as SQL, parameters, timing, and results) for debugging, monitoring, and performance analysis.
  • Prototype manipulation: Changing built-in or third-party JavaScript prototypes at runtime to alter behavior, a technique that can be brittle and couple code to internal implementations.

Reader FAQ

Does this approach require modifying Drizzle or the Postgres driver internals?
No. The solution uses AsyncLocalStorage to carry context and avoids prototype or internal driver changes.

Can you capture execution time and row counts with this pattern?
Yes. By creating a context at query start and reading it after the result returns, you can compute elapsed time and include row counts.

Is there a runtime performance cost to using AsyncLocalStorage?
The author states there is no overhead beyond what a logging solution would normally introduce.

Do other observability tools use the same pattern?
Yes. The author cites OpenTelemetry and Sentry as examples that use AsyncLocalStorage-style propagation for traces and error context.

Will Drizzle add richer logging hooks in the future?
not confirmed in the source

Discover more from Numeric Engineering Writings from Numeric's engineering org Subscribe By subscribing, I agree to Substack's Terms of Use, and acknowledge its Information Collection Notice and Privacy Policy. Already…

Sources

Related posts

By

Leave a Reply

Your email address will not be published. Required fields are marked *