Database traces filterable by writer and replica pools

Prisma query traces now include a datasource attribute, allowing engineers to easily filter traffic and distinguish between the primary writer and read replica connection pools.
Database query traces no longer blur the line between read replicas and primary writers. OpenTelemetry spans generated by Prisma are now explicitly tagged with a db.datasource attribute, categorizing each database query as either a writer or replica operation.
Previously, shared instrumentation meant spans from both client instances looked identical, making it difficult to verify if read-heavy operations were correctly routing to the replica pool rather than bogging down the primary database. By wrapping the Prisma clients and injecting context processors in the web application's tracer, telemetry data is now trivially filterable. This removes the guesswork from connection pool monitoring and allows teams to accurately track database load distribution.
View Original GitHub DescriptionFact Check
Summary
Stamp every Prisma span with db.datasource: "writer" | "replica" so traces can distinguish which client the query went through.
Both PrismaClient instances share the same global @prisma/instrumentation, so their spans come out with identical names and attributes today. This makes them trivially filterable.
How
Two pieces in apps/webapp/app/:
v3/tracer.server.ts— aDatasourceAttributeSpanProcessorreads an OTel context key inonStartand callsspan.setAttribute("db.datasource", value). Registered as the first span processor.db.server.ts—tagDatasource(datasource, client)wraps eachPrismaClientwith$extends({ query: { $allOperations } }). The middleware sets the context key around the query and directly tags the active span (to catchprisma:client:operation, which Prisma creates before the middleware fires).
Context-propagation gotcha
PrismaPromise is lazy — query(args) returns a thenable that only starts when someone .then()s it. The naive context.with(ctx, () => query(args)) restores ALS synchronously, so when Prisma's internal code awaits the thenable later, the engine spans fire with the original ALS. Wrapping as async () => await query(args) forces the .then() inside the context.with callback, so ALS stays on our context for the engine spans.
Coverage
- Tagged: all
prisma:engine:*(connection,db_query,serialize,query, etc.),prisma:client:operation,prisma:client:serialize,prisma:client:connect - Not tagged:
prisma:client:load_engine— one-time startup, fires before any query
Concurrent Promise.all([writer.x, replica.y]) correctly tags each pool separately (ALS isolates per-Promise chain).
Performance
One context.with (~200ns) and one setAttribute per span (effectively free per OTel JS benchmarks) per Prisma op. Negligible against a query path measured in milliseconds.
Test plan
- Verify
db.datasourceappears onprisma:engine:connectionspans after the webapp is restarted - Spot-check a handful of real traces carry the attribute