newrelic 8.16.0 → 8.17.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,3 +1,7 @@
1
+ ### v8.17.0 (2022-07-27)
2
+
3
+ * Added instrumentation for `grpc-js` server unary, client-streaming, server-streaming and bidirectional streaming handlers.
4
+
1
5
  ### v8.16.0 (2022-07-21)
2
6
 
3
7
  * Automatic application log forwarding is now enabled by default. This version of the agent will automatically send enriched application logs to New Relic. To learn more about about this feature, see the [APM logs in context documentation](https://docs.newrelic.com/docs/apm/new-relic-apm/getting-started/get-started-logs-context/). For additional configuration options, see the [Node.js logs in context documentation](https://docs.newrelic.com/docs/logs/logs-context/configure-logs-context-nodejs). To learn about how to toggle log ingestion on or off by account, see our documentation to [disable automatic logging](https://docs.newrelic.com/docs/logs/logs-context/disable-automatic-logging) via the UI or API.
@@ -770,6 +770,18 @@ exports.config = () => ({
770
770
  database_name_reporting: { enabled: true }
771
771
  },
772
772
 
773
+ /**
774
+ * Controls behavior of gRPC server instrumentation.
775
+ */
776
+ grpc: {
777
+ /**
778
+ * Enables recording of non-zero gRPC status codes. Default is `true`.
779
+ *
780
+ * @env NEW_RELIC_GRPC_RECORD_ERRORS
781
+ */
782
+ record_errors: true
783
+ },
784
+
773
785
  /**
774
786
  * Controls the behavior of span events produced by the agent.
775
787
  */
package/lib/config/env.js CHANGED
@@ -123,6 +123,9 @@ const ENV_MAPPING = {
123
123
  enabled: 'NEW_RELIC_DATASTORE_DATABASE_NAME_REPORTING_ENABLED'
124
124
  }
125
125
  },
126
+ grpc: {
127
+ record_errors: 'NEW_RELIC_GRPC_RECORD_ERRORS'
128
+ },
126
129
  span_events: {
127
130
  enabled: 'NEW_RELIC_SPAN_EVENTS_ENABLED',
128
131
  attributes: {
@@ -240,6 +243,7 @@ const BOOLEAN_VARS = new Set([
240
243
  'NEW_RELIC_BROWSER_MONITOR_DEBUG',
241
244
  'NEW_RELIC_HIGH_SECURITY',
242
245
  'NEW_RELIC_SLOW_SQL_ENABLED',
246
+ 'NEW_RELIC_GRPC_RECORD_ERRORS',
243
247
  'NEW_RELIC_SPAN_EVENTS_ENABLED',
244
248
  'NEW_RELIC_SPAN_EVENTS_ATTRIBUTES_ENABLED',
245
249
  'NEW_RELIC_DISTRIBUTED_TRACING_ENABLED',
@@ -6,80 +6,215 @@
6
6
  'use strict'
7
7
 
8
8
  const recordExternal = require('../../metrics/recorders/http_external')
9
+ const recordHttp = require('../../metrics/recorders/http')
10
+ const { DESTINATIONS } = require('../../config/attribute-filter')
11
+ const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT
12
+ const semver = require('semver')
9
13
 
10
14
  module.exports = function instrument(shim) {
11
- const callStream = shim.require('./build/src/call-stream')
12
- shim.wrap(callStream.Http2CallStream.prototype, 'start', (shim, original) => {
13
- return function wrappedStart() {
14
- const activeSegment = shim.getActiveSegment()
15
- if (!activeSegment) {
16
- return original.apply(this, arguments)
17
- }
15
+ const genericShim = shim.makeSpecializedShim(shim.GENERIC, 'call-stream')
16
+ const callStream = genericShim.require('./build/src/call-stream')
17
+ genericShim.wrap(callStream.Http2CallStream.prototype, 'start', wrapStart)
18
+
19
+ const webFrameworkShim = shim.makeSpecializedShim(shim.WEB_FRAMEWORK, 'server')
20
+ const server = webFrameworkShim.require('./build/src/server')
21
+ webFrameworkShim.setFramework('gRPC')
22
+ webFrameworkShim.wrap(server.Server.prototype, 'register', wrapRegister)
23
+ }
24
+
25
+ /**
26
+ * Instruments grpc-js client by intercepting the function that
27
+ * initiates client requests. This handles all four types of client
28
+ * invocations: unary, client-streaming, server-streaming, and
29
+ * bidirectional streaming.
30
+ *
31
+ * @param {object} shim the generic shim to instrument with
32
+ * @param {Function} original the original function
33
+ * @returns {Function} the instrumented function
34
+ */
35
+ function wrapStart(shim, original) {
36
+ return function wrappedStart() {
37
+ const activeSegment = shim.getActiveSegment()
38
+ if (!activeSegment) {
39
+ return original.apply(this, arguments)
40
+ }
18
41
 
19
- const channel = this.channel
20
- const authorityName = (channel.target && channel.target.path) || channel.getDefaultAuthority
42
+ const channel = this.channel
43
+ const authorityName = (channel.target && channel.target.path) || channel.getDefaultAuthority
21
44
 
22
- const segment = shim.createSegment({
23
- name: `External/${authorityName}${this.methodName}`,
24
- opaque: true,
25
- recorder: recordExternal(authorityName, 'gRPC')
26
- })
45
+ const segment = shim.createSegment({
46
+ name: `External/${authorityName}${this.methodName}`,
47
+ opaque: true,
48
+ recorder: recordExternal(authorityName, 'gRPC')
49
+ })
27
50
 
28
- return shim.applySegment(callStart, segment, true, this, arguments)
51
+ return shim.applySegment(callStart, segment, true, this, arguments)
52
+
53
+ function callStart() {
54
+ const args = shim.argsToArray.apply(shim, arguments)
55
+
56
+ const transaction = segment.transaction
57
+
58
+ const originalMetadata = args[0]
59
+ const nrMetadata = originalMetadata.clone()
60
+
61
+ const outboundAgentHeaders = Object.create(null)
62
+ if (shim.agent.config.distributed_tracing.enabled) {
63
+ transaction.insertDistributedTraceHeaders(outboundAgentHeaders)
64
+ Object.keys(outboundAgentHeaders).forEach((key) => {
65
+ nrMetadata.add(key, outboundAgentHeaders[key])
66
+ })
67
+ } else {
68
+ shim.logger.debug('Distributed tracing disabled by instrumentation.')
69
+ }
29
70
 
30
- function callStart() {
31
- const args = shim.argsToArray.apply(shim, arguments)
71
+ args[0] = nrMetadata
32
72
 
33
- const transaction = segment.transaction
73
+ const originalListener = args[1]
74
+ const nrListener = Object.assign({}, originalListener)
75
+ nrListener.onReceiveStatus = (status) => {
76
+ const { code, details } = status
34
77
 
35
- const originalMetadata = args[0]
36
- const nrMetadata = originalMetadata.clone()
78
+ segment.addAttribute('grpc.statusCode', code)
79
+ segment.addAttribute('grpc.statusText', details)
37
80
 
38
- const outboundAgentHeaders = Object.create(null)
39
- if (shim.agent.config.distributed_tracing.enabled) {
40
- transaction.insertDistributedTraceHeaders(outboundAgentHeaders)
41
- Object.keys(outboundAgentHeaders).forEach((key) => {
42
- nrMetadata.add(key, outboundAgentHeaders[key])
43
- })
44
- } else {
45
- shim.logger.debug('Distributed tracing disabled by instrumentation.')
81
+ // Java captures client errors based on status code
82
+ // but has specific gRPC configuration to turn off
83
+ if (code !== 0) {
84
+ // this is currently just creating an error from the details string
85
+ shim.agent.errors.add(segment.transaction, details)
46
86
  }
47
87
 
48
- args[0] = nrMetadata
88
+ segment.addAttribute('component', 'gRPC')
49
89
 
50
- const originalListener = args[1]
51
- const nrListener = Object.assign({}, originalListener)
52
- nrListener.onReceiveStatus = (status) => {
53
- const { code, details } = status
90
+ const protocol = 'grpc'
54
91
 
55
- segment.addAttribute('grpc.statusCode', code)
56
- segment.addAttribute('grpc.statusText', details)
92
+ const url = `${protocol}://${authorityName}${this.methodName}`
57
93
 
58
- // Java captures client errors based on status code
59
- // but has specific gRPC configuration to turn off
60
- if (code !== 0) {
61
- // this is currently just creating an error from the details string
62
- shim.agent.errors.add(segment.transaction, details)
63
- }
94
+ segment.addAttribute('http.url', url)
95
+ segment.addAttribute('http.method', this.methodName)
64
96
 
65
- segment.addAttribute('component', 'gRPC')
97
+ segment.end()
66
98
 
67
- const protocol = 'grpc'
99
+ originalListener && originalListener.onReceiveStatus(status)
100
+ }
68
101
 
69
- const url = `${protocol}://${authorityName}${this.methodName}`
102
+ args[1] = nrListener
70
103
 
71
- segment.addAttribute('http.url', url)
72
- segment.addAttribute('http.method', this.methodName)
104
+ return original.apply(this, args)
105
+ }
106
+ }
107
+ }
73
108
 
74
- segment.end()
109
+ /**
110
+ * Instruments the grpc-js server by intercepting the moment when
111
+ * server methods are registered from the method implementations
112
+ * provided to grpc-js. This handles all four types of server
113
+ * invocations: unary, client-streaming, server-streaming, and
114
+ * bidirectional streaming.
115
+ *
116
+ * @param {object} shim the web shim to instrument with
117
+ * @param {Function} original the original function
118
+ * @returns {Function} the instrumented function
119
+ */
120
+ function wrapRegister(shim, original) {
121
+ const constants = shim.require('./build/src/constants')
75
122
 
76
- originalListener && originalListener.onReceiveStatus(status)
77
- }
123
+ return function wrappedRegister() {
124
+ const args = shim.argsToArray.apply(shim, arguments)
78
125
 
79
- args[1] = nrListener
126
+ const name = args[0]
127
+ const handler = args[1]
128
+ const type = args[args.length - 1]
80
129
 
81
- return original.apply(this, args)
82
- }
130
+ if (this.handlers.has(name)) {
131
+ shim.logger.debug(
132
+ `Not re-instrumenting gRPC method handler for ${name}: it is already registered in the server.`
133
+ )
134
+ return original.apply(this, arguments)
135
+ }
136
+
137
+ args[1] = shim.bindCreateTransaction(instrumentedHandler, { type: shim.WEB })
138
+
139
+ return original.apply(this, args)
140
+
141
+ function instrumentedHandler(stream) {
142
+ const { transaction, segment } = createTransaction()
143
+ acceptDTHeaders(stream, transaction)
144
+ instrumentEventListeners(stream, transaction)
145
+ return shim.applySegment(handler, segment, true, this, arguments)
83
146
  }
84
- })
147
+
148
+ function createTransaction() {
149
+ const parent = shim.getActiveSegment()
150
+ const transaction = parent.transaction
151
+
152
+ // Create the transaction segment using the request URL for now. Once a
153
+ // better name can be determined this segment will be renamed to that.
154
+ const segment = shim.createSegment(name, recordHttp)
155
+ segment.start()
156
+
157
+ transaction.type = 'web'
158
+ transaction.baseSegment = segment
159
+ transaction.url = name
160
+
161
+ transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', transaction.url)
162
+
163
+ transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', transaction.url)
164
+
165
+ shim.setTransactionUri(transaction.url)
166
+
167
+ // This attribute isn't required by the spec, but it seems useful to have
168
+ transaction.trace.attributes.addAttribute(DESTINATION, 'grpc.type', type)
169
+ return { transaction, segment }
170
+ }
171
+
172
+ function acceptDTHeaders(stream, transaction) {
173
+ const metadata = stream.metadata
174
+ Object.entries(metadata.getMap()).forEach(([key, value]) => {
175
+ transaction.trace.attributes.addAttribute(DESTINATION, `request.headers.${key}`, value)
176
+ })
177
+
178
+ const headers = Object.create(null)
179
+ headers.tracestate = metadata.get('tracestate').join(',')
180
+ headers.traceparent = metadata.get('traceparent').join(',')
181
+ headers.newrelic = metadata.get('newrelic').join(',')
182
+ transaction.acceptDistributedTraceHeaders('HTTP', headers)
183
+ }
184
+
185
+ function instrumentEventListeners(stream, transaction) {
186
+ const agent = shim.agent
187
+ stream.call.once('callEnd', (statusCode) => {
188
+ transaction.trace.attributes.addAttribute(DESTINATION, 'response.status', statusCode)
189
+ if (statusCode !== 0 && agent.config.grpc.record_errors) {
190
+ const status = constants.Status[statusCode]
191
+ const error = new Error(`gRPC status code ${statusCode}: ${status}`)
192
+ agent.errors.add(transaction, error)
193
+ }
194
+ })
195
+ stream.call.once('streamEnd', () => {
196
+ transaction.end()
197
+ })
198
+ // TODO should also instrument the 'data' event on the stream
199
+ // object, as that can ensue in lots of processing when the
200
+ // client is streaming. https://issues.newrelic.com/browse/NEWRELIC-1460
201
+ }
202
+ }
203
+ }
204
+
205
+ module.exports = function instrument(shim) {
206
+ const genericShim = shim.makeSpecializedShim(shim.GENERIC, 'call-stream')
207
+ const callStream = genericShim.require('./build/src/call-stream')
208
+ genericShim.wrap(callStream.Http2CallStream.prototype, 'start', wrapStart)
209
+
210
+ const webFrameworkShim = shim.makeSpecializedShim(shim.WEB_FRAMEWORK, 'server')
211
+ const grpcVersion = shim.require('./package.json').version
212
+ if (semver.lt(grpcVersion, '1.4.0')) {
213
+ shim.logger.debug('gRPC server-side instrumentation only supported on grpc-js >=1.4.0')
214
+ return
215
+ }
216
+
217
+ const server = webFrameworkShim.require('./build/src/server')
218
+ webFrameworkShim.setFramework('gRPC')
219
+ webFrameworkShim.wrap(server.Server.prototype, 'register', wrapRegister)
85
220
  }
@@ -12,7 +12,7 @@ const grpc = require('./grpc')
12
12
  */
13
13
  module.exports = [
14
14
  {
15
- type: 'generic',
15
+ type: 'conglomerate', // generic shim for client, web framework shim for server
16
16
  moduleName: '@grpc/grpc-js',
17
17
  onResolved: grpc
18
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "8.16.0",
3
+ "version": "8.17.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [