newrelic 9.10.2 → 9.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NEWS.md CHANGED
@@ -1,6 +1,21 @@
1
+ ### v9.11.0 (2023-03-08)
2
+ * Added instrumentation for Prisma(`@prisma/client`).
3
+ * Miniumum supported version of `@prisma/client` is 4.0.0.
4
+ * Captures spans for queries.
5
+ * It names them based on the model and action.(i.e. Datastore/statement/Prisma/user/create)
6
+ * For statements and queries using the `$queryRaw`, `$executeRaw`, `$queryRawUnsafe`, and `$executeRawUnsafe` the names will be aligned with the raw SQL.(i.e. Datastore/statement/Prisma/User/select)
7
+ * Captures database metrics for all statements and queries.
8
+ * Captures SQL Traces.
9
+ * Provides connection between application and database server via service maps.
10
+
11
+ Huge shoutout to @osmanmrtacar for the original contribution 🙏🏻
12
+
13
+ * Updated `@grpc/protoloader` from 0.7.4 to 0.7.5.
14
+ * Updated `@grpc/grpc-js` from 1.8.7 to 1.8.8.
15
+
1
16
  ### v9.10.2 (2023-02-21)
2
17
 
3
- * fix: Replaced `request.aborted` with `response.close` in HTTP instrumentation.
18
+ * Replaced `request.aborted` with `response.close` in HTTP instrumentation.
4
19
  * Fixed issue where setting `NEW_RELIC_GRPC_IGNORE_STATUS_CODES` was not properly parsing the codes as integers, thus not ignoring reporting errors of certain status codes.
5
20
  * Upgraded `@grpc/grpc-js` from 1.8.4 to 1.8.7.
6
21
 
