Skip to content

Add TracingChannel for observability #1171

Description

@logaretm

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 postgres is first require()'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:

among many others.


Happy to put together a PR with the implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions