
# Tracing

> db0 can emit tracing events for every query it runs.

## Overview

db0 uses [tracing channels](https://nodejs.org/api/diagnostics_channel.html) of the diagnostics channel module to emit traceable actions for query operations.

Common use cases for tracing are:

- Query performance monitoring and profiling
- Logging
- Query debugging
- Error tracking

## Enabling tracing

Tracing is opt-in. Wrap the database instance with `withTracing` from `db0/tracing` and use the returned instance:

```ts
import { createDatabase } from "db0";
import { withTracing } from "db0/tracing";
import sqlite from "db0/connectors/better-sqlite3";

const db = withTracing(createDatabase(sqlite({})));

// Queries made with `db` now emit tracing events
const rows = await db.sql`SELECT * FROM users`;
```

Only the wrapped instance emits events, so tracing costs nothing for databases you do not wrap. Wrapping an already wrapped instance is a no-op.

Tracing is looked up through `process.getBuiltinModule("node:diagnostics_channel")`, which requires Node.js `20.16+` or `22.3+`. On older versions, or on runtimes where diagnostics channels are unavailable, `withTracing` returns the database unchanged and silently emits no events.

The wrapper delegates every call to the original instance, so `connector`, `dialect`, `capabilities`, `disposed`, `getInstance()` and `dispose()` behave exactly as they do on an untraced database. Two things differ:

- `db.exec()` and the statement methods (`all()`, `run()`, `get()`) always return a promise. On a disposed database `exec()` rejects instead of throwing synchronously, so that connectors failing synchronously emit the same events as connectors failing asynchronously. Every built-in connector already returns promises from these methods, so nothing changes in practice.
- The returned object is a plain object that forwards to the original, not the original itself. If you implement `Database` as a class, `instanceof` checks and any members outside the `Database` interface do not carry over to the traced instance.

## Query Tracing Channel

db0 uses `db0.query` for the tracing channel name.

To set up tracing, you need to subscribe to the tracing channel. Since query operations are asynchronous, you need to subscribe to the `asyncEnd` event as it signals the completion of the operation.

`db0/tracing` exports the name as `QUERY_CHANNEL` so you do not have to hardcode it:

```ts
import { tracingChannel } from "node:diagnostics_channel";
import { QUERY_CHANNEL } from "db0/tracing";

const queryChannel = tracingChannel(QUERY_CHANNEL);

queryChannel.subscribe({
  start: (data) => {
    console.log("start", data.query);
  },
  asyncEnd: (data) => {
    console.log("end", data.query, data.result);
  },
});
```

The event payload contains several properties that can be used to track the query operation:

- `query`: The query string as `db0` sees it — for `sql` it is the template rebuilt with `?` placeholders. It is not always what reaches the server: the PostgreSQL connector rewrites `?` to `$1`, `$2`, ... on its way out, so traced queries will not match PostgreSQL server logs verbatim.
- `method`: The method used to execute the query (`exec`, `sql`, `prepare.all`, `prepare.run`, `prepare.get`).
- `connector`: The name of the connector (e.g. `sqlite`, `postgresql`).
- `dialect`: The dialect of the database connection (`sqlite`, `libsql`, `postgresql` or `mysql`).
- `result`: The result of the query (available on the `asyncStart` and `asyncEnd` events).
- `error`: The error that occurred during the query operation (available on the `error`, `asyncStart` and `asyncEnd` events).

The events are emitted in the following order:

- On success: `start` → `end` → `asyncStart` → `asyncEnd`
- On failure: `start` → `end` → `error` → `asyncStart` → `asyncEnd`

Queries always settle asynchronously, even with connectors that fail synchronously, so `asyncEnd` is emitted for every traced query.

::note
Bound parameters are never included in the traced context. Note that the `query` string itself can still contain literal values, either from raw `db.exec()` strings or from static interpolations (`${}` inside `{}`) in the `sql` template. Diagnostics channels are process-wide, so any subscriber can read them.
::

## Next steps

:read-more{to="/connectors"}

:read-more{to="/integrations"}
