Skip to content

Add diagnostics_channel TracingChannel support - #39

Closed
MiichelleZhou wants to merge 7 commits into
eval/3650/basefrom
eval/3650/cubic/round-11
Closed

MiichelleZhou wants to merge 7 commits into
eval/3650/basefrom
eval/3650/cubic/round-11

Conversation

@MiichelleZhou

@MiichelleZhou MiichelleZhou commented Oct 5, 2026 •

Copy link
Copy Markdown

This PR introduces tracing channels support to pg-pool and pg querying methods as discussed in #3619

I haven't yet run a benchmark, but without active tracing or subscribers the overhead is non-existent so existing users shouldn't be affected. The overhead would come from the consumers of the diag channels published here which is done anyways via monkey patching.

I added the following channels:

Channel Type Package Description
pg:query TracingChannel pg Query lifecycle (start, end, error with async context)
pg:connection TracingChannel pg Client connection lifecycle
pg:pool:connect TracingChannel pg-pool Pool connection acquisition lifecycle
pg:pool:release Channel pg-pool Client released back to pool
pg:pool:remove Channel pg-pool Client removed from pool

I understand there may be hesitation given that AI tooling was used in the implementation. To be clear, this isn't a drive-by contribution. I designed the channel layout, wrote the proposal document, and iterated through multiple rounds to handle real edge cases (Node 18 compat, preserving custom Promise types, hasSubscribers guards). The AI assisted with the code, but the approach and decisions are mine.

Usage

Instrumentations will only need to subscribe to tracing channels to create traces, logs or metrics:

import { tracingChannel } from 'diagnostics_channel'

tracingChannel('pg:query').subscribe({
  start({ query, client }) {
    // start span
    // query: { text, name, rowMode }
    // client: { database, host, port, user, processID, ssl }
  },
  asyncEnd({ query, result }) {
    // end span
    // result: { rowCount, command }
  },
  error({ query, error }) {
    // record error on span
  },
})

This is part of a broader initiative to bring TracingChannel support to the most widely used Node.js database and cache libraries. The same pattern has already been merged and shipped in:

I have been directly involved in those prior implementations and I'm happy to own this through to release, address any feedback, and provide whatever support is needed. Would love to hear your thoughts.

Supersedes #3624.


Summary by cubic

Adds diagnostics_channel TracingChannel support to pg and pg-pool so instrumentation libraries can subscribe to structured query and pool events instead of monkey-patching. No behavior change for existing users: all tracing is gated behind subscriber checks, so there is no overhead when nothing is subscribed.

New channels

  • pg:query and pg:connection tracing channels on Client, carrying query text/name/rowMode and client connection details with async context.
  • pg:pool:connect tracing channel and pg:pool:release / pg:pool:remove plain channels on Pool, carrying pool statistics and client IDs.

Compatibility

  • Node 18 backported TracingChannel without the aggregated hasSubscribers getter, so the guard treats undefined as "may have subscribers" to keep tracing working there.
  • Promise-based connect preserves user-configured Promise types by tracing through the callback path instead of tracePromise.

Written for commit b922f0d. Summary will update on new commits.

Review in cubic

logaretm and others added 7 commits March 5, 2026 12:40
Enables instrumentation libraries (OpenTelemetry, etc.) to subscribe to
structured events without monkey-patching. Uses TracingChannel for async
context propagation and plain channels for simple events.

Channels:
- pg:query (TracingChannel) — query lifecycle with result enrichment
- pg:connection (TracingChannel) — client connect lifecycle
- pg:pool:connect (TracingChannel) — pool checkout lifecycle
- pg:pool:release (plain) — client released back to pool
- pg:pool:remove (plain) — client removed from pool

All instrumentation is guarded by hasSubscribers for zero overhead when
unused. Gracefully degrades to no-ops on Node < 19.9 or non-Node
environments.

Closes #3619

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TracingChannel is not available on Node 18 LTS. Skip the tracing-dependent
tests gracefully instead of failing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Node 18 backported TracingChannel but without the aggregated
`hasSubscribers` getter (it returns `undefined` instead of a boolean).
Raw truthiness checks treat `undefined` as "no subscribers" which
silently disables tracing on Node 18.

Replace all `channel.hasSubscribers` guards with `shouldTrace(channel)`
which checks `hasSubscribers !== false` — treating `undefined` (Node 18)
as "might have subscribers, trace unconditionally" and `false` (Node 20+)
as "definitely no subscribers, skip".

