Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { defineBuildConfig } from "obuild/config";
export default defineBuildConfig({
entries: [
{ type: "bundle", input: "src/index.ts" },
{ type: "bundle", input: "src/tracing.ts" },
{ type: "transform", input: "src/connectors/", outDir: "dist/connectors" },
{
type: "transform",
Expand Down
2 changes: 2 additions & 0 deletions docs/1.guide/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ console.log(rows);

## Next steps

:read-more{to="/guide/tracing"}

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

:read-more{to="/integrations"}
84 changes: 84 additions & 0 deletions docs/1.guide/2.tracing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
icon: ph:chart-bar-horizontal-duotone
---

# 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.

## 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.

```ts
import { tracingChannel } from "node:diagnostics_channel";

const queryChannel = tracingChannel("db0.query");

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.
- `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"}
51 changes: 51 additions & 0 deletions examples/tracing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createDatabase } from "../../src";
import { withTracing } from "../../src/tracing";
import type { TraceContext } from "../../src/tracing";
import { tracingChannel } from "node:diagnostics_channel";

import sqlite from "../../src/connectors/better-sqlite3";
Comment on lines +1 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether other examples/* directories use relative src imports or the public db0 package.
fd -t d . examples | while read -r dir; do
  echo "== $dir =="
  fd -e ts -e js . "$dir" --exec grep -nE "from ['\"](\.\./\.\./src|db0)" {} \;
done

Repository: unjs/db0

Length of output: 530


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples files =="
fd -t f . examples

echo
echo "== package.json =="
sed -n '1,220p' package.json

echo
echo "== examples/drizzle/index.ts =="
sed -n '1,120p' examples/drizzle/index.ts

echo
echo "== examples/tracing/index.ts =="
sed -n '1,120p' examples/tracing/index.ts

Repository: unjs/db0

Length of output: 5394


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples/drizzle/package.json =="
sed -n '1,200p' examples/drizzle/package.json

echo
echo "== examples/tracing/package.json =="
sed -n '1,200p' examples/tracing/package.json

echo
echo "== tsconfig files =="
fd -a -t f 'tsconfig*.json' .

Repository: unjs/db0

Length of output: 683


Use the published db0 entry points here

This example already declares db0 in examples/tracing/package.json, so it should import from db0, db0/tracing, and db0/connectors/better-sqlite3 instead of ../../src/*. That makes the example exercise the exported ./tracing subpath and match consumer usage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/tracing/index.ts` around lines 1 - 5, Update the imports in the
tracing example to use the published db0 entry points: import createDatabase
from db0, TraceContext and withTracing from db0/tracing, and sqlite from
db0/connectors/better-sqlite3, preserving the existing symbols and usage.


async function main() {
const db = withTracing(createDatabase(sqlite({})));

// Subscribe to tracing events
subscribeToTracing();

await db.sql`create table if not exists users (
id integer primary key autoincrement,
full_name text
)`;

const res = await db.sql`insert into users (full_name) values ('John Doe')`;

console.log({ res });
}

function subscribeToTracing() {
const queryChannel = tracingChannel<TraceContext>("db0.query");

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

// eslint-disable-next-line unicorn/prefer-top-level-await
main().catch((error) => {
console.error(error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
11 changes: 11 additions & 0 deletions examples/tracing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "db0-with-tracing",
"private": true,
"scripts": {
"start": "jiti ./index.ts"
},
"devDependencies": {
"jiti": "^1.21.0",
"db0": "latest"
}
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./tracing": {
"types": "./dist/tracing.d.mts",
"default": "./dist/tracing.mjs"
},
"./connectors/*": {
"types": "./dist/connectors/*.d.ts",
"default": "./dist/connectors/*.mjs"
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

148 changes: 148 additions & 0 deletions src/tracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import type { ConnectorName } from "./_connectors.ts";
import type {
Connector,
Database,
Primitive,
SQLDialect,
Statement,
} from "./types.ts";
import { sqlTemplate } from "./template.ts";

export type TracedOperation = "query";

const QUERY_OPERATION: TracedOperation = "query";

export interface TraceContext {
query: string;
method: "exec" | "sql" | "prepare.all" | "prepare.run" | "prepare.get";
connector: ConnectorName;
dialect: SQLDialect;
}

const TRACED: unique symbol = Symbol.for("db0.traced");

type MaybeTracedDatabase<TConnector extends Connector = Connector> =
Database<TConnector> & {
[TRACED]?: boolean;
};

/**
* Wrap a database instance with tracing functionality.
*/
export function withTracing<TConnector extends Connector = Connector>(
db: MaybeTracedDatabase<TConnector>,
): Database<TConnector> {
// Avoids double patching
if (db[TRACED]) {
return db;
}

const { tracingChannel } =
globalThis.process?.getBuiltinModule?.("node:diagnostics_channel") || {};
if (!tracingChannel) {
return db;
}

const queryChannel = tracingChannel<TraceContext>(`db0.${QUERY_OPERATION}`);

// The context is built lazily to avoid any work when nobody is subscribed.
// This is an async function so that subscribing never turns a rejection
// (e.g. an invalid `sql` template) into a synchronous throw.
async function tracePromise<T>(
exec: () => Promise<T>,
context: () => TraceContext,
): Promise<T> {
if (!queryChannel.hasSubscribers) {
return exec();
}
return queryChannel.tracePromise(exec, context());
}

// Copy the property descriptors instead of spreading, so that getters like
// `dialect` and `disposed` stay getters. Spreading would evaluate them once,
// making `disposed` report the state at wrap-time forever after.
const tracedDb = Object.defineProperties(
{} as MaybeTracedDatabase<TConnector>,
Object.getOwnPropertyDescriptors(db),
);
// Non-enumerable, so spreading or serializing a traced database does not leak it.
Object.defineProperty(tracedDb, TRACED, { value: true });

// `exec` is wrapped in an async function so connectors throwing synchronously
// emit the same event sequence as connectors rejecting asynchronously.
tracedDb.exec = (query) =>
tracePromise(
async () => db.exec(query),
() => ({
query,
method: "exec",
connector: db.connector,
dialect: db.dialect,
}),
);

tracedDb.sql = (strings, ...values) =>
tracePromise(
async () => db.sql(strings, ...values),
() => ({
query: sqlTemplate(strings, ...values)[0],
method: "sql",
connector: db.connector,
dialect: db.dialect,
}),
);

class TracedStatement implements Statement {
#statement: Statement;
#query: string;

constructor(statement: Statement, query: string) {
this.#statement = statement;
this.#query = query;
}

#withTrace<T>(
fn: () => Promise<T>,
method: "prepare.all" | "prepare.run" | "prepare.get",
) {
return tracePromise(fn, () => ({
query: this.#query,
method,
connector: db.connector,
dialect: db.dialect,
}));
}

bind(...args: Primitive[]) {
return new TracedStatement(this.#statement.bind(...args), this.#query);
}

all(...args: Primitive[]) {
return this.#withTrace(
async () => this.#statement.all(...args),
"prepare.all",
);
}

run(...args: Primitive[]) {
return this.#withTrace(
async () => this.#statement.run(...args),
"prepare.run",
);
}

get(...args: Primitive[]) {
return this.#withTrace(
async () => this.#statement.get(...args),
"prepare.get",
);
}
}

/**
* Prepare needs a special treatment because it returns a statement instance that needs to be patched.
*/
tracedDb.prepare = (query) => new TracedStatement(db.prepare(query), query);

return tracedDb;
}
Loading
Loading