From b2101fd606af43093b9caf525dbef9e122f6ca7e Mon Sep 17 00:00:00 2001 From: Bob Evans Date: Thu, 11 May 2023 16:35:03 -0400 Subject: [PATCH] fix: updated prisma instrumentation to properly parse database connection strings that work across all versions of prisma (#1634) --- THIRD_PARTY_NOTICES.md | 29 +++ lib/instrumentation/@prisma/client.js | 75 ++++-- package-lock.json | 178 ++++++++++--- package.json | 1 + .../instrumentation/prisma-client.test.js | 241 ++++++++++-------- test/versioned/prisma/package.json | 2 +- test/versioned/prisma/prisma.tap.js | 2 +- third_party_manifest.json | 14 +- 8 files changed, 366 insertions(+), 176 deletions(-) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index ea9fb4e2a1..9548d0cc4d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -16,6 +16,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic * [@grpc/grpc-js](#grpcgrpc-js) * [@grpc/proto-loader](#grpcproto-loader) +* [@mrleebo/prisma-ast](#mrleeboprisma-ast) * [@newrelic/aws-sdk](#newrelicaws-sdk) * [@newrelic/koa](#newrelickoa) * [@newrelic/superagent](#newrelicsuperagent) @@ -504,6 +505,34 @@ This product includes source derived from [@grpc/proto-loader](https://github.co ``` +### @mrleebo/prisma-ast + +This product includes source derived from [@mrleebo/prisma-ast](https://github.com/MrLeebo/prisma-ast) ([v0.5.2](https://github.com/MrLeebo/prisma-ast/tree/v0.5.2)), distributed under the [MIT License](https://github.com/MrLeebo/prisma-ast/blob/v0.5.2/LICENSE): + +``` +MIT License + +Copyright (c) 2021 Jeremy Liberman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + ### @newrelic/aws-sdk This product includes source derived from [@newrelic/aws-sdk](https://github.com/newrelic/node-newrelic-aws-sdk) ([v5.0.2](https://github.com/newrelic/node-newrelic-aws-sdk/tree/v5.0.2)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-newrelic-aws-sdk/blob/v5.0.2/LICENSE): diff --git a/lib/instrumentation/@prisma/client.js b/lib/instrumentation/@prisma/client.js index 27857b7630..06858c9e2f 100644 --- a/lib/instrumentation/@prisma/client.js +++ b/lib/instrumentation/@prisma/client.js @@ -13,17 +13,42 @@ const parseSql = require('../../db/query-parsers/sql') const RAW_COMMANDS = ['executeRaw', 'queryRaw'] const semver = require('semver') +const { getSchema } = require('@mrleebo/prisma-ast') /** - * Extracts the connection url from env var or the .value prop - * Very similar to this helper: https://github.com/prisma/prisma/blob/main/packages/internals/src/utils/parseEnvValue.ts + * The library we use to parse the prisma schema retains double quotes around + * strings, and they need to be stripped * - * @param {string} datasource object from prisma config { url, fromEnvVar } - * @returns {string} connection string + * @param {string} [str=''] string to strip double-quotes from + * @returns {string} stripped string */ -function extractConnectionString(datasource = {}) { - return process.env[datasource.fromEnvVar] || datasource.value +function trimQuotes(str = '') { + return str.match(/"(.*)"/)[1] } + +/** + * You can set the connection string in schema as raw string, + * env var mapping, or an override at client instantiation time. + * + * @param {*} url string/object value of url in datsource stanza of schema + * @param {string} overrideUrl value of url in overrides at client instantiation + * @returns {string} properly parsed connection url + */ +function parseDataModelUrl(url, overrideUrl) { + let parsedUrl = '' + + if (overrideUrl) { + parsedUrl = overrideUrl + } else if (typeof url === 'string') { + parsedUrl = trimQuotes(url) + } else if (url.name && url.name === 'env') { + const envVar = trimQuotes(url.params[0]) + parsedUrl = process.env[envVar] + } + + return parsedUrl +} + /** * Parses a connection string. Most database engines in prisma are SQL and all * have similar engine strings. @@ -31,12 +56,10 @@ function extractConnectionString(datasource = {}) { * **Note**: This will not parse ms sql server, instead will log a warning * * @param {string} provider prisma provider(i.e. mysql, postgres, mongodb) - * @param {string} datasource object from prisma config { url, fromEnvVar } + * @param {string} connectionUrl connection string to db * @returns {object} { host, port, dbName } */ -function parseConnectionString(provider, datasource) { - const connectionUrl = extractConnectionString(datasource) - +function parseConnectionString(provider, connectionUrl) { let parameters = {} try { const parsedUrl = new URL(connectionUrl) @@ -124,24 +147,23 @@ function queryParser(query) { } /** - * Extracts the prisma connection information from the engine. In pre 4.11.0 this existed - * on a different object and was also a promise. + * Extracts the prisma connection information from the engine. This used to use + * prisma functions available on engine `getConfig` but that's no longer accessible. + * Instead we went the route of parsing the schema DSL. * * @param {object} client prisma client instance - * @param {string} pkgVersion prisma version - * @returns {Promise} returns prisma connection configuration + * @returns {Promise} returns prisma datasource connection configuration { provider, url } */ -function extractPrismaConfig(client, pkgVersion) { - if (semver.gte(pkgVersion, '4.11.0')) { - // wait for the library promise to resolve before getting the config - return client._engine.libraryInstantiationPromise.then(() => { - return client._engine.library.getConfig({ - datamodel: client._engine.datamodel, - ignoreEnvVarErrors: true - }) - }) +function extractPrismaDatasource(client) { + const { datamodel, datasourceOverrides: overrides } = client._engine + const schema = getSchema(datamodel) + const datasource = schema.list.filter(({ type }) => type === 'datasource')[0] + const urlData = datasource.assignments.filter(({ key }) => key === 'url')[0].value + const url = parseDataModelUrl(urlData, overrides[datasource.name]) + return { + provider: trimQuotes(datasource.assignments.filter(({ key }) => key === 'provider')[0].value), + url } - return client._engine.getConfig() } /** @@ -179,11 +201,10 @@ module.exports = async function initialize(_agent, prisma, _moduleName, shim) { * Adds the relevant host, port, database_name parameters * to the active segment */ - inContext: async function inContext() { + inContext: function inContext() { if (!client[prismaConnection]) { try { - const prismaConfig = await extractPrismaConfig(client, pkgVersion) - const activeDatasource = prismaConfig?.datasources[0] + const activeDatasource = extractPrismaDatasource(client) const dbParams = parseConnectionString( activeDatasource?.provider, activeDatasource?.url diff --git a/package-lock.json b/package-lock.json index c63246db29..97fefa0456 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@grpc/grpc-js": "^1.8.10", "@grpc/proto-loader": "^0.7.5", + "@mrleebo/prisma-ast": "^0.5.2", "@newrelic/aws-sdk": "^5.0.2", "@newrelic/koa": "^7.1.1", "@newrelic/superagent": "^6.0.0", @@ -507,6 +508,35 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -805,6 +835,17 @@ "node": ">=v12.0.0" } }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.5.2.tgz", + "integrity": "sha512-v2jwtrLt/x5/MaF7Sucsz/do8tDUmiq3KA+UYdyZfr3OQ2IGXUtpNSXmdlvyRM+vQ7Abn/FxpLW/qqhZGB9vhQ==", + "dependencies": { + "chevrotain": "^10.4.2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@newrelic/aws-sdk": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@newrelic/aws-sdk/-/aws-sdk-5.0.2.tgz", @@ -4747,6 +4788,19 @@ "node": "*" } }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -7650,9 +7704,9 @@ } }, "node_modules/is-core-module": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", - "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -8053,15 +8107,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/istanbul-lib-processinfo/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", @@ -8918,8 +8963,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash._reinterpolate": { "version": "3.0.0", @@ -10905,6 +10949,11 @@ "esprima": "~4.0.0" } }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" + }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -10979,12 +11028,12 @@ } }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -14253,6 +14302,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -14879,6 +14937,35 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "requires": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "requires": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==" + }, + "@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==" + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -15101,6 +15188,14 @@ "lodash": "^4.17.21" } }, + "@mrleebo/prisma-ast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.5.2.tgz", + "integrity": "sha512-v2jwtrLt/x5/MaF7Sucsz/do8tDUmiq3KA+UYdyZfr3OQ2IGXUtpNSXmdlvyRM+vQ7Abn/FxpLW/qqhZGB9vhQ==", + "requires": { + "chevrotain": "^10.4.2" + } + }, "@newrelic/aws-sdk": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@newrelic/aws-sdk/-/aws-sdk-5.0.2.tgz", @@ -18171,6 +18266,19 @@ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, + "chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "requires": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -20324,9 +20432,9 @@ "dev": true }, "is-core-module": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", - "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", "dev": true, "requires": { "has": "^1.0.3" @@ -20602,12 +20710,6 @@ "requires": { "glob": "^7.1.3" } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true } } }, @@ -21282,8 +21384,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash._reinterpolate": { "version": "3.0.0", @@ -22823,6 +22924,11 @@ "esprima": "~4.0.0" } }, + "regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" + }, "regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -22876,12 +22982,12 @@ } }, "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -25185,6 +25291,12 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, "v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", diff --git a/package.json b/package.json index 67d9cedeb2..d138ad6c30 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,7 @@ "dependencies": { "@grpc/grpc-js": "^1.8.10", "@grpc/proto-loader": "^0.7.5", + "@mrleebo/prisma-ast": "^0.5.2", "@newrelic/aws-sdk": "^5.0.2", "@newrelic/koa": "^7.1.1", "@newrelic/superagent": "^6.0.0", diff --git a/test/unit/instrumentation/prisma-client.test.js b/test/unit/instrumentation/prisma-client.test.js index 3da200baf6..ee25d8ae66 100644 --- a/test/unit/instrumentation/prisma-client.test.js +++ b/test/unit/instrumentation/prisma-client.test.js @@ -11,7 +11,7 @@ const helper = require('../../lib/agent_helper') const DatastoreShim = require('../../../lib/shim/datastore-shim.js') const symbols = require('../../../lib/symbols') const sinon = require('sinon') -const semver = require('semver') +const proxyquire = require('proxyquire') let agent = null let initialize = null @@ -19,61 +19,53 @@ let shim = null test('PrismaClient unit tests', (t) => { t.autoend() + let getSchemaSpy + let sandbox t.beforeEach(function () { + sandbox = sinon.createSandbox() // TODO: update to use loadMockedAgent with async local context manager when we drop Node 14 // enabling async local ctx mgr so I don't have to call instrumentMockedAgent which bootstraps // all instrumentation. Need context propagation for the inContext function // agent = helper.loadMockedAgent({ feature_flag: { async_local_context: true } }) agent = helper.instrumentMockedAgent() - initialize = require('../../../lib/instrumentation/@prisma/client') + const prismaAst = require('@mrleebo/prisma-ast') + getSchemaSpy = sandbox.spy(prismaAst, 'getSchema') + initialize = proxyquire('../../../lib/instrumentation/@prisma/client', { + '@mrleebo/prisma-ast': prismaAst + }) shim = new DatastoreShim(agent, 'prisma') - sinon.stub(shim, 'require') + sandbox.stub(shim, 'require') shim.require.returns({ version: '4.0.0' }) }) t.afterEach(function () { helper.unloadAgent(agent) + sandbox.restore() }) - function getMockModule(version = '4.0.0') { - function Engine() {} - - Engine.prototype.getConfig = sinon.stub() - let PrismaClient - if (semver.gte(version, '4.11.0')) { - PrismaClient = function () { - const libraryInstantiationPromise = new Promise((resolve) => resolve()) - this._engine = { - libraryInstantiationPromise - } - this._engine.library = new Engine() - } - } else { - PrismaClient = function () { - this._engine = new Engine() - } + function getMockModule() { + const PrismaClient = function () { + this._engine = { datamodel: {}, datasourceOverrides: {} } } - PrismaClient.prototype._executeRequest = sinon.stub().resolves() + PrismaClient.prototype._executeRequest = sandbox.stub().resolves() return PrismaClient } - t.test('should parse connection string from url.value', (t) => { + t.test('should get connection string from datasource url', (t) => { const MockPrismaClient = getMockModule() const prisma = { PrismaClient: MockPrismaClient } initialize(agent, prisma, '@prisma/client', shim) const client = new prisma.PrismaClient() - client._engine.getConfig.resolves({ - datasources: [ - { - provider: 'postgres', - url: { value: 'postgresql://postgres:prisma@localhost:5436/db%20with%20spaces' } - } - ] - }) + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = "postgresql://postgres:prisma@localhost:5436/db%20with%20spaces" + } + ` helper.runInTransaction(agent, async () => { await client._executeRequest({ clientMethod: 'user.create' }) @@ -86,16 +78,19 @@ test('PrismaClient unit tests', (t) => { }) }) - t.test('should parse connection string from url.fromEnvVar', (t) => { + t.test('should parse connection string from datasource url env var', (t) => { const MockPrismaClient = getMockModule() const prisma = { PrismaClient: MockPrismaClient } initialize(agent, prisma, '@prisma/client', shim) const client = new prisma.PrismaClient() process.env.TEST_URL = 'postgresql://postgres:prisma@host:5437/' - client._engine.getConfig.resolves({ - datasources: [{ provider: 'postgres', url: { fromEnvVar: 'TEST_URL' } }] - }) + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = env("TEST_URL") + } + ` helper.runInTransaction(agent, async () => { await client._executeRequest({ clientMethod: 'user.create' }) @@ -108,20 +103,70 @@ test('PrismaClient unit tests', (t) => { }) }) - t.test('should only call _engine.getConfig once per connection', (t) => { + t.test('should parse connection string client override', (t) => { + const MockPrismaClient = getMockModule() + const prisma = { PrismaClient: MockPrismaClient } + + initialize(agent, prisma, '@prisma/client', shim) + const client = new prisma.PrismaClient() + client._engine.datasourceOverrides = { + db: 'postgresql://postgres:prisma@localhost:5433/override-db' + } + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = "postgresql://postgres:prisma@localhost:5436/db" + } + ` + helper.runInTransaction(agent, async () => { + await client._executeRequest({ clientMethod: 'user.create' }) + t.same(client[symbols.prismaConnection], { + host: 'localhost', + port: '5433', + dbName: 'override-db' + }) + t.end() + }) + }) + + t.test('should not override with client override when datasource name does not match', (t) => { const MockPrismaClient = getMockModule() const prisma = { PrismaClient: MockPrismaClient } initialize(agent, prisma, '@prisma/client', shim) const client = new prisma.PrismaClient() - client._engine.getConfig.resolves({ - datasources: [ - { - provider: 'postgres', - url: { value: 'postgresql://postgres:prisma@localhost:5436/db%20with%20spaces' } - } - ] + client._engine.datasourceOverrides = { + temp_db: 'postgresql://postgres:prisma@localhost:5433/override-db' + } + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = "postgresql://postgres:prisma@localhost:5436/db" + } + ` + helper.runInTransaction(agent, async () => { + await client._executeRequest({ clientMethod: 'user.create' }) + t.same(client[symbols.prismaConnection], { + host: 'localhost', + port: '5436', + dbName: 'db' + }) + t.end() }) + }) + + t.test('should only try to parse the schema once per connection', (t) => { + const MockPrismaClient = getMockModule() + const prisma = { PrismaClient: MockPrismaClient } + + initialize(agent, prisma, '@prisma/client', shim) + const client = new prisma.PrismaClient() + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = "postgresql://postgres:prisma@localhost:5436/db%20with%20spaces" + } + ` helper.runInTransaction(agent, async () => { await client._executeRequest({ clientMethod: 'user.create', action: 'create' }) @@ -129,11 +174,9 @@ test('PrismaClient unit tests', (t) => { args: { query: 'select test from unit-test;' }, action: 'executeRaw' }) - t.equal( - client._engine.getConfig.callCount, - 1, - 'should only call getConfig once per connection' - ) + + t.equal(getSchemaSpy.callCount, 1, 'should only parse schema once') + t.end() }) }) @@ -144,12 +187,12 @@ test('PrismaClient unit tests', (t) => { initialize(agent, prisma, '@prisma/client', shim) const client = new prisma.PrismaClient() - client._engine.getConfig.resolves({ - datasources: [ - { provider: 'postgres', url: { value: 'postgresql://postgres:prisma@my-host:5436/db' } } - ] - }) - + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = "postgresql://postgres:prisma@my-host:5436/db" + } + ` helper.runInTransaction(agent, async (tx) => { await client._executeRequest({ clientMethod: 'user.create', action: 'create' }) await client._executeRequest({ @@ -183,18 +226,12 @@ test('PrismaClient unit tests', (t) => { initialize(agent, prisma, '@prisma/client', shim) const client = new prisma.PrismaClient() - client._engine.getConfig.resolves({ - datasources: [ - { - provider: 'sqlserver', - url: { - value: - 'Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;' - } - } - ] - }) - + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;" + } + ` helper.runInTransaction(agent, async () => { await client._executeRequest({ clientMethod: 'user.create', action: 'create' }) t.same(client[symbols.prismaConnection], {}) @@ -202,63 +239,63 @@ test('PrismaClient unit tests', (t) => { }) }) - t.test('should not set connection params if it fails to retrieve config', (t) => { + t.test('should not crash if it fails to extract query from call', (t) => { const MockPrismaClient = getMockModule() const prisma = { PrismaClient: MockPrismaClient } initialize(agent, prisma, '@prisma/client', shim) const client = new prisma.PrismaClient() - const err = new Error('i failed') - client._engine.getConfig.rejects(err) + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = "postgresql://postgres:prisma@my-host:5436/db" + } + ` - helper.runInTransaction(agent, async () => { - await client._executeRequest({ clientMethod: 'user.create', action: 'create' }) - t.same(client[symbols.prismaConnection], {}) + helper.runInTransaction(agent, async (tx) => { + await client._executeRequest({ action: 'executeRaw' }) + const { children } = tx.trace.root + const [firstSegment] = children + t.equal(firstSegment.name, 'Datastore/statement/Prisma/other/other') t.end() }) }) - t.test('should not crash if it fails to extract query from call', (t) => { + t.test('should not crash if it fails to parse prisma schema', (t) => { const MockPrismaClient = getMockModule() const prisma = { PrismaClient: MockPrismaClient } initialize(agent, prisma, '@prisma/client', shim) const client = new prisma.PrismaClient() - client._engine.getConfig.resolves({ - datasources: [ - { provider: 'postgres', url: { value: 'postgresql://postgres:prisma@my-host:5436/db' } } - ] - }) + client._engine.datamodel = ` + datasource db { + } + ` - helper.runInTransaction(agent, async (tx) => { + helper.runInTransaction(agent, async () => { await client._executeRequest({ action: 'executeRaw' }) - const { children } = tx.trace.root - const [firstSegment] = children - t.equal(firstSegment.name, 'Datastore/statement/Prisma/other/other') + t.same(client[symbols.prismaConnection], {}) t.end() }) }) t.test('should work on 4.11.0', (t) => { const version = '4.11.0' - const MockPrismaClient = getMockModule(version) + const MockPrismaClient = getMockModule() const prisma = { PrismaClient: MockPrismaClient } shim.require.returns({ version }) initialize(agent, prisma, '@prisma/client', shim) const client = new prisma.PrismaClient() - client._engine.library.getConfig.returns({ - datasources: [ - { provider: 'postgres', url: { value: 'postgresql://postgres:prisma@my-host:5436/db' } } - ] - }) + client._engine.datamodel = ` + datasource db { + provider = "postgres" + url = "postgresql://postgres:prisma@my-host:5436/db" + } + ` helper.runInTransaction(agent, async (tx) => { await client._executeRequest({ clientMethod: 'user.create', action: 'create' }) - // need this here to work around the fact inContext is typically sync - // but we use it as async. In normal behavior you will have more async - // work happening to where it will apply the instance configuration to the active segment accordingly but in unit tests this is wonky - await new Promise((resolve) => resolve()) await client._executeRequest({ args: [['select test from unit-test;']], action: 'executeRaw' @@ -279,28 +316,6 @@ test('PrismaClient unit tests', (t) => { }) }) - t.test('should not set connection params in 4.11.0+ if it fails to retrieve config', (t) => { - const version = '4.11.0' - const MockPrismaClient = getMockModule(version) - const prisma = { PrismaClient: MockPrismaClient } - - shim.require.returns({ version }) - initialize(agent, prisma, '@prisma/client', shim) - const client = new prisma.PrismaClient() - const err = new Error('i failed') - client._engine.library.getConfig.throws(err) - - helper.runInTransaction(agent, async () => { - await client._executeRequest({ clientMethod: 'user.create', action: 'create' }) - // need this here to work around the fact inContext is typically sync - // but we use it as async. In normal behavior you will have more async - // work happening to where it will apply the instance configuration to the active segment accordingly but in unit tests this is wonky - await new Promise((resolve) => resolve()) - t.same(client[symbols.prismaConnection], {}) - t.end() - }) - }) - t.test('should not instrument prisma/client on versions less than 4.0.0', (t) => { const MockPrismaClient = getMockModule() const prisma = { PrismaClient: MockPrismaClient } diff --git a/test/versioned/prisma/package.json b/test/versioned/prisma/package.json index 6fd22f799b..f9f10ac5d0 100644 --- a/test/versioned/prisma/package.json +++ b/test/versioned/prisma/package.json @@ -11,7 +11,7 @@ "node": ">=14" }, "dependencies": { - "@prisma/client": ">=4.0.0 <4.14.0", + "@prisma/client": ">=4.0.0", "prisma": "latest" }, "files": [ diff --git a/test/versioned/prisma/prisma.tap.js b/test/versioned/prisma/prisma.tap.js index 11615ea0c6..8f51c086d6 100644 --- a/test/versioned/prisma/prisma.tap.js +++ b/test/versioned/prisma/prisma.tap.js @@ -24,7 +24,7 @@ tap.test('Basic run through prisma functionality', { timeout: 30 * 1000 }, (t) = t.beforeEach(async () => { process.env.DATABASE_URL = getPostgresUrl() agent = helper.instrumentMockedAgent() - PrismaClient = require('@prisma/client').PrismaClient + ;({ PrismaClient } = require('@prisma/client')) prisma = new PrismaClient() }) diff --git a/third_party_manifest.json b/third_party_manifest.json index 0dd9d2ecf6..4e6a7a95f2 100644 --- a/third_party_manifest.json +++ b/third_party_manifest.json @@ -1,5 +1,5 @@ { - "lastUpdated": "Tue May 09 2023 11:02:46 GMT-0400 (Eastern Daylight Time)", + "lastUpdated": "Wed May 10 2023 16:48:17 GMT-0400 (Eastern Daylight Time)", "projectName": "New Relic Node Agent", "projectUrl": "https://github.com/newrelic/node-newrelic", "includeOptDeps": true, @@ -56,6 +56,18 @@ "licenseTextSource": "file", "publisher": "Google Inc." }, + "@mrleebo/prisma-ast@0.5.2": { + "name": "@mrleebo/prisma-ast", + "version": "0.5.2", + "range": "^0.5.2", + "licenses": "MIT", + "repoUrl": "https://github.com/MrLeebo/prisma-ast", + "versionedRepoUrl": "https://github.com/MrLeebo/prisma-ast/tree/v0.5.2", + "licenseFile": "node_modules/@mrleebo/prisma-ast/LICENSE", + "licenseUrl": "https://github.com/MrLeebo/prisma-ast/blob/v0.5.2/LICENSE", + "licenseTextSource": "file", + "publisher": "Jeremy Liberman" + }, "@newrelic/aws-sdk@5.0.2": { "name": "@newrelic/aws-sdk", "version": "5.0.2",