Repository navigation
Conversation
APM and OpenTelemetry integrations had to patch the driver's source, which breaks whenever the code shape moves and does not cover the fork's 4.x line. The driver now publishes diagnostics_channel TracingChannels postgres:query, postgres:connection and postgres:transaction, plus the plain channels postgres:query:send and postgres:connection:close. A query starts in Query.handle() through start.runStores in the caller's synchronous context, so stores bound to the start channel are current for the code that runs the query. The settlement events come from the query's own resolve and reject, not from wrapping the Query in tracePromise: a cursor read with for await never settles the Query itself, and the promise tracePromise returns would reject unhandled. The cursor's replacement resolve and reject publish too, and a COPY stream publishes when the copy is over, not when the stream is handed out. Each query settles once, whatever path it takes. The query text, parameters, connection id and server address are filled in when the connection builds the query, because serializers depend on fetched types. postgres:query:send is published at that point with the same context. The connection span covers one acquisition, from the first attempt to the hand-off or the failure that ends it. sql.begin() and sql.savepoint() publish on the transaction channel. With no subscriber a query costs one hasSubscribers check and allocates nothing. resolve and reject stay single-expression arrows for dd-trace, and the build, toBuffer and handler shapes it rewrites are unchanged. The Deno build maps the module to node:diagnostics_channel and leaves the new test file out, as Deno's std shim has neither.
The new tests subscribe to the channels and check the event order and the context for a plain query, a failing one, cursors read with a callback and with for await (to the end and on break), forEach, COPY in both directions, describe, simple, unsafe, file, pipelined queries, a query cancelled before it is sent, a reserved connection, a transaction with a savepoint and one that rolls back, and a connection that succeeds, fails to authenticate and is refused. They also check that a store bound to start is current where the query and the transaction run, and that nothing is published while nothing subscribes. The workerd scenario runs at every compatibility date in the matrix. It asserts that tracingChannel, runStores and bindStore exist there and that one query publishes the connection and query events in order.
Adds the Tracing channels section to the API reference with the channel list, the order of events, when each context field is set and a subscriber example, and a line each in the README feature lists.
The channels rewrite code that dd-trace and Sentry hook into by shape, and npm run test:tracing was only ever run by hand. It packs the fork, starts a throwaway cluster and compares the spans against Postgres.js 3.4.9, so a shape change that loses spans now fails a check instead of reaching a release.
…t is already settled A transaction context was created with a result key set to undefined. Sentry treats a result or error key present at end as a finished synchronous call and closes the span there, so transaction and savepoint spans ended at once. The key now appears only when the commit result is set. A query cancelled before it was awaited was rejected without a trace, then started one when awaited, and the driver dropped it before sending, so its span never closed. A query that has already settled no longer starts a trace. The connection and transaction contexts are now built by helpers in tracing.js next to the query ones, so the payload field names live in one source file.
A regression that loses dd-trace or Sentry spans should stop a publish like a failing test does, so the publish job now waits for the tracing job.
…nels Adds a check that no context carries a result or error key before it is set, cancel-before-await with several connections, one connection and a pool that must connect, a retried query, a server error and a throwing callback in the middle of a cursor, cancellation from the queue of a reserved connection and of a transaction, and queries rejected as the pool ends. The caller-context test now runs the query inside a store of its own and asserts the bound transform sees it.
…s published asyncEnd also fires after an error, where result is absent, so the example reads it with optional chaining. postgres:query:send is published only for queries that had a postgres:query subscriber when they started.
…channels (#73) The query, connect and pool payloads now use the field names of the node-postgres tracing channels, so a subscriber written for one driver reads the other with few changes. postgres:query carries { query: { text, name }, client: { database, host, port, user, ssl, processID } }. Parameter values are no longer published. The client holds the configured values at start and the actual host, port, tls state and backend pid once the query is written to a connection. postgres:connection is now postgres:connect with { connection: { database, host, port, user, ssl } }, and postgres:connection:close is now postgres:pool:remove with { client: { processID }, error }. postgres:pool:connect traces every lease acquisition (sql.reserve() and sql.begin()) with the pool counts at start and { processID, reused } on success. Its failures come from the place the lease already settles. postgres:pool:release is published when a lease gives its connection back, with the error the lease ended with: the failure that ended a transaction before its connection was released, or the error that closed the connection. A commit the server answered with a rollback is reported after the release, so the release carries no error then. postgres:transaction uses the same client object.
The existing tests read the node-postgres style fields. New tests cover postgres:pool:connect with the pool counts at start and reused false then true, a checkout that fails (refused connection, ended pool) with no client, postgres:pool:release for a reserved connection (once, with the checkout's process id) and for transactions that commit and that reject, a named prepared statement, and the pool channels staying silent without a subscriber. The workerd scenario reads the renamed connect channel.
…nels Lists the tracing and plain channels, shows how they fit together, and gives one field table per channel with when each field is set.
) A plain pooled query now publishes postgres:pool:connect as a child of its query span. The span starts when the pool routes the query and ends when the connection makes it the active query, whether the connection was idle or the query had to wait behind others, so postgres:connect for a connection the routing opens nests inside it. When a connection takes the query over it records handoverTime (epoch milliseconds), queuedBehind (the queries already running or waiting on that connection) and the client with reused false for a query that opened its connection. handoverTime minus the start time is the wait for a connection, and the rest is time behind other queries. A lease records the same fields with queuedBehind 0. A query that settles while its pool wait is still open fails the wait first, from the query's own settlement, so ending the pool, a copy in progress, cancellation, a failed connection attempt and a connection lost behind other queries all end the span exactly once, before the query's asyncEnd. postgres:query:send is removed: the pool span now shows the wait, and the query context is still filled in when the query is built.
A query refused while it was being routed (the pool is ending, or a COPY holds every connection) was rejected inside the pool span's run, so the span published its error before its end. The routing now returns the refusal and the handler rejects after run, which gives the order of tracePromise.
…checks Checks the pool span for a query on an idle connection, on a connection it opens (connect nested in the pool span through a bound store), sent ahead to a busy connection (it ends after the earlier query's result), and drained from the pool queue. Failures before the query starts are covered for an ending pool, a COPY in progress, cancellation in the pool queue, a refused connection, and a connection lost while the query waited behind others. Each span settles once and before its query, and nothing is published when only one of the query and pool channels has a subscriber.
…nnel pool:connect now covers every wait for a connection. The page explains how handoverTime and queuedBehind split the wait, notes the coarse clock on Cloudflare Workers, and shows an OpenTelemetry span event at the handover.
…on is idle A query that finds an idle connection becomes active inside the routing, so the pool span settled before it published end. The settlement is held until end has been published, which gives the order of tracePromise.
…s routed A query that failed to build while the pool routed it onto an idle connection settled its pool span twice, the second time with no context, and a build failure on a busy connection published the pool span's error before its end. While the routing is in progress the query's failure is now held, and after the span's end the pool span is resolved if the query was activated or failed otherwise, followed by the query's own failure. The state of the wait lives on a private object, so an untraced query carries only trace and poolWait. A lease now reports the queries its connection really had in flight, and a queued query that a reconnect hands to connect() counts as having opened the connection.
… on leases Adds the build-failure cases on an idle and on a pipelining connection with subscribers that dereference the context, a lease taken on a busy connection, and statements inside begin and reserve publishing no plain pool span. The lost-connection test now checks the pool span ends before its query, and the tests that raced a delay against a sleeping query wait for the routing with setImmediate instead.
handoverTime, queuedBehind and client are set once a connection took the call over, which also covers a query that fails after that. The OpenTelemetry example skips waits that never had a hand-over.
…the driver module asks for it The edge worker reads an optional subscribe export of the generated driver module and subscribes at module scope, with an AsyncLocalStorage bound to each tracing channel's start so runStores does real work. Without the export nothing changes.
ab_subscribe (none, query, all) makes prepare export the subscription mode to the worker. The base copy's tracing module gets its own channel names, so the no-op subscribers reach only the head build even when both are this branch; with none nothing is rewritten and the job behaves as before.
…by a reconnect A build error for a query routed onto a connection failed the pool span when the query was pipelined behind others but resolved it on an idle or fresh connection. Once a connection took the query over the pool span now resolves whatever the route, so a build error fails only the query span. A queued lease that a closed connection's reconnect served reported reused true. connect() now flags the lease waiter and the acquisition reads the flag.
…ind the initial query The busy-connection build failure now expects a resolved pool span. The reconnect tests count the spans reporting reused false: the lease one is 0 without the flag on the waiter. The lease behind the initial query reports queuedBehind 1; it was 0 before leases read the connection's in-flight count.
…used means A lease span ends a microtask after the hand-over, so the time behind the queries ahead of it falls inside its first statement. reused is false only for the call that had to open the connection.
Author
|
Superseded by #196, which rebuilds the tracing channels on the request lifecycle restructure. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #73.
What it adds
The driver publishes
diagnostics_channelevents, only while something subscribes. Names and fields follow the node-postgres tracing channels (brianc/node-postgres#3650);postgres:transactionandprocessIDon the query client are additions.postgres:query{ query: { text, name }, client: { database, host, port, user, ssl, processID }, result?, error? }postgres:connect{ connection: { database, host, port, user, ssl }, error? }, one span per acquisitionpostgres:pool:connectsql.reserve(),sql.begin()and plain pooled queries (a child of the query span, ending when the connection starts the query).{ pool: { totalCount, idleCount, waitingCount, maxSize } }, thenhandoverTime,queuedBehindandclient: { processID, reused }on successpostgres:transaction{ client, options, savepoint, prepared, result?: { committed }, error? }forsql.begin()andsql.savepoint()postgres:pool:release{ client: { processID }, error }when a reserved connection or a transaction gives its connection backpostgres:pool:remove{ client: { processID }, error }when a connection closes;erroris undefined for a clean closeNo payload contains parameter values.
resultanderrorkeys appear only once set. A query that settles before its connection starts it fails the pool span first.query.text,query.name, the actual host, port, tls state andprocessIDare filled in when the query is written to a connection.startgoes throughstart.runStoresin the caller's synchronous context. The async events come from the query's own settlement rather thantracePromise, so cursors (both forms),forEach, COPY streams,describe(),simple(),unsafe(),file(), pipelined queries, retries and cancelled queries each settle exactly once. With no subscriber a query costs onehasSubscriberscheck and allocates nothing.resolveandrejectinsrc/query.jsstay single-expression arrows for dd-trace. The reference is the new "Tracing channels" section indocs/api.md.How it was tested
tests/tracing.jsruns in the Node and CJS suites against PostgreSQL: event order and context for each query path, transactions and savepoints, connect success, authentication failure and refusal, pool spans for leases and plain queries, pool releases, store propagation throughbindStore, cancellation before await, retries, mid-cursor errors, pool ending, and no publishing without subscribers.tests-startup/workerd-worker.jshas atracing-channelsscenario at every compatibility date of the workerd matrix, and in the edge Preview.node:diagnostics_channel; the new test file is left out of the Deno build.tracingjob intest.ymlrunsnpm run test:tracingon every pull request and gates the publish job: dd-trace and Sentry record the same spans as with Postgres.js 3.4.9.