@@ -0,0 +1,208 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const { prismaConnection, prismaModelCall } = require('../../symbols')
9
+ const { URL } = require('url')
10
+ const logger = require('../../logger').child({ component: 'prisma' })
11
+ const parseSql = require('../../db/query-parsers/sql')
12
+ // Note: runCommandRaw is another raw command but it is mongo which we cannot parse as sql
13
+ const RAW_COMMANDS = ['executeRaw', 'queryRaw']
14
+
15
+ const semver = require('semver')
16
+
17
+ /**
18
+ * Extracts the connection url from env var or the .value prop
19
+ * Very similar to this helper: https://github.com/prisma/prisma/blob/main/packages/internals/src/utils/parseEnvValue.ts
20
+ *
21
+ * @param {string} datasource object from prisma config { url, fromEnvVar }
22
+ * @returns {string} connection string
23
+ */
24
+ function extractConnectionString(datasource = {}) {
25
+ return process.env[datasource.fromEnvVar] || datasource.value
26
+ }
27
+ /**
28
+ * Parses a connection string. Most database engines in prisma are SQL and all
29
+ * have similar engine strings.
30
+ *
31
+ * **Note**: This will not parse ms sql server, instead will log a warning
32
+ *
33
+ * @param {string} provider prisma provider(i.e. mysql, postgres, mongodb)
34
+ * @param {string} datasource object from prisma config { url, fromEnvVar }
35
+ * @returns {object} { host, port, dbName }
36
+ */
37
+ function parseConnectionString(provider, datasource) {
38
+ const connectionUrl = extractConnectionString(datasource)
39
+
40
+ let parameters = {}
41
+ try {
42
+ const parsedUrl = new URL(connectionUrl)
43
+ parameters = {
44
+ host: parsedUrl.hostname,
45
+ port: parsedUrl.port,
46
+ dbName: parsedUrl.pathname && decodeURIComponent(parsedUrl.pathname.split('/')[1])
47
+ }
48
+ } catch (err) {
49
+ logger.warn('Failed to parse connection string for %s: %s', provider, err.message)
50
+ }
51
+ return parameters
52
+ }
53
+
54
+ /**
55
+ * Extracts the raw query string from the appropriate location within the function args.
56
+ * In 4.11.0 prisma refactored code and args are an array on .args, whereas before they
57
+ * were an object on .args.
58
+ *
59
+ * @param {Array} args args passed to the prisma function
60
+ * @param {string} pkgVersion prisma version
61
+ * @returns {string} raw query string
62
+ */
63
+ function extractQueryArgs(args, pkgVersion) {
64
+ let query = ''
65
+ try {
66
+ if (semver.gte(pkgVersion, '4.11.0')) {
67
+ query = args[0].args[0]
68
+ if (Array.isArray(query)) {
69
+ // RawUnsafe pass in a string, but plain Raw methods pass in an
70
+ // array containing a prepared SQL statement and the SQL parameters
71
+ query = query[0]
72
+ }
73
+ } else {
74
+ query = args[0].args.query
75
+ }
76
+ } catch (err) {
77
+ logger.error('Failed to extract query from raw query: %s', err.message)
78
+ }
79
+
80
+ return query
81
+ }
82
+
83
+ /**
84
+ * Extracts either the raw query or the client method.
85
+ *
86
+ * @param {Array} args arguments to a prisma operation
87
+ * @param {string} pkgVersion prisma version
88
+ * @returns {string} query raw query string or model call <collection>.<operation>
89
+ */
90
+ function retrieveQuery(args, pkgVersion) {
91
+ if (Array.isArray(args)) {
92
+ const action = args[0].action
93
+ if (RAW_COMMANDS.includes(action)) {
94
+ return extractQueryArgs(args, pkgVersion)
95
+ }
96
+
97
+ // cast to string obj to attach symbol
98
+ // this is done to tell query parser that we need to split string
99
+ // to extract contents
100
+ const clientMethod = new String(args[0].clientMethod)
101
+ clientMethod[prismaModelCall] = true
102
+ return clientMethod
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Parses formatted string to extract the collection and operation.
108
+ * In case of executeRaw the string is created above in `retrieveQuery`
109
+ *
110
+ * @param {string} query raw query string or model call <collection>.<operation>
111
+ * @returns {object} { collection, operation, query }
112
+ */
113
+ function queryParser(query) {
114
+ if (query[prismaModelCall]) {
115
+ const [collection, operation] = query.split('.')
116
+ return {
117
+ collection,
118
+ operation,
119
+ // this is a String object, need to parse to string literal
120
+ query: query.toString()
121
+ }
122
+ }
123
+ return parseSql(query)
124
+ }
125
+
126
+ /**
127
+ * Extracts the prisma connection information from the engine. In pre 4.11.0 this existed
128
+ * on a different object and was also a promise.
129
+ *
130
+ * @param {object} client prisma client instance
131
+ * @param {string} pkgVersion prisma version
132
+ * @returns {Promise} returns prisma connection configuration
133
+ */
134
+ function extractPrismaConfig(client, pkgVersion) {
135
+ if (semver.gte(pkgVersion, '4.11.0')) {
136
+ // wait for the library promise to resolve before getting the config
137
+ return client._engine.libraryInstantiationPromise.then(() => {
138
+ return client._engine.library.getConfig({
139
+ datamodel: client._engine.datamodel,
140
+ ignoreEnvVarErrors: true
141
+ })
142
+ })
143
+ }
144
+ return client._engine.getConfig()
145
+ }
146
+
147
+ /**
148
+ * Instruments the `@prisma/client` module, function that is
149
+ * passed to `onRequire` when instantiating instrumentation.
150
+ *
151
+ * @param {object} _agent New Relic agent
152
+ * @param {object} prisma resolved module
153
+ * @param {string} _moduleName string representation of require/import path
154
+ * @param {object} shim New Relic shim
155
+ */
156
+ module.exports = async function initialize(_agent, prisma, _moduleName, shim) {
157
+ const pkgVersion = shim.require('./package.json').version
158
+ if (semver.lt(pkgVersion, '4.0.0')) {
159
+ logger.warn(
160
+ 'Skipping instrumentation of @prisma/client. Minimum supported version of library is 4.0.0, actual version %s',
161
+ pkgVersion
162
+ )
163
+ return
164
+ }
165
+
166
+ shim.setDatastore(shim.PRISMA)
167
+ shim.setParser(queryParser)
168
+
169
+ shim.recordQuery(
170
+ prisma.PrismaClient.prototype,
171
+ '_executeRequest',
172
+ function wrapExecuteRequest(shim, _executeRequest, _fnName, args) {
173
+ const client = this
174
+
175
+ return {
176
+ promise: true,
177
+ query: retrieveQuery(args, pkgVersion),
178
+ /**
179
+ * Adds the relevant host, port, database_name parameters
180
+ * to the active segment
181
+ */
182
+ inContext: async function inContext() {
183
+ if (!client[prismaConnection]) {
184
+ try {
185
+ const prismaConfig = await extractPrismaConfig(client, pkgVersion)
186
+ const activeDatasource = prismaConfig?.datasources[0]
187
+ const dbParams = parseConnectionString(
188
+ activeDatasource?.provider,
189
+ activeDatasource?.url
190
+ )
191
+ shim.captureInstanceAttributes(dbParams.host, dbParams.port, dbParams.dbName)
192
+ client[prismaConnection] = dbParams
193
+ } catch (err) {
194
+ logger.error('Failed to retrieve prisma config in %s: %s', pkgVersion, err.message)
195
+ client[prismaConnection] = {}
196
+ }
197
+ } else {
198
+ shim.captureInstanceAttributes(
199
+ client[prismaConnection].host,
200
+ client[prismaConnection].port,
201
+ client[prismaConnection].dbName
202
+ )
203
+ }
204
+ }
205
+ }
206
+ }
207
+ )
208
+ }
@@ -51,7 +51,7 @@ module.exports = function instrumentations() {
51
51
  'loglevel': { type: MODULE_TYPE.TRACKING },
52
52
  'npmlog': { type: MODULE_TYPE.TRACKING },
53
53
  'fancy-log': { type: MODULE_TYPE.TRACKING },
54
- '@prisma/client': { type: MODULE_TYPE.TRACKING },
54
+ '@prisma/client': { type: MODULE_TYPE.DATASTORE },
55
55
  '@nestjs/core': { type: MODULE_TYPE.TRACKING },
56
56
  'knex': { type: MODULE_TYPE.TRACKING }
57
57
  }
@@ -12,6 +12,7 @@ const NODEJS = {
12
12
  const ALL = 'all'
13
13
  const POSTGRES_LITERAL = 'Postgres'
14
14
  const CASSANDRA_LITERAL = 'Cassandra'
15
+ const PRISMA_LITERAL = 'Prisma'
15
16
  const EXPRESS_LITERAL = 'Expressjs'
16
17
  const OTHER_TRANSACTION_MESSAGE = 'OtherTransaction/Message'
17
18
 
@@ -150,6 +151,13 @@ const CASSANDRA = {
150
151
  ALL: DB.PREFIX + `${CASSANDRA_LITERAL}/` + ALL
151
152
  }
152
153
 
154
+ const PRISMA = {
155
+ PREFIX: PRISMA_LITERAL,
156
+ STATEMENT: `${DB.STATEMENT}/${PRISMA_LITERAL}/`,
157
+ OPERATION: `${DB.OPERATION}/${PRISMA_LITERAL}/`,
158
+ INSTANCE: `${DB.INSTANCE}/${PRISMA_LITERAL}/`
159
+ }
160
+
153
161
  const EXPRESS = {
154
162
  PREFIX: `${EXPRESS_LITERAL}/`,
155
163
  MIDDLEWARE: MIDDLEWARE.PREFIX + `${EXPRESS_LITERAL}/`,
@@ -345,5 +353,6 @@ module.exports = {
345
353
  URI: 'Uri',
346
354
  UTILIZATION: UTILIZATION,
347
355
  VIEW: VIEW,
348
- WEB: WEB
356
+ WEB: WEB,
357
+ PRISMA: PRISMA
349
358
  }
@@ -5,8 +5,6 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- /* eslint sonarjs/cognitive-complexity: ["error", 24] -- TODO: https://issues.newrelic.com/browse/NEWRELIC-5252 */
9
-
10
8
  const EventEmitter = require('events').EventEmitter
11
9
  const util = require('util')
12
10
  const logger = require('../logger').child({ component: 'metric_normalizer' })
@@ -112,45 +110,53 @@ MetricNormalizer.prototype.load = function load(json) {
112
110
  }
113
111
  }
114
112
 
113
+ /**
114
+ * Helper for loadFromConfig
115
+ *
116
+ * @param rule {object} from this.config.rules
117
+ * @param ctx {object} context from the calling function
118
+ */
119
+ function processNameRule(rule, ctx) {
120
+ if (!rule.pattern) {
121
+ return logger.error({ rule: rule }, 'Simple naming rules require a pattern.')
122
+ }
123
+ if (!rule.name) {
124
+ return logger.error({ rule: rule }, 'Simple naming rules require a replacement name.')
125
+ }
126
+
127
+ const precedence = rule.precedence
128
+ const terminal = rule.terminate_chain
129
+ const json = {
130
+ match_expression: rule.pattern,
131
+ eval_order: typeof precedence === 'number' ? precedence : 500,
132
+ terminate_chain: typeof terminal === 'boolean' ? terminal : true,
133
+ replace_all: rule.replace_all,
134
+ replacement: rule.name,
135
+ ignore: false
136
+ }
137
+
138
+ // Find where the rule should be inserted and do so.
139
+ const reverse = ctx.config.feature_flag.reverse_naming_rules
140
+ const insert = ctx.rules.findIndex(function findRule(r) {
141
+ return reverse ? r.precedence >= json.eval_order : r.precedence > json.eval_order
142
+ })
143
+ if (insert === -1) {
144
+ ctx.rules.push(new NormalizerRule(json))
145
+ } else {
146
+ ctx.rules.splice(insert, 0, new NormalizerRule(json))
147
+ }
148
+ }
115
149
  /**
116
150
  * Load any rules found in the configuration into a metric normalizer.
117
151
  *
118
152
  * Operates via side effects.
119
153
  */
120
154
  MetricNormalizer.prototype.loadFromConfig = function loadFromConfig() {
155
+ const ctx = this
121
156
  const rules = this.config.rules
122
157
 
123
158
  if (rules && rules.name && rules.name.length > 0) {
124
- rules.name.forEach((rule) => {
125
- if (!rule.pattern) {
126
- return logger.error({ rule: rule }, 'Simple naming rules require a pattern.')
127
- }
128
- if (!rule.name) {
129
- return logger.error({ rule: rule }, 'Simple naming rules require a replacement name.')
130
- }
131
-
132
- const precedence = rule.precedence
133
- const terminal = rule.terminate_chain
134
- const json = {
135
- match_expression: rule.pattern,
136
- eval_order: typeof precedence === 'number' ? precedence : 500,
137
- terminate_chain: typeof terminal === 'boolean' ? terminal : true,
138
- replace_all: rule.replace_all,
139
- replacement: rule.name,
140
- ignore: false
141
- }
142
-
143
- // Find where the rule should be inserted and do so.
144
- const reverse = this.config.feature_flag.reverse_naming_rules
145
- const insert = this.rules.findIndex(function findRule(r) {
146
- return reverse ? r.precedence >= json.eval_order : r.precedence > json.eval_order
147
- })
148
- if (insert === -1) {
149
- this.rules.push(new NormalizerRule(json))
150
- } else {
151
- this.rules.splice(insert, 0, new NormalizerRule(json))
152
- }
153
- })
159
+ rules.name.forEach((name) => processNameRule(name, ctx))
154
160
  }
155
161
 
156
162
  if (rules && rules.ignore && rules.ignore.length > 0) {
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- /* eslint sonarjs/cognitive-complexity: ["error", 52] -- TODO: https://issues.newrelic.com/browse/NEWRELIC-5252 */
8
+ /* eslint sonarjs/cognitive-complexity: ["error", 57] -- TODO: https://issues.newrelic.com/browse/NEWRELIC-5252 */
9
9
 
10
10
  const dbutil = require('../db/utils')
11
11
  const hasOwnProperty = require('../util/properties').hasOwn
@@ -37,7 +37,8 @@ const DATASTORE_NAMES = {
37
37
  MYSQL: 'MySQL',
38
38
  NEPTUNE: 'Neptune',
39
39
  POSTGRES: 'Postgres',
40
- REDIS: 'Redis'
40
+ REDIS: 'Redis',
41
+ PRISMA: 'Prisma'
41
42
  }
42
43
 
43
44
  /**
@@ -182,14 +183,14 @@ DatastoreShim.prototype.getDatabaseNameFromUseQuery = getDatabaseNameFromUseQuer
182
183
  * used instead.
183
184
  * @property {DatastoreParameters} [parameters]
184
185
  * Extra parameters to be set on the metric for the operation.
185
- * @property {bool} [record=true]
186
+ * @property {boolean} [record=true]
186
187
  * Indicates if the operation should be recorded as a metric. A segment will be
187
188
  * created even if this is `false`.
188
189
  * @property {number|CallbackBindFunction} [callback]
189
190
  * If a number, it is the offset in the arguments array for the operation's
190
191
  * callback argument. If it is a function, it should perform the segment
191
192
  * binding to the callback.
192
- * @property {bool} [promise=false]
193
+ * @property {boolean} [promise=false]
193
194
  * If `true`, the return value will be wrapped as a Promise.
194
195
  * @see DatastoreShim#recordOperation
195
196
  * @see QuerySpec
@@ -202,7 +203,7 @@ DatastoreShim.prototype.getDatabaseNameFromUseQuery = getDatabaseNameFromUseQuer
202
203
  * @description
203
204
  * Describes the interface for a query function. Extends {@link OperationSpec}
204
205
  * with query-specific parameters.
205
- * @property {bool} [stream=false]
206
+ * @property {boolean} [stream=false]
206
207
  * If `true`, the return value will be wrapped as a stream.
207
208
  * @property {number|string|QueryFunction} query
208
209
  * If a number, it is the offset in the arguments array for the query string
@@ -693,6 +694,7 @@ function _recordQuery(suffix, nodule, properties, querySpec) {
693
694
  promise: 'promise' in queryDesc ? queryDesc.promise : null,
694
695
  internal: 'internal' in queryDesc ? queryDesc.internal : true,
695
696
  opaque: 'opaque' in queryDesc ? queryDesc.opaque : false,
697
+ inContext: 'inContext' in queryDesc ? queryDesc.inContext : null,
696
698
  recorder: function queryRecorder(segment, scope) {
697
699
  if (segment) {
698
700
  parsed.recordMetrics(segment, scope)
package/lib/shim/shim.js CHANGED
@@ -1443,7 +1443,7 @@ function isFunction(obj) {
1443
1443
  * @returns {bool} True if the object is a string, else false.
1444
1444
  */
1445
1445
  function isString(obj) {
1446
- return typeof obj === 'string'
1446
+ return typeof obj === 'string' || obj instanceof String
1447
1447
  }
1448
1448
 
1449
1449
  /**
package/lib/symbols.js CHANGED
@@ -18,6 +18,8 @@ module.exports = {
18
18
  onceExecuted: Symbol('onceExecuted'),
19
19
  offTheRecord: Symbol('offTheRecord'),
20
20
  original: Symbol('original'),
21
+ prismaConnection: Symbol('prismaConnection'),
22
+ prismaModelCall: Symbol('modelCall'),
21
23
  segment: Symbol('segment'),
22
24
  shim: Symbol('shim'),
23
25
  storeDatabase: Symbol('storeDatabase'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "9.10.2",
3
+ "version": "9.11.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -179,8 +179,8 @@
179
179
  "newrelic-naming-rules": "./bin/test-naming-rules.js"
180
180
  },
181
181
  "dependencies": {
182
- "@grpc/grpc-js": "^1.8.7",
183
- "@grpc/proto-loader": "^0.7.4",
182
+ "@grpc/grpc-js": "^1.8.8",
183
+ "@grpc/proto-loader": "^0.7.5",
184
184
  "@newrelic/aws-sdk": "^5.0.2",
185
185
  "@newrelic/koa": "^7.1.1",
186
186
  "@newrelic/superagent": "^6.0.0",