Skip to content

Commit b269a72

Browse files
bojbrooktlhuntercrysmagsBridgeARrochdev
authored
Support for Prisma client library (#5605)
Next to the prisma instrumentation this is also improving the way externals is able to install additional dependencies in our test setup. The dependency version may now be pinned to the main dependency version to align these by specifying the main dependency as `dep`. --------- Co-authored-by: Thomas Hunter II <tlhunter@datadog.com> Co-authored-by: Crystal Magloire <crys.magloire@gmail.com> Co-authored-by: Ruben Bridgewater <ruben.bridgewater@datadoghq.com> Co-authored-by: Roch Devost <roch.devost@datadoghq.com>
1 parent 9f9568b commit b269a72

File tree

20 files changed

+658
-13
lines changed

20 files changed

+658
-13
lines changed

.github/workflows/apm-integrations.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,21 @@ jobs:
825825
steps:
826826
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
827827
- uses: ./.github/actions/plugins/test
828+
prisma:
829+
runs-on: ubuntu-latest
830+
services:
831+
postgres:
832+
image: postgres:9.5
833+
env:
834+
POSTGRES_PASSWORD: postgres
835+
ports:
836+
- 5432:5432
837+
env:
838+
PLUGINS: prisma
839+
SERVICES: prisma
840+
steps:
841+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
842+
- uses: ./.github/actions/plugins/test
828843

829844
protobufjs:
830845
runs-on: ubuntu-latest

docs/API.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ tracer.use('pg', {
9090
<h5 id="pg"></h5>
9191
<h5 id="pg-tags"></h5>
9292
<h5 id="pg-config"></h5>
93+
<h5 id="prisma"></h5>
9394
<h5 id="protobufjs"></h5>
9495
<h5 id="redis"></h5>
9596
<h5 id="redis-tags"></h5>
@@ -147,6 +148,7 @@ tracer.use('pg', {
147148
* [oracledb](./interfaces/export_.plugins.oracledb.html)
148149
* [pino](./interfaces/export_.plugins.pino.html)
149150
* [pg](./interfaces/export_.plugins.pg.html)
151+
* [primsa](./interfaces/export_.plugins.prisma.html)
150152
* [promise](./interfaces/export_.plugins.promise.html)
151153
* [promise-js](./interfaces/export_.plugins.promise_js.html)
152154
* [protobufjs](./interfaces/export_.plugins.protobufjs.html)

docs/add-redirects.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ declare -a plugins=(
5555
"oracledb"
5656
"pino"
5757
"pg"
58+
"prisma"
5859
"promise"
5960
"promise_js"
6061
"protobufjs"

docs/test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ tracer.use('playwright');
388388
tracer.use('pg');
389389
tracer.use('pg', { service: params => `${params.host}-${params.database}` });
390390
tracer.use('pino');
391+
tracer.use('prisma');
391392
tracer.use('protobufjs');
392393
tracer.use('redis');
393394
tracer.use('redis', redisOptions);

index.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ interface Plugins {
212212
"playwright": tracer.plugins.playwright;
213213
"pg": tracer.plugins.pg;
214214
"pino": tracer.plugins.pino;
215+
"prisma": tracer.plugins.prisma;
215216
"protobufjs": tracer.plugins.protobufjs;
216217
"redis": tracer.plugins.redis;
217218
"restify": tracer.plugins.restify;
@@ -1271,6 +1272,15 @@ declare namespace tracer {
12711272
meta?: boolean;
12721273
}
12731274

1275+
/** @hidden */
1276+
interface Prisma extends Instrumentation {}
1277+
1278+
/** @hidden */
1279+
interface PrismaClient extends Prisma {}
1280+
1281+
/** @hidden */
1282+
interface PrismaEngine extends Prisma {}
1283+
12741284
/**
12751285
* This plugin automatically instruments the
12761286
* [aerospike](https://github.com/aerospike/aerospike-client-nodejs) for module versions >= v3.16.2.
@@ -1934,6 +1944,23 @@ declare namespace tracer {
19341944
* on the tracer.
19351945
*/
19361946
interface pino extends Integration {}
1947+
1948+
/**
1949+
* This plugin automatically instruments the
1950+
* [@prisma/client](https://www.prisma.io/docs/orm/prisma-client) module.
1951+
*/
1952+
interface prisma extends PrismaClient, PrismaEngine {
1953+
/**
1954+
* Configuration for prisma client.
1955+
*/
1956+
client?: PrismaClient | boolean,
1957+
1958+
/**
1959+
* Configuration for Prisma engine.
1960+
*/
1961+
engine?: PrismaEngine | boolean
1962+
}
1963+
19371964
/**
19381965
* This plugin automatically patches the [protobufjs](https://protobufjs.github.io/protobuf.js/)
19391966
* to collect protobuf message schemas when Datastreams Monitoring is enabled.

packages/datadog-instrumentations/src/helpers/hooks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module.exports = {
2424
'@node-redis/client': () => require('../redis'),
2525
'@opensearch-project/opensearch': () => require('../opensearch'),
2626
'@opentelemetry/sdk-trace-node': () => require('../otel-sdk-trace'),
27+
'@prisma/client': () => require('../prisma'),
2728
'@redis/client': () => require('../redis'),
2829
'@smithy/smithy-client': () => require('../aws-sdk'),
2930
'@vitest/runner': { esmFirst: true, fn: () => require('../vitest') },
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict'
2+
3+
const {
4+
channel,
5+
addHook
6+
} = require('./helpers/instrument')
7+
8+
const prismaEngineStart = channel('apm:prisma:engine:start')
9+
const tracingChannel = require('dc-polyfill').tracingChannel
10+
const clientCH = tracingChannel('apm:prisma:client')
11+
12+
const allowedClientSpanOperations = new Set([
13+
'operation',
14+
'serialize',
15+
'transaction'
16+
])
17+
18+
class TracingHelper {
19+
dbConfig = null
20+
isEnabled () {
21+
return true
22+
}
23+
24+
// needs a sampled tracecontext to generate engine spans
25+
getTraceParent (context) {
26+
return '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01' // valid sampled traceparent
27+
}
28+
29+
dispatchEngineSpans (spans) {
30+
for (const span of spans) {
31+
if (span.parentId === null) {
32+
prismaEngineStart.publish({ engineSpan: span, allEngineSpans: spans, dbConfig: this.dbConfig })
33+
}
34+
}
35+
}
36+
37+
getActiveContext () {}
38+
39+
runInChildSpan (options, callback) {
40+
if (typeof options === 'string') {
41+
options = {
42+
name: options
43+
}
44+
}
45+
46+
if (allowedClientSpanOperations.has(options.name)) {
47+
const ctx = {
48+
resourceName: options.name,
49+
attributes: options.attributes || {}
50+
}
51+
52+
if (options.name !== 'serialize') {
53+
return clientCH.tracePromise(callback, ctx, this, ...arguments)
54+
}
55+
56+
return clientCH.traceSync(callback, ctx, this, ...arguments)
57+
}
58+
return callback()
59+
}
60+
61+
setDbString (dbConfig) {
62+
this.dbConfig = dbConfig
63+
}
64+
}
65+
66+
addHook({ name: '@prisma/client', versions: ['>=6.1.0'] }, (prisma, version) => {
67+
const tracingHelper = new TracingHelper()
68+
69+
/*
70+
* This is a custom PrismaClient that extends the original PrismaClient
71+
* This allows us to grab additional information from the PrismaClient such as DB connection strings
72+
*/
73+
class PrismaClient extends prisma.PrismaClient {
74+
constructor (...args) {
75+
super(...args)
76+
77+
const datasources = this._engine?.config.inlineDatasources?.db.url?.value
78+
if (datasources) {
79+
const result = parseDBString(datasources)
80+
tracingHelper.setDbString(result)
81+
}
82+
}
83+
}
84+
85+
prisma.PrismaClient = PrismaClient
86+
/*
87+
* This is taking advantage of the built in tracing support from Prisma.
88+
* The below variable is setting a global tracing helper that Prisma uses
89+
* to enable OpenTelemetry.
90+
*/
91+
// https://github.com/prisma/prisma/blob/478293bbfce91e41ceff02f2a0b03bb8acbca03e/packages/instrumentation/src/PrismaInstrumentation.ts#L42
92+
const versions = version.split('.')
93+
if (versions[0] === '6' && versions[1] < 4) {
94+
global.PRISMA_INSTRUMENTATION = {
95+
helper: tracingHelper
96+
}
97+
} else {
98+
global[`V${versions[0]}_PRISMA_INSTRUMENTATION`] = {
99+
helper: tracingHelper
100+
}
101+
}
102+
103+
return prisma
104+
})
105+
106+
function parseDBString (dbString) {
107+
const url = new URL(dbString)
108+
const dbConfig = {
109+
user: url.username,
110+
password: url.password,
111+
host: url.hostname,
112+
port: url.port,
113+
database: url.pathname.slice(1) // Remove leading slash
114+
}
115+
return dbConfig
116+
}

packages/datadog-plugin-next/test/next.config.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ if (satisfies(VERSION, '<11')) {
1818
}
1919
}
2020

21-
// In older versions of Next.js (11.0.1 and before), the webpack config doesn't support 'node' prefixes by default
21+
// In older versions of Next.js (11.X and before), the webpack config doesn't support 'node' prefixes by default
2222
// So, any "node" prefixes are replaced for these older versions by this webpack plugin
2323
// Additionally, webpack was having problems with our use of 'worker_threads', so we don't resolve it
24-
if (satisfies(VERSION, '<11.1.0')) {
24+
if (satisfies(VERSION, '<=11')) {
2525
config.webpack = (config, { webpack }) => {
2626
config.plugins.push(
2727
new webpack.NormalModuleReplacementPlugin(/^node:/, resource => {
@@ -35,7 +35,8 @@ if (satisfies(VERSION, '<11.1.0')) {
3535
config.resolve.fallback = {
3636
...config.resolve.fallback,
3737
worker_threads: false,
38-
perf_hooks: false
38+
perf_hooks: false,
39+
'util/types': false
3940
}
4041

4142
return config
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict'
2+
const DatabasePlugin = require('../../dd-trace/src/plugins/database')
3+
class PrismaClientPlugin extends DatabasePlugin {
4+
static get id () { return 'prisma' }
5+
static get operation () { return 'client' }
6+
static get system () { return 'prisma' }
7+
static get prefix () {
8+
return 'tracing:apm:prisma:client'
9+
}
10+
11+
bindStart (ctx) {
12+
const service = this.serviceName({ pluginConfig: this.config })
13+
const resource = formatResourceName(ctx.resourceName, ctx.attributes)
14+
15+
const options = { service, resource }
16+
17+
if (ctx.resourceName === 'operation') {
18+
options.meta = {
19+
prisma: {
20+
method: ctx.attributes.method,
21+
model: ctx.attributes.model,
22+
type: 'client'
23+
}
24+
}
25+
}
26+
const operationName = this.operationName({ operation: this.operation })
27+
this.startSpan(operationName, options, ctx)
28+
29+
return ctx.currentStore
30+
}
31+
32+
end (ctx) {
33+
// Only synchronous operations would have `result` on `end`.
34+
if (Object.hasOwn(ctx, 'result')) {
35+
this.finish(ctx)
36+
}
37+
}
38+
39+
bindAsyncStart (ctx) {
40+
return this.bindFinish(ctx)
41+
}
42+
43+
asyncStart (ctx) {
44+
this.finish(ctx)
45+
}
46+
47+
error (error) {
48+
this.addError(error)
49+
}
50+
}
51+
52+
function formatResourceName (resource, attributes) {
53+
if (attributes?.name) {
54+
return `${attributes.name}`.trim()
55+
}
56+
if (attributes?.model && attributes.method) {
57+
return `${attributes.model}.${attributes.method}`.trim()
58+
}
59+
return resource
60+
}
61+
62+
module.exports = PrismaClientPlugin
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use strict'
2+
3+
const DatabasePlugin = require('../../dd-trace/src/plugins/database')
4+
const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
5+
6+
const databaseDriverMapper = {
7+
postgresql: {
8+
type: 'sql',
9+
'db.type': 'postgres'
10+
},
11+
mysql: {
12+
type: 'sql',
13+
'db.type': 'mysql'
14+
},
15+
mongodb: {
16+
type: 'mongodb',
17+
'db.type': 'mongodb'
18+
},
19+
sqlite: {
20+
type: 'sql',
21+
'db.type': 'sqlite'
22+
}
23+
}
24+
25+
class PrismaEngine extends DatabasePlugin {
26+
static get id () { return 'prisma' }
27+
static get operation () { return 'engine' }
28+
static get system () { return 'prisma' }
29+
30+
start (ctx) {
31+
const { engineSpan, allEngineSpans, childOf, dbConfig } = ctx
32+
const service = this.serviceName({ pluginConfig: this.config, system: this.system })
33+
const spanName = engineSpan.name.slice(14) // remove 'prisma:engine:' prefix
34+
const options = {
35+
childOf,
36+
resource: spanName,
37+
service,
38+
kind: engineSpan.kind,
39+
meta: {
40+
prisma: {
41+
name: spanName,
42+
type: 'engine'
43+
}
44+
}
45+
}
46+
47+
if (spanName === 'db_query') {
48+
const query = engineSpan.attributes['db.query.text']
49+
const originalStatement = this.maybeTruncate(query)
50+
const type = databaseDriverMapper[engineSpan.attributes['db.system']]?.type
51+
const dbType = databaseDriverMapper[engineSpan.attributes['db.system']]?.['db.type']
52+
53+
options.resource = originalStatement
54+
options.type = type || engineSpan.attributes['db.system']
55+
options.meta['db.type'] = dbType || engineSpan.attributes['db.system']
56+
options.meta['db.instance'] = dbConfig?.database
57+
options.meta['db.name'] = dbConfig?.user
58+
options.meta['out.host'] = dbConfig?.host
59+
options.meta[CLIENT_PORT_KEY] = dbConfig?.port
60+
}
61+
62+
const activeSpan = this.startSpan(this.operationName({ operation: this.operation }), options)
63+
activeSpan._startTime = hrTimeToUnixTimeMs(engineSpan.startTime)
64+
for (const span of allEngineSpans) {
65+
if (span.parentId === engineSpan.id) {
66+
const startCtx = { engineSpan: span, allEngineSpans, childOf: activeSpan, dbConfig }
67+
this.start(startCtx)
68+
}
69+
}
70+
const unixEndTime = hrTimeToUnixTimeMs(engineSpan.endTime)
71+
activeSpan.finish(unixEndTime)
72+
}
73+
}
74+
75+
// Opentelemetry time format is defined here
76+
// https://github.com/open-telemetry/opentelemetry-js/blob/cbc912d/api/src/common/Time.ts#L19-L30.
77+
function hrTimeToUnixTimeMs ([seconds, nanoseconds]) {
78+
return seconds * 1000 + nanoseconds / 1e6
79+
}
80+
81+
module.exports = PrismaEngine

0 commit comments

Comments
 (0)