Merged
Size
S
Change Breakdown
Feature80%
Maintenance20%
#3422feat(webapp): tag Prisma spans with db.datasource attribute

Database traces filterable by writer and replica pools

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/:

  1. v3/tracer.server.ts — a DatasourceAttributeSpanProcessor reads an OTel context key in onStart and calls span.setAttribute("db.datasource", value). Registered as the first span processor.
  2. db.server.tstagDatasource(datasource, client) wraps each PrismaClient with $extends({ query: { $allOperations } }). The middleware sets the context key around the query and directly tags the active span (to catch prisma: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.datasource appears on prisma:engine:connection spans after the webapp is restarted
  • Spot-check a handful of real traces carry the attribute
© 2026 · via Gitpulse