docs

Drizzle

Trace Drizzle ORM queries as database spans with @unhandled/instrumentation-drizzle, wrapping either the Drizzle client or the node-postgres driver.

@unhandled/instrumentation-drizzle records Drizzle ORM queries and prepared queries as CLIENT spans, so they appear in queries. It needs drizzle-orm 0.28.0 or later and your own database driver. The package is opt-in: it has no /integration entry and the AdonisJS agent never enables it. You wrap your database yourself, after OpenTelemetry has started.

Install

Terminal
npm install @unhandled/instrumentation-drizzle

Wrap the Drizzle client

instrumentDrizzleClient() patches the session behind the object drizzle() returns, so it works with any Drizzle driver.

ts
import pg from 'pg'
import { drizzle } from 'drizzle-orm/node-postgres'
import { instrumentDrizzleClient } from '@unhandled/instrumentation-drizzle'

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })

export const db = instrumentDrizzleClient(drizzle(pool), {
  dbSystem: 'postgresql',
  dbName: 'shop',
  peerName: 'db.internal',
  peerPort: 5432,
})

It throws a TypeError when the object has no session to instrument.

Wrap the node-postgres driver

instrumentDrizzle() wraps query() on a node-postgres Pool, including the clients it hands out, or on a single Client. Pass the result to drizzle().

ts
import pg from 'pg'
import { drizzle } from 'drizzle-orm/node-postgres'
import { instrumentDrizzle } from '@unhandled/instrumentation-drizzle'

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })

export const db = drizzle(instrumentDrizzle(pool, { dbSystem: 'postgresql', dbName: 'shop' }))

Any object without a query() method throws a TypeError. For other drivers, use instrumentDrizzleClient().

Heads up. Pick one entry point per database. Using both traces every query twice, once at the Drizzle layer and once at the driver, with the driver span nested inside. The same applies if you also run OpenTelemetry's own pg instrumentation.

Options

OptionDefaultEffect
dbSystemunknownValue of db.system.name, such as postgresql, mysql or sqlite. Set it.
dbNamenonedb.namespace
peerNamenoneserver.address, the database host
peerPortnoneserver.port
captureQueryTexttruefalse leaves db.query.text off every span.
maxQueryTextLength1000Query text is cut to this many characters. Operation and table still come from the full SQL.

Bound parameters are never recorded. SQL text can still contain literals you inlined, so turn off captureQueryText if that matters. A failed query's span carries the driver's error rather than Drizzle's wrapper error, which embeds the parameters.

What it records

Every span is CLIENT kind and named <OPERATION> <table>, such as SELECT orders. Without a table the name is the operation alone, and without a parsable operation it is the dbSystem value. A prepared query produces one span per execution.

AttributeValue
db.system.namedbSystem
db.query.textSQL with placeholders, truncated, when captured
db.operation.nameUpper-cased first keyword, such as SELECT
db.collection.nameTarget table, when it can be parsed
db.namespacedbName
server.addresspeerName
server.portpeerPort
db.transactiontrue for statements inside a transaction
error.typeError class name on failure, with the exception recorded and ERROR status

Transactions are traced differently by the two entry points:

  • instrumentDrizzle() sees what the connection sees. BEGIN, COMMIT, ROLLBACK, savepoints and every query between them become spans with db.transaction.
  • instrumentDrizzleClient() traces the queries you run inside db.transaction() and flags them. The control statements Drizzle issues itself are not traced.

The SQL parser is exported as parseSql(sql), returning { operation, collection }.

When @opentelemetry/api is installed, Drizzle also emits its own drizzle.* spans under the drizzle-orm scope. They are independent of this package and cannot be turned off from here.