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
npm install @unhandled/instrumentation-drizzleWrap the Drizzle client
instrumentDrizzleClient() patches the session behind the object drizzle() returns, so it works with any Drizzle driver.
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().
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
pginstrumentation.
Options
| Option | Default | Effect |
|---|---|---|
dbSystem | unknown | Value of db.system.name, such as postgresql, mysql or sqlite. Set it. |
dbName | none | db.namespace |
peerName | none | server.address, the database host |
peerPort | none | server.port |
captureQueryText | true | false leaves db.query.text off every span. |
maxQueryTextLength | 1000 | Query 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.
| Attribute | Value |
|---|---|
db.system.name | dbSystem |
db.query.text | SQL with placeholders, truncated, when captured |
db.operation.name | Upper-cased first keyword, such as SELECT |
db.collection.name | Target table, when it can be parsed |
db.namespace | dbName |
server.address | peerName |
server.port | peerPort |
db.transaction | true for statements inside a transaction |
error.type | Error 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 withdb.transaction.instrumentDrizzleClient()traces the queries you run insidedb.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.