-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: Migrate Prisma from 6.14.0 to 7.7.0 with driver adapters #3389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import { | |
| type PrismaTransactionClient, | ||
| type PrismaTransactionOptions, | ||
| } from "@trigger.dev/database"; | ||
| import { PrismaPg } from "@prisma/adapter-pg"; | ||
| import invariant from "tiny-invariant"; | ||
| import { z } from "zod"; | ||
| import { env } from "./env.server"; | ||
|
|
@@ -109,21 +110,22 @@ function getClient() { | |
| const { DATABASE_URL } = process.env; | ||
| invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set"); | ||
|
|
||
| const databaseUrl = extendQueryParams(DATABASE_URL, { | ||
| connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(), | ||
| pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(), | ||
| connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(), | ||
| application_name: env.SERVICE_NAME, | ||
| }); | ||
| const databaseUrl = new URL(DATABASE_URL); | ||
|
|
||
| // Set application_name as a query param on the connection string (pg understands this) | ||
| databaseUrl.searchParams.set("application_name", env.SERVICE_NAME); | ||
|
|
||
| console.log(`🔌 setting up prisma client to ${redactUrlSecrets(databaseUrl)}`); | ||
|
|
||
| const adapter = new PrismaPg({ | ||
| connectionString: databaseUrl.href, | ||
| max: env.DATABASE_CONNECTION_LIMIT, | ||
| idleTimeoutMillis: env.DATABASE_POOL_TIMEOUT * 1000, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 DATABASE_POOL_TIMEOUT incorrectly mapped to idleTimeoutMillis instead of a connection acquisition timeout The Impact on production behavior
The old Prisma Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — this was a real semantic bug. Fixed in commit a59aebc. The old Prisma Note that |
||
| connectionTimeoutMillis: env.DATABASE_CONNECTION_TIMEOUT * 1000, | ||
| }); | ||
|
|
||
| const client = new PrismaClient({ | ||
| datasources: { | ||
| db: { | ||
| url: databaseUrl.href, | ||
| }, | ||
| }, | ||
| adapter, | ||
| log: [ | ||
| // events | ||
| { | ||
|
|
@@ -233,21 +235,20 @@ function getReplicaClient() { | |
| return; | ||
| } | ||
|
|
||
| const replicaUrl = extendQueryParams(env.DATABASE_READ_REPLICA_URL, { | ||
| connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(), | ||
| pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(), | ||
| connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(), | ||
| application_name: env.SERVICE_NAME, | ||
| }); | ||
| const replicaUrl = new URL(env.DATABASE_READ_REPLICA_URL); | ||
| replicaUrl.searchParams.set("application_name", env.SERVICE_NAME); | ||
|
|
||
| console.log(`🔌 setting up read replica connection to ${redactUrlSecrets(replicaUrl)}`); | ||
|
|
||
| const adapter = new PrismaPg({ | ||
| connectionString: replicaUrl.href, | ||
| max: env.DATABASE_CONNECTION_LIMIT, | ||
| idleTimeoutMillis: env.DATABASE_POOL_TIMEOUT * 1000, | ||
| connectionTimeoutMillis: env.DATABASE_CONNECTION_TIMEOUT * 1000, | ||
| }); | ||
|
|
||
| const replicaClient = new PrismaClient({ | ||
| datasources: { | ||
| db: { | ||
| url: replicaUrl.href, | ||
| }, | ||
| }, | ||
| adapter, | ||
| log: [ | ||
| // events | ||
| { | ||
|
|
@@ -350,19 +351,6 @@ function getReplicaClient() { | |
| return replicaClient; | ||
| } | ||
|
|
||
| function extendQueryParams(hrefOrUrl: string | URL, queryParams: Record<string, string>) { | ||
| const url = new URL(hrefOrUrl); | ||
| const query = url.searchParams; | ||
|
|
||
| for (const [key, val] of Object.entries(queryParams)) { | ||
| query.set(key, val); | ||
| } | ||
|
|
||
| url.search = query.toString(); | ||
|
|
||
| return url; | ||
| } | ||
|
|
||
| function redactUrlSecrets(hrefOrUrl: string | URL) { | ||
| const url = new URL(hrefOrUrl); | ||
| url.password = ""; | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 @prisma/instrumentation version not updated alongside Prisma 7 migration The webapp's (Refers to line 37) Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — updated |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,9 +54,7 @@ import { LoggerSpanExporter } from "./telemetry/loggerExporter.server"; | |
| import { CompactMetricExporter } from "./telemetry/compactMetricExporter.server"; | ||
| import { logger } from "~/services/logger.server"; | ||
| import { flattenAttributes } from "@trigger.dev/core/v3"; | ||
| import { prisma } from "~/db.server"; | ||
| import { metricsRegister } from "~/metrics.server"; | ||
| import type { Prisma } from "@trigger.dev/database"; | ||
| import { performance } from "node:perf_hooks"; | ||
|
|
||
| export const SEMINTATTRS_FORCE_RECORDING = "forceRecording"; | ||
|
|
@@ -330,221 +328,12 @@ function setupMetrics() { | |
|
|
||
| const meter = meterProvider.getMeter("trigger.dev", "3.3.12"); | ||
|
|
||
| configurePrismaMetrics({ meter }); | ||
| configureNodejsMetrics({ meter }); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Prisma metrics fully removed — loss of database pool observability The PR removes all Prisma Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is intentional — the removal of |
||
| configureHostMetrics({ meterProvider }); | ||
|
|
||
| return meter; | ||
| } | ||
|
|
||
| function configurePrismaMetrics({ meter }: { meter: Meter }) { | ||
| // Counters | ||
| const queriesTotal = meter.createObservableCounter("db.client.queries.total", { | ||
| description: "Total number of Prisma Client queries executed", | ||
| unit: "queries", | ||
| }); | ||
| const datasourceQueriesTotal = meter.createObservableCounter("db.datasource.queries.total", { | ||
| description: "Total number of datasource queries executed", | ||
| unit: "queries", | ||
| }); | ||
| const connectionsOpenedTotal = meter.createObservableCounter("db.pool.connections.opened.total", { | ||
| description: "Total number of pool connections opened", | ||
| unit: "connections", | ||
| }); | ||
| const connectionsClosedTotal = meter.createObservableCounter("db.pool.connections.closed.total", { | ||
| description: "Total number of pool connections closed", | ||
| unit: "connections", | ||
| }); | ||
|
|
||
| // Gauges | ||
| const queriesActive = meter.createObservableGauge("db.client.queries.active", { | ||
| description: "Number of currently active Prisma Client queries", | ||
| unit: "queries", | ||
| }); | ||
| const queriesWait = meter.createObservableGauge("db.client.queries.wait", { | ||
| description: "Number of queries currently waiting for a connection", | ||
| unit: "queries", | ||
| }); | ||
| const totalGauge = meter.createObservableGauge("db.pool.connections.total", { | ||
| description: "Open Prisma-pool connections", | ||
| unit: "connections", | ||
| }); | ||
| const busyGauge = meter.createObservableGauge("db.pool.connections.busy", { | ||
| description: "Connections currently executing queries", | ||
| unit: "connections", | ||
| }); | ||
| const freeGauge = meter.createObservableGauge("db.pool.connections.free", { | ||
| description: "Idle (free) connections in the pool", | ||
| unit: "connections", | ||
| }); | ||
|
|
||
| // Histogram statistics as gauges | ||
| const queriesWaitTimeCount = meter.createObservableGauge("db.client.queries.wait_time.count", { | ||
| description: "Number of wait time observations", | ||
| unit: "observations", | ||
| }); | ||
| const queriesWaitTimeSum = meter.createObservableGauge("db.client.queries.wait_time.sum", { | ||
| description: "Total wait time across all observations", | ||
| unit: "ms", | ||
| }); | ||
| const queriesWaitTimeMean = meter.createObservableGauge("db.client.queries.wait_time.mean", { | ||
| description: "Average wait time for a connection", | ||
| unit: "ms", | ||
| }); | ||
|
|
||
| const queriesDurationCount = meter.createObservableGauge("db.client.queries.duration.count", { | ||
| description: "Number of query duration observations", | ||
| unit: "observations", | ||
| }); | ||
| const queriesDurationSum = meter.createObservableGauge("db.client.queries.duration.sum", { | ||
| description: "Total query duration across all observations", | ||
| unit: "ms", | ||
| }); | ||
| const queriesDurationMean = meter.createObservableGauge("db.client.queries.duration.mean", { | ||
| description: "Average duration of Prisma Client queries", | ||
| unit: "ms", | ||
| }); | ||
|
|
||
| const datasourceQueriesDurationCount = meter.createObservableGauge( | ||
| "db.datasource.queries.duration.count", | ||
| { | ||
| description: "Number of datasource query duration observations", | ||
| unit: "observations", | ||
| } | ||
| ); | ||
| const datasourceQueriesDurationSum = meter.createObservableGauge( | ||
| "db.datasource.queries.duration.sum", | ||
| { | ||
| description: "Total datasource query duration across all observations", | ||
| unit: "ms", | ||
| } | ||
| ); | ||
| const datasourceQueriesDurationMean = meter.createObservableGauge( | ||
| "db.datasource.queries.duration.mean", | ||
| { | ||
| description: "Average duration of datasource queries", | ||
| unit: "ms", | ||
| } | ||
| ); | ||
|
|
||
| // Single helper so we hit Prisma only once per scrape --------------------- | ||
| async function readPrismaMetrics() { | ||
| const metrics = await prisma.$metrics.json(); | ||
|
|
||
| // Extract counter values | ||
| const counters: Record<string, number> = {}; | ||
| for (const counter of metrics.counters) { | ||
| counters[counter.key] = counter.value; | ||
| } | ||
|
|
||
| // Extract gauge values | ||
| const gauges: Record<string, number> = {}; | ||
| for (const gauge of metrics.gauges) { | ||
| gauges[gauge.key] = gauge.value; | ||
| } | ||
|
|
||
| // Extract histogram values | ||
| const histograms: Record<string, Prisma.MetricHistogram> = {}; | ||
| for (const histogram of metrics.histograms) { | ||
| histograms[histogram.key] = histogram.value; | ||
| } | ||
|
|
||
| return { | ||
| counters: { | ||
| queriesTotal: counters["prisma_client_queries_total"] ?? 0, | ||
| datasourceQueriesTotal: counters["prisma_datasource_queries_total"] ?? 0, | ||
| connectionsOpenedTotal: counters["prisma_pool_connections_opened_total"] ?? 0, | ||
| connectionsClosedTotal: counters["prisma_pool_connections_closed_total"] ?? 0, | ||
| }, | ||
| gauges: { | ||
| queriesActive: gauges["prisma_client_queries_active"] ?? 0, | ||
| queriesWait: gauges["prisma_client_queries_wait"] ?? 0, | ||
| connectionsOpen: gauges["prisma_pool_connections_open"] ?? 0, | ||
| connectionsBusy: gauges["prisma_pool_connections_busy"] ?? 0, | ||
| connectionsIdle: gauges["prisma_pool_connections_idle"] ?? 0, | ||
| }, | ||
| histograms: { | ||
| queriesWait: histograms["prisma_client_queries_wait_histogram_ms"], | ||
| queriesDuration: histograms["prisma_client_queries_duration_histogram_ms"], | ||
| datasourceQueriesDuration: histograms["prisma_datasource_queries_duration_histogram_ms"], | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| meter.addBatchObservableCallback( | ||
| async (res) => { | ||
| const { counters, gauges, histograms } = await readPrismaMetrics(); | ||
|
|
||
| // Observe counters | ||
| res.observe(queriesTotal, counters.queriesTotal); | ||
| res.observe(datasourceQueriesTotal, counters.datasourceQueriesTotal); | ||
| res.observe(connectionsOpenedTotal, counters.connectionsOpenedTotal); | ||
| res.observe(connectionsClosedTotal, counters.connectionsClosedTotal); | ||
|
|
||
| // Observe gauges | ||
| res.observe(queriesActive, gauges.queriesActive); | ||
| res.observe(queriesWait, gauges.queriesWait); | ||
| res.observe(totalGauge, gauges.connectionsOpen); | ||
| res.observe(busyGauge, gauges.connectionsBusy); | ||
| res.observe(freeGauge, gauges.connectionsIdle); | ||
|
|
||
| // Observe histogram statistics as gauges | ||
| if (histograms.queriesWait) { | ||
| res.observe(queriesWaitTimeCount, histograms.queriesWait.count); | ||
| res.observe(queriesWaitTimeSum, histograms.queriesWait.sum); | ||
| res.observe( | ||
| queriesWaitTimeMean, | ||
| histograms.queriesWait.count > 0 | ||
| ? histograms.queriesWait.sum / histograms.queriesWait.count | ||
| : 0 | ||
| ); | ||
| } | ||
|
|
||
| if (histograms.queriesDuration) { | ||
| res.observe(queriesDurationCount, histograms.queriesDuration.count); | ||
| res.observe(queriesDurationSum, histograms.queriesDuration.sum); | ||
| res.observe( | ||
| queriesDurationMean, | ||
| histograms.queriesDuration.count > 0 | ||
| ? histograms.queriesDuration.sum / histograms.queriesDuration.count | ||
| : 0 | ||
| ); | ||
| } | ||
|
|
||
| if (histograms.datasourceQueriesDuration) { | ||
| res.observe(datasourceQueriesDurationCount, histograms.datasourceQueriesDuration.count); | ||
| res.observe(datasourceQueriesDurationSum, histograms.datasourceQueriesDuration.sum); | ||
| res.observe( | ||
| datasourceQueriesDurationMean, | ||
| histograms.datasourceQueriesDuration.count > 0 | ||
| ? histograms.datasourceQueriesDuration.sum / histograms.datasourceQueriesDuration.count | ||
| : 0 | ||
| ); | ||
| } | ||
| }, | ||
| [ | ||
| queriesTotal, | ||
| datasourceQueriesTotal, | ||
| connectionsOpenedTotal, | ||
| connectionsClosedTotal, | ||
| queriesActive, | ||
| queriesWait, | ||
| totalGauge, | ||
| busyGauge, | ||
| freeGauge, | ||
| queriesWaitTimeCount, | ||
| queriesWaitTimeSum, | ||
| queriesWaitTimeMean, | ||
| queriesDurationCount, | ||
| queriesDurationSum, | ||
| queriesDurationMean, | ||
| datasourceQueriesDurationCount, | ||
| datasourceQueriesDurationSum, | ||
| datasourceQueriesDurationMean, | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| function configureNodejsMetrics({ meter }: { meter: Meter }) { | ||
| if (!env.INTERNAL_OTEL_NODEJS_METRICS_ENABLED) { | ||
| return; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 Query event handler may not fire with driver adapters
Both the primary and replica clients register
$on('query', ...)handlers for query performance monitoring (apps/webapp/app/db.server.ts:220-222andapps/webapp/app/db.server.ts:342-343). With Prisma's new client engine (engineType = "client") and driver adapters, thequerylog event behavior may differ from the binary engine — in some adapter configurations, query events may not includeduration,params, orqueryfields, or may not fire at all. TheQueryPerformanceMonitor.onQuery()depends on these fields being present. If they're absent, slow query detection silently stops working without any error.(Refers to lines 220-222)
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid concern. According to Prisma 7 docs,
$on('query', ...)events are still supported with the new client engine and driver adapters — thequery,params, anddurationfields should still be populated. However, this should be verified at runtime in staging before production rollout. Added to the PR's testing checklist.