Also removes the now-unnecessary test skip logic since TracingChannel
does exist on Node 18.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node 18 backported TracingChannel but with a buggy implementation —
unsubscribing and resubscribing to the same channel crashes internally
(`_subscribers` becomes undefined). Node 16 has no TracingChannel at all.

Gate tests on `hasStableTracingChannel` which checks both that
`dc.tracingChannel` exists AND that the aggregated `hasSubscribers`
getter returns a boolean (only true on Node 19.9+/20.5+).

TracingChannel tests: skipped on Node 16/18, run on Node 20+
Plain channel tests (release/remove): run on all versions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…omise type

tracePromise wraps the result in a native Promise, which breaks
clients configured with a custom Promise implementation (e.g. bluebird).
Switch to traceCallback inside the user's this._Promise constructor
so the returned promise type is always correct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@MiichelleZhou

Copy link
Copy Markdown
Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 5, 2026

Copy link
Copy Markdown

@cubic-dev-ai review this PR

@MiichelleZhou I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

4 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/pg/lib/diagnostics.js">

<violation number="1" location="packages/pg/lib/diagnostics.js:10">
P3: Node names this API `process.getBuiltinModule` (lowercase `i`), so this check never selects the built-in-module path on supported Node versions. Correct the spelling on both lines.</violation>

<violation number="2" location="packages/pg/lib/diagnostics.js:28">
P2: On Node 16.19+ and 18.x, `TracingChannel.prototype.hasSubscribers` is `undefined`, so `shouldTrace` returns `true` for `queryChannel`/`connectionChannel` even when nothing is subscribed. Every `connect()`, every callback/promise `query()`, and (via `packages/pg-pool/diagnostics.js`, which uses the identical function) every `pool.connect()` then allocates a context object, closures, and invokes `traceCallback` (which eagerly wraps the callback) — for all users on those Node versions, not just instrumented ones. That defeats the PR's stated guard: "Tracing is guarded when there are no subscribers to avoid affecting existing users." pg declares `engines.node >= 16`, so 18.2–18.18 (where tracing is unconditionally enabled) is a large supported LTS population. The sub-channels (`start`/`end`/`asyncStart`/`asyncEnd`/`error`) each expose a proper boolean `hasSubscribers` on those versions, so the aggregated check can be reconstructed. Note the new tests in `packages/pg/test/unit/client/diagnostics-tests.js`/`packages/pg-pool/test/diagnostics.js` are skipped unless `hasSubscribers` is a boolean — the traced path runs unconditionally on exactly the versions where it is untested.</violation>
</file>

<file name="packages/pg/lib/client.js">

<violation number="1" location="packages/pg/lib/client.js:709">
P2: Callback-free `Submittable` queries, including `QueryStream`, skip `pg:query` tracing entirely because this guard requires `query.callback`. Trace their `end`/`error` lifecycle too so stream-based queries are observable.</violation>

<violation number="2" location="packages/pg/lib/client.js:728">
P2: `query_timeout` bypasses this traced callback, leaving `pg:query` spans without an `error` or `asyncEnd` event when a query times out. Ensure the timeout path completes tracing as well as notifying the original callback.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment on lines +28 to +30
return channel.hasSubscribers !== false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: On Node 16.19+ and 18.x, TracingChannel.prototype.hasSubscribers is undefined, so shouldTrace returns true for queryChannel/connectionChannel even when nothing is subscribed. Every connect(), every callback/promise query(), and (via packages/pg-pool/diagnostics.js, which uses the identical function) every pool.connect() then allocates a context object, closures, and invokes traceCallback (which eagerly wraps the callback) — for all users on those Node versions, not just instrumented ones. That defeats the PR's stated guard: "Tracing is guarded when there are no subscribers to avoid affecting existing users." pg declares engines.node >= 16, so 18.2–18.18 (where tracing is unconditionally enabled) is a large supported LTS population. The sub-channels (start/end/asyncStart/asyncEnd/error) each expose a proper boolean hasSubscribers on those versions, so the aggregated check can be reconstructed. Note the new tests in packages/pg/test/unit/client/diagnostics-tests.js/packages/pg-pool/test/diagnostics.js are skipped unless hasSubscribers is a boolean — the traced path runs unconditionally on exactly the versions where it is untested.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/pg/lib/diagnostics.js, line 28:

