porsager / porsager/postgres

Add `TracingChannel` for observability

Open
#1,171 2 comments 5 reactions 0 assignees View on GitHub

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.