Repository navigation
TracingChannel Proposal #29353
Description
Feature Summary
I'd like to propose adding first-class TracingChannel support to @prisma/client, following the pattern established by undici in Node.js core.
I would like propose emitting tracing channel events as part of the current observability/instrumentation story, either as a separate plugin @prisma/tracing or built-in given that tracing channels are a node built-in and it won't add much to existing codebases.
Use Cases & Problem Description
We started advocating for the adoption of tracing channels across the ecosystem since it solves many issues about the observability story in today's JavaScript. The main pain points are the usage of IITM/RITM and monkey patching and it's fragile nature. This makes instrumentations in general fragile in today's ecosystem where we can have multiple runtimes that may not support module hooks properly, and server-side bundling frameworks or single executable packing that escapes the module hooks cycle entirely.
The current @prisma/instrumentation is already ahead in a sense that it does not use IITM/RITM, no prototype patching, no module loader hacks which is great.
However, this architecture is tightly coupled to the OpenTelemetry API. APM tools that don't use the OTel SDK, Datadog's native tracer, Sentry's node-light SDK, custom in-house tracing, cannot subscribe to Prisma's internal operations without either:
- Adopting the full OTel SDK just to get Prisma spans.
- Reimplementing the
TracingHelpercontract privately (fragile, undocumented).
Tracing Channels offer a native way to build observable APIs by default, the notable features being:
- Context propagation is handled by default, all traced calls with
tracePromiseortraceCallbackpropagate and correlate the event lifecycle hooks automatically. - You can subscribe to them anywhere anytime, so no need to make sure to create the trace helper interface before setting up the client.
- Zero overhead if no subscribers are present.
- Backward compatible from node 18, and can be conditionally loaded with
process.getBuiltInModule.
Proposed Solution
Adopting Tracing channels can be done in a couple of ways, but first let me outline the channels I think could be a great start:
| TracingChannel | Tracks | Context fields |
|---|---|---|
prisma:client:operation |
All model operations — findMany, create, update, delete, upsert, aggregate, groupBy, count, etc. |
model, operation, args, database, serverAddress, serverPort |
prisma:client:query |
Raw SQL execution — $queryRaw, $executeRaw, and engine-level SQL dispatched by model operations |
query, params, database, serverAddress, serverPort, dbSystem |
prisma:client:transaction |
Transaction lifecycle — $transaction([...]) (batch) and $transaction(async (tx) => { ... }) (interactive) |
isolationLevel, transactionType, database, serverAddress, serverPort |
prisma:client:connect |
Engine lifecycle — $connect() |
database, serverAddress, serverPort, dbSystem |
Plain diagnostics_channel (fire-and-forget, no async lifecycle):
| Channel | Tracks | Context fields |
|---|---|---|
prisma:client:disconnect |
Engine teardown — $disconnect() |
database, serverAddress, serverPort |
With that in mind, we can use the tracing channels in these two ways:
Option A
The existing TracingHelper contract continues to power @prisma/instrumentation for OTel users, while TracingChannel provides an independent subscription point for non-OTel APM tools. Both coexist, emitting from the same code paths.
Option B
TracingChannel as the vehicle for @prisma/instrumentation. TracingChannel becomes the underlying emission mechanism. @prisma/client emits events via diagnostics_channel, and @prisma/instrumentation becomes a thin OTel subscriber that translates TracingChannel events into OTel spans, just like @opentelemetry/instrumentation-undici subscribes to undici:request. This eliminates the custom TracingHelper contract entirely in favor of a platform primitive.
@prisma/client ──TracingChannel──> @prisma/instrumentation (OTel subscriber)
└> Datadog subscriber
└> Sentry subscriber
└> Custom subscriber
This means users can subscribe to the tracing channel base layer and do whatever they need to, while at the same time getting OTEL data if they wish to continue doing so. I think this is the best of both worlds and provides a cleaner way for context propagation and span relationships implementation.
It would look like this at the end user side:
const dc = require('node:diagnostics_channel');
// Any APM tool: OTel, Sentry, Datadog, or custom subscribes directly
dc.tracingChannel('prisma:client:query').subscribe({
start(ctx) {
ctx.span = tracer.startSpan('prisma:query', {
attributes: {
'db.system.name': ctx.dbSystem,
'db.query.text': ctx.query,
'db.namespace': ctx.database,
'server.address': ctx.serverAddress,
'server.port': ctx.serverPort,
},
});
},
asyncEnd(ctx) { ctx.span?.end(); },
error(ctx) { ctx.span?.setStatus({ code: SpanStatusCode.ERROR }); },
});Alternative Solutions
I considered implementing the TracingHelper interface from @prisma/instrumentation-contract and set the global helper itself. I think it could work but it has drawbacks:
- It's a completely internal module, not a public API.
- Hardwired to OTEL in types shape.
- Only one global trace helper can exist, doesn't seem to be possible to have multiple APMs active at the same time.
- Global helper must be still created before the client is initialized. Posing a timing/order issue.
TracingChannel solves all of these by providing a vendor-neutral, platform-level primitive. Any APM tool (OTel-based or not) can subscribe to structured lifecycle events without depending on any tracing SDK. Multiple subscribers coexist by design, there's no initialization ordering constraint, and the contract is diagnostics_channel itself, which is a stable Node.js API.
Potential Considerations
Prior Art
This approach follows the same pattern already adopted or in progress by other major libraries:
undici(Node.js core) — shipsTracingChannelsupport since Node 20.12:undici:requestnode-redis— redis/node-redis#3195 (node-redis:command,node-redis:connect)ioredis— redis/ioredis#2089 (ioredis:command,ioredis:connect)pg/pg-pool— brianc/node-postgres#3624 (pg:query,pg:connection,pg:pool:connect)mysql2— sidorares/node-mysql2#4178 (mysql2:query,mysql2:execute,mysql2:connect,mysql2:pool:connect)
I have also pitched it to other database drivers and libraries and they are being discussed.
I'm more than happy to help spec this and submit a PR for it as i understand there is a roadmap, but the code change itself is minimal and I'm able to fully work on this and get it to the finish line.
Prisma Version
No response
What part of Prisma does this affect?
Prisma Client
Additional Context
No response
Pre-Submission Checklist
- I have searched existing issues to make sure this is not a duplicate
- I have checked the Prisma roadmap to see if this is already planned
- I have described why this belongs in Prisma Core rather than a solution in application code
Activity