Add `TracingChannel` for observability
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 8.7k
- Forks
- 374
- Avg merge
- 11d 16h
- Merged PRs (30d)
- 1
Description
Hi, I work at Sentry and we've been leading an effort to help the ecosystem adopt tracing channels and given how popular this library is and that we already maintain an instrumentation for it, I thought I would reach out.
I'm suggesting adding a first-class TracingChannel support to Postgres.js, following the pattern established by undici in Node.js core.
TracingChannel is a higher-level API built on top of diagnostics_channel, designed for tracing async operations. It provides structured lifecycle channels (start, end, error, asyncStart, asyncEnd) and handles async context propagation correctly.
Motivation
Why native tracing matters for Postgres.js
Postgres.js is one of the most popular PostgreSQL clients for Node.js. APM tools (Sentry, OTel, Datadog) need to instrument it for query-level observability. Today they do this through monkey-patching, which is fragile and has several ecosystem concerns:
- Runtime lock-in: RITM and IITM rely on Node.js-specific module loader internals (
Module._resolveFilename,module.register()). They don't work on Bun or Deno, which implement the Node.js API surface but not the module loader internals. - Initialization ordering: Both require instrumentation to be set up before
postgresis firstrequire()'d /import'd. Get the order wrong and instrumentation silently does nothing. - Bundling: Users must ensure instrumented modules are externalized, which is increasingly difficult as frameworks bundle server-side code.
Given the alignment on having a runtime agnostic code, I think tracing channels is a great addition without compromising on that. Tracing channels are supported on Node, Bun, Deno and Cloudflare workers.
What APM tools do today
Sentry's current instrumentation (428 LOC) wraps the module's default export, proxies resolve/reject on each query to capture completion, and patches Query.prototype.handle across three file paths as a fallback for pre-existing instances. All of this breaks if Postgres.js renames internals or refactors the query flow.
Relationship to the existing debug option
Postgres.js already has a debug callback:
const sql = postgres({ debug: (id, string, parameters, types) => { ... } })
This fires during the build() phase and provides the query string and parameters. However, it doesn't cover the async lifecycle (no completion/error events), doesn't propagate async context, and requires per-instance configuration.
TracingChannels complements debug by providing structured lifecycle events that APM tools can subscribe to globally.
Proposed Tracing Channels
I'm thinking the following channels could be a good start for this proposal, covering most use-cases and operations.
| Channel | Type | Tracks |
|---|---|---|
postgres:query |
TracingChannel | Query execution (tagged template sql...``, .execute()) |
postgres:connection |
TracingChannel | Connection establishment |
postgres:transaction |
TracingChannel | Transaction lifecycle (sql.begin()) |
Usage Example
If Tracing Channels were to be added to this library, an APM or even the user can do:
```js
const dc = require('node:diagnostics_channel');
dc.tracingChannel('postgres:query').subscribe({
start(ctx) {
ctx.span = tracer.startSpan(ctx.result?.command || 'postgres.query', {
attributes: {
'db.system': 'postgres',
'db.query.text': sanitize(ctx.query.text),
'db.namespace': ctx.connection.database,
'server.address': ctx.connection.host,
'server.port': ctx.connection.port,
},
});
},
asyncEnd(ctx) {
if (ctx.result) {
ctx.span?.setAttribute('db.operation.name', ctx.result.command);
}
ctx.span?.end();
},
error(ctx) {
ctx.span?.setStatus({ code: SpanStatusCode.ERROR, message: ctx.error?.message });
ctx.span?.end();
},
});
dc.tracingChannel('postgres:transaction').subscribe({
start(ctx) {
ctx.span = tracer.startSpan('postgres.transaction');
},
asyncEnd(ctx) {
ctx.span?.setAttribute('db.transaction.committed', ctx.result?.committed);
ctx.span?.end();
},
error(ctx) {
ctx.span?.setStatus({ code: SpanStatusCode.ERROR });
ctx.span?.end();
},
});
This would work on all server runtimes, without need for module patching or monkey patching.
Prior Art
This approach follows the same pattern already adopted by other database libraries:
- sidorares/node-mysql2#4178
- redis/node-redis#3195
- redis/ioredis#2089
- graphql/graphql-js#4670
- Automattic/mongoose#16275
among many others.
Happy to put together a PR with the implementation.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the tagged-template sql, .execute(), and sql.begin() entry points and trace their query, connection, and transaction lifecycles. Use the existing debug/build behavior and undici's TracingChannel pattern as references. Done means the proposed postgres:query, postgres:connection, and postgres:transaction channels expose the stated lifecycle and error events without relying on monkey-patching.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nodejs, postgresql
- Domain
- databases, observability
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100