<comment>On Node 16.19+ and 18.x, `TracingChannel.prototype.hasSubscribers` is `undefined`, so `shouldTrace` returns `true` for `queryChannel`/`connectionChannel` even when nothing is subscribed. Every `connect()`, every callback/promise `query()`, and (via `packages/pg-pool/diagnostics.js`, which uses the identical function) every `pool.connect()` then allocates a context object, closures, and invokes `traceCallback` (which eagerly wraps the callback) — for all users on those Node versions, not just instrumented ones. That defeats the PR's stated guard: "Tracing is guarded when there are no subscribers to avoid affecting existing users." pg declares `engines.node >= 16`, so 18.2–18.18 (where tracing is unconditionally enabled) is a large supported LTS population. The sub-channels (`start`/`end`/`asyncStart`/`asyncEnd`/`error`) each expose a proper boolean `hasSubscribers` on those versions, so the aggregated check can be reconstructed. Note the new tests in `packages/pg/test/unit/client/diagnostics-tests.js`/`packages/pg-pool/test/diagnostics.js` are skipped unless `hasSubscribers` is a boolean — the traced path runs unconditionally on exactly the versions where it is untested.</comment>

<file context>
@@ -0,0 +1,31 @@
+// backported TracingChannel but not the getter). When `undefined`, we assume
+// there may be subscribers and trace unconditionally.
+function shouldTrace(channel) {
+  return channel.hasSubscribers !== false
+}
+
</file context>
Suggested change
return channel.hasSubscribers !== false
}
function shouldTrace(channel) {
if (channel.hasSubscribers !== undefined) {
return channel.hasSubscribers
}
// Node 16.19/18.x lack the aggregated getter; fall back to the individual
// channels so tracing stays off when nothing is subscribed.
return (
channel.start.hasSubscribers ||
channel.asyncStart.hasSubscribers ||
channel.error.hasSubscribers ||
channel.asyncEnd.hasSubscribers ||
channel.end.hasSubscribers
)
}

Comment thread packages/pg/lib/client.js
}
queryChannel.traceCallback(
(tracedCb) => {
query.callback = tracedCb

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: query_timeout bypasses this traced callback, leaving pg:query spans without an error or asyncEnd event when a query times out. Ensure the timeout path completes tracing as well as notifying the original callback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/pg/lib/client.js, line 728:

<comment>`query_timeout` bypasses this traced callback, leaving `pg:query` spans without an `error` or `asyncEnd` event when a query times out. Ensure the timeout path completes tracing as well as notifying the original callback.</comment>

<file context>
@@ -687,11 +700,42 @@ class Client extends EventEmitter {
+      }
+      queryChannel.traceCallback(
+        (tracedCb) => {
+          query.callback = tracedCb
+          enqueue()
+        },
</file context>

Comment thread packages/pg/lib/client.js
this._pulseQueryQueue()
}

if (shouldTrace(queryChannel) && query.callback) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Callback-free Submittable queries, including QueryStream, skip pg:query tracing entirely because this guard requires query.callback. Trace their end/error lifecycle too so stream-based queries are observable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/pg/lib/client.js, line 709:

<comment>Callback-free `Submittable` queries, including `QueryStream`, skip `pg:query` tracing entirely because this guard requires `query.callback`. Trace their `end`/`error` lifecycle too so stream-based queries are observable.</comment>

<file context>
@@ -687,11 +700,42 @@ class Client extends EventEmitter {
+      this._pulseQueryQueue()
+    }
+
+    if (shouldTrace(queryChannel) && query.callback) {
+      const context = {
+        query: { text: query.text, name: query.name, rowMode: query._rowMode },
</file context>

Comment on lines +10 to +11
if (typeof process.getBuiltInModule === 'function') {
dc = process.getBuiltInModule('diagnostics_channel')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Node names this API process.getBuiltinModule (lowercase i), so this check never selects the built-in-module path on supported Node versions. Correct the spelling on both lines.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/pg/lib/diagnostics.js, line 10:

<comment>Node names this API `process.getBuiltinModule` (lowercase `i`), so this check never selects the built-in-module path on supported Node versions. Correct the spelling on both lines.</comment>

<file context>
@@ -0,0 +1,31 @@
+
+try {
+  let dc
+  if (typeof process.getBuiltInModule === 'function') {
+    dc = process.getBuiltInModule('diagnostics_channel')
+  } else {
</file context>
Suggested change
if (typeof process.getBuiltInModule === 'function') {
dc = process.getBuiltInModule('diagnostics_channel')
if (typeof process.getBuiltinModule === 'function') {
dc = process.getBuiltinModule('diagnostics_channel')

@MiichelleZhou
MiichelleZhou deleted the eval/3650/cubic/round-11 branch October 5, 2026 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants