newrelic 13.2.1 → 13.3.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.
Files changed (56) hide show
  1. package/NEWS.md +44 -10
  2. package/api.js +1 -1
  3. package/lib/instrumentation/@azure/functions.js +8 -46
  4. package/lib/instrumentation/core/http-outbound.js +26 -10
  5. package/lib/instrumentation/core/http.js +20 -118
  6. package/lib/instrumentations.js +0 -14
  7. package/lib/otel/logs/index.js +80 -0
  8. package/lib/otel/metrics/index.js +119 -0
  9. package/lib/otel/setup-signal.js +37 -0
  10. package/lib/otel/setup.js +15 -25
  11. package/lib/otel/{attr-reconciler.js → traces/attr-reconciler.js} +2 -2
  12. package/lib/otel/{exception-mapping.js → traces/exception-mapping.js} +1 -1
  13. package/lib/otel/traces/index.js +40 -0
  14. package/lib/otel/{rules.js → traces/rules.js} +1 -1
  15. package/lib/otel/{segment-synthesis.js → traces/segment-synthesis.js} +5 -4
  16. package/lib/otel/{segments → traces/segments}/consumer.js +4 -4
  17. package/lib/otel/{segments → traces/segments}/database.js +6 -6
  18. package/lib/otel/{segments → traces/segments}/http-external.js +11 -8
  19. package/lib/otel/traces/segments/index.js +22 -0
  20. package/lib/otel/{segments → traces/segments}/internal.js +1 -1
  21. package/lib/otel/{segments → traces/segments}/producer.js +2 -2
  22. package/lib/otel/{segments → traces/segments}/server.js +4 -4
  23. package/lib/otel/{segments → traces/segments}/utils.js +1 -1
  24. package/lib/otel/{span-processor.js → traces/span-processor.js} +13 -19
  25. package/lib/otel/{transformation-rules.json → traces/transformation-rules.json} +3 -2
  26. package/lib/otel/{utils.js → traces/utils.js} +2 -1
  27. package/lib/serverless/aws-lambda.js +7 -68
  28. package/lib/shim/webframework-shim/middleware.js +1 -1
  29. package/lib/shimmer.js +1 -1
  30. package/lib/subscriber-configs.js +2 -1
  31. package/lib/subscribers/base.js +30 -0
  32. package/lib/subscribers/elasticsearch/config.js +1 -0
  33. package/lib/subscribers/mcp-sdk/client-request.js +66 -0
  34. package/lib/subscribers/mcp-sdk/config.js +13 -75
  35. package/lib/subscribers/openai/base.js +21 -0
  36. package/lib/subscribers/openai/chat-responses.js +15 -0
  37. package/lib/subscribers/openai/chat.js +85 -0
  38. package/lib/subscribers/openai/client.js +31 -0
  39. package/lib/subscribers/openai/config.js +85 -0
  40. package/lib/subscribers/openai/embeddings.js +54 -0
  41. package/lib/subscribers/openai/utils.js +349 -0
  42. package/lib/transaction/index.js +157 -56
  43. package/lib/transaction/trace/segment.js +5 -2
  44. package/lib/util/urltils.js +158 -192
  45. package/lib/utilization/common.js +0 -6
  46. package/package.json +1 -1
  47. package/lib/instrumentation/openai.js +0 -482
  48. package/lib/otel/logs/bootstrap-logs.js +0 -84
  49. package/lib/otel/metrics/bootstrap-metrics.js +0 -117
  50. package/lib/otel/segments/index.js +0 -22
  51. package/lib/subscribers/mcp-sdk/client-prompt.js +0 -23
  52. package/lib/subscribers/mcp-sdk/client-resource.js +0 -24
  53. package/lib/subscribers/mcp-sdk/client-tool.js +0 -23
  54. package/lib/subscribers/mcp-sdk/client.js +0 -29
  55. /package/lib/otel/{constants.js → traces/constants.js} +0 -0
  56. /package/lib/otel/{sampler.js → traces/sampler.js} +0 -0
package/NEWS.md CHANGED
@@ -1,13 +1,45 @@
1
- ### v13.2.1 (2025-08-21)
2
-
3
- #### Code refactoring
4
-
5
- * Updated agent to use `@apm-js-collab/tracing-hooks` for the CJS/ESM registration for tracing channel based instrumentation ([#3324](https://github.com/newrelic/node-newrelic/pull/3324)) ([4b2c14f](https://github.com/newrelic/node-newrelic/commit/4b2c14f35d5c658160ad9420ca562f73ae55bbc0))
6
-
7
- #### Documentation
8
-
9
- * Updated compatibility report ([#3330](https://github.com/newrelic/node-newrelic/pull/3330)) ([2831a89](https://github.com/newrelic/node-newrelic/commit/2831a895e0278a05bed8e9f81245c7f9ae515eca))
10
-
1
+ ### v13.3.0 (2025-09-08)
2
+
3
+ #### Features
4
+
5
+ * Added MCP `StreamableHTTPClientTransport` support ([#3337](https://github.com/newrelic/node-newrelic/pull/3337)) ([7189a35](https://github.com/newrelic/node-newrelic/commit/7189a35d0676a01d995420b2c6f27a54f8eb0307))
6
+
7
+ #### Code refactoring
8
+
9
+ * Removed instances of `url.parse`. Streamlined how http instrumentation creates relevant attributes. ([#3340](https://github.com/newrelic/node-newrelic/pull/3340)) ([1fc1e1a](https://github.com/newrelic/node-newrelic/commit/1fc1e1a4086200bd3fd76081913498d7ca344a3a))
10
+ * Updated `openai` instrumentation to subscribe to events emitted ([#3331](https://github.com/newrelic/node-newrelic/pull/3331)) ([04dd677](https://github.com/newrelic/node-newrelic/commit/04dd6774df87489cce41b0bb2b60c56b79e2265d))
11
+
12
+ #### Documentation
13
+
14
+ * Updated compatibility report ([#3344](https://github.com/newrelic/node-newrelic/pull/3344)) ([e422919](https://github.com/newrelic/node-newrelic/commit/e4229195318b2d2e33cc753b7898357daf72a372))
15
+
16
+ #### Miscellaneous chores
17
+
18
+ * Refactored OTEL bridge signals setup ([#3244](https://github.com/newrelic/node-newrelic/pull/3244)) ([ec8edef](https://github.com/newrelic/node-newrelic/commit/ec8edefdc49de276f30a1fc778f09b0d3042c392))
19
+
20
+ #### Tests
21
+
22
+ * Added more cassandra-driver tests ([#3323](https://github.com/newrelic/node-newrelic/pull/3323)) ([d2e9d10](https://github.com/newrelic/node-newrelic/commit/d2e9d103ee74358e88e53ab89c498f261f387ae5))
23
+ * Fixed long running benchmark test and add `shimmer.subscribers` benchmark test ([#3349](https://github.com/newrelic/node-newrelic/pull/3349)) ([15253c0](https://github.com/newrelic/node-newrelic/commit/15253c08f6d8381f516040e339a0aedaf04e365e))
24
+ * Fixed langchain openai streaming tests by explicitly passing `streaming: true` to chat model ([#3346](https://github.com/newrelic/node-newrelic/pull/3346)) ([4f12499](https://github.com/newrelic/node-newrelic/commit/4f12499c2c9248b7f510c76f19cc8d54eb53da2e))
25
+ * Fixed openai test after migration to tracing channel ([#3336](https://github.com/newrelic/node-newrelic/pull/3336)) ([0d01e29](https://github.com/newrelic/node-newrelic/commit/0d01e299cb84c9715f3136ab458534c72eea3145))
26
+ * Hardened `batch-span-streamer.test.js` to avoid random failures in CI ([#3338](https://github.com/newrelic/node-newrelic/pull/3338)) ([2077ce3](https://github.com/newrelic/node-newrelic/commit/2077ce35db319d0128337faed0ff77b00f76d8f1))
27
+
28
+ #### Continuous integration
29
+
30
+ * Set benchmark host default to staging ([#3332](https://github.com/newrelic/node-newrelic/pull/3332)) ([55f6b8d](https://github.com/newrelic/node-newrelic/commit/55f6b8d757afc1632f1be1c0ef1a4ccab23ca3c4))
31
+ * Updated the redis container to use the official `redis` image as `bitnami/redis` is moving and no reason to use a 3rd party image ([#3342](https://github.com/newrelic/node-newrelic/pull/3342)) ([8ffb393](https://github.com/newrelic/node-newrelic/commit/8ffb393ef5e84b188d899580061b2b57171e8166))
32
+
33
+ ### v13.2.1 (2025-08-21)
34
+
35
+ #### Code refactoring
36
+
37
+ * Updated agent to use `@apm-js-collab/tracing-hooks` for the CJS/ESM registration for tracing channel based instrumentation ([#3324](https://github.com/newrelic/node-newrelic/pull/3324)) ([4b2c14f](https://github.com/newrelic/node-newrelic/commit/4b2c14f35d5c658160ad9420ca562f73ae55bbc0))
38
+
39
+ #### Documentation
40
+
41
+ * Updated compatibility report ([#3330](https://github.com/newrelic/node-newrelic/pull/3330)) ([2831a89](https://github.com/newrelic/node-newrelic/commit/2831a895e0278a05bed8e9f81245c7f9ae515eca))
42
+
11
43
  ### v13.2.0 (2025-08-20)
12
44
 
13
45
  #### Features
@@ -7823,3 +7855,5 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
7823
7855
  [mdn-async-function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
7824
7856
 
7825
7857
 
7858
+
7859
+
package/api.js CHANGED
@@ -1133,7 +1133,7 @@ API.prototype.endTransaction = function endTransaction() {
1133
1133
  if (tx) {
1134
1134
  if (tx.baseSegment) {
1135
1135
  if (tx.type === 'web') {
1136
- tx.finalizeNameFromUri(tx.url, 0)
1136
+ tx.finalizeNameFromWeb(0)
1137
1137
  }
1138
1138
  tx.baseSegment.end()
1139
1139
  }
@@ -6,9 +6,6 @@
6
6
  'use strict'
7
7
 
8
8
  const defaultLogger = require('../../logger').child({ component: 'azure-functions' })
9
- const urltils = require('../../util/urltils')
10
- const headerProcessing = require('../../header-processing')
11
- const synthetics = require('../../synthetics')
12
9
  const { Transform } = require('node:stream')
13
10
 
14
11
  const backgroundRecorder = require('../../metrics/recorders/other.js')
@@ -29,12 +26,8 @@ const RESOURCE_GROUP_NAME = WEBSITE_RESOURCE_GROUP ?? WEBSITE_OWNER_NAME?.split(
29
26
  const AZURE_FUNCTION_APP_NAME = WEBSITE_SITE_NAME
30
27
 
31
28
  let coldStart = true
32
- let _agent
33
- let _logger
34
29
 
35
- module.exports = function initialize(agent, azureFunctions, _moduleName, shim, { logger = defaultLogger } = {}) {
36
- _agent = agent
37
- _logger = logger
30
+ module.exports = function initialize(_agent, azureFunctions, _moduleName, shim, { logger = defaultLogger } = {}) {
38
31
  if (!SUBSCRIPTION_ID || !RESOURCE_GROUP_NAME || !AZURE_FUNCTION_APP_NAME) {
39
32
  logger.warn(
40
33
  {
@@ -91,7 +84,6 @@ function wrapAzureHttpMethods(shim, appMethod) {
91
84
  // Set the transaction name according to our spec (category + function name).
92
85
  tx.setPartialName(`AzureFunction/${context.functionName}`)
93
86
 
94
- const url = new URL(request.url)
95
87
  const segment = tracer.createSegment({
96
88
  name: request.url,
97
89
  recorder: recordWeb,
@@ -100,56 +92,26 @@ function wrapAzureHttpMethods(shim, appMethod) {
100
92
  })
101
93
  segment.start()
102
94
 
95
+ const absoluteUrl = request.url
96
+ const url = new URL(absoluteUrl)
103
97
  const transport = url.protocol === 'https:' ? 'HTTPS' : 'HTTP'
104
- tx.type = TYPES.WEB
98
+ const port = url.port || (transport === 'HTTPS' ? 443 : 80)
105
99
  tx.baseSegment = segment
106
- tx.parsedUrl = url
107
- tx.url = urltils.obfuscatePath(_agent.config, url.pathname)
108
- tx.verb = request.method
109
- if (url.port === '') {
110
- tx.port = transport === 'HTTPS' ? '443' : '80'
111
- } else {
112
- tx.port = url.port
113
- }
100
+ tx.initializeWeb({ absoluteUrl, method: request.method, port, headers: request.headers, transport })
114
101
 
115
- tx.trace.attributes.addAttribute(
116
- DESTS.TRANS_EVENT | DESTS.ERROR_EVENT,
117
- 'request.uri',
118
- tx.url
119
- )
120
- segment.addSpanAttribute('request.uri', tx.url)
121
- if (request.method != null) {
122
- segment.addSpanAttribute('request.method', request.method)
123
- }
124
102
  addAttributes({ transaction: tx, functionContext: context })
125
103
 
126
- const queueTimeStamp = headerProcessing.getQueueTime(_logger, request.headers)
127
- if (queueTimeStamp) {
128
- tx.queueTime = Date.now() - queueTimeStamp
129
- }
130
-
131
- synthetics.assignHeadersToTransaction(_agent.config, tx, request.headers)
132
- if (_agent.config.distributed_tracing.enabled === true) {
133
- tx.acceptDistributedTraceHeaders(transport, request.headers)
134
- }
135
-
136
104
  const newContext = ctx.enterSegment({ segment })
137
105
  const boundHandler = tracer.bindFunction(handler, newContext)
138
106
  const result = await boundHandler(...args)
139
107
 
140
- // Responses should have a shape as described at:
141
- // https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response
142
- if (result?.status) {
143
- tx.trace.attributes.addAttribute(
144
- DESTS.TRANS_COMMON,
145
- 'http.statusCode',
146
- result.status
147
- )
148
- }
149
108
  if (coldStart === true) {
150
109
  tx.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'faas.coldStart', true)
151
110
  coldStart = false
152
111
  }
112
+ // Responses should have a shape as described at:
113
+ // https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response
114
+ tx.finalizeWeb({ statusCode: result?.status, headers: result?.headers, end: false })
153
115
  if (result?.body instanceof Transform) {
154
116
  result.body.on('close', () => {
155
117
  tx.end()
@@ -10,7 +10,6 @@ const cat = require('../../util/cat')
10
10
  const urltils = require('../../util/urltils')
11
11
  const logger = require('../../logger').child({ component: 'outbound' })
12
12
  const shimmer = require('../../shimmer')
13
- const url = require('url')
14
13
  const symbols = require('../../symbols')
15
14
  const synthetics = require('../../synthetics')
16
15
  const { URL } = require('node:url')
@@ -245,16 +244,33 @@ function applySegment({ opts, makeRequest, host, port, hostname, segment, config
245
244
  segment.start()
246
245
  const request = makeRequest(opts)
247
246
 
248
- // Attempt to parse into new URL.
249
- // If no href, revert to url.parse.
250
- // Rarely, the href doesn't contain the
251
- // path, so we have to use url.parse to get the path.
252
- const urlObject = (opts?.href?.includes(request.path)) ? new URL(opts.href) : url.parse(request.path, true)
253
- const parsed = urltils.scrubAndParseParameters(urlObject)
254
- const obfuscatedPath = urltils.obfuscatePath(config, parsed.path)
247
+ // Parse the appropriate URL from the request options
248
+ // 1. If the path is a proxy url use the entire path
249
+ // 2. construct url from path, protocol and host if href does not already include path
250
+ // 3. use href to construct url
251
+ let absoluteUrl = null
252
+ if (request?.path?.startsWith('http://') || request?.path?.startsWith('https://')) {
253
+ absoluteUrl = request.path
254
+ } else if (!opts?.href?.includes(request.path)) {
255
+ const protocol = opts.protocol || 'http:'
256
+ absoluteUrl = `${protocol}//${host}${request.path}`
257
+ } else {
258
+ absoluteUrl = opts.href
259
+ }
260
+
261
+ let obfuscatedPath, parsed
262
+ try {
263
+ const urlObject = new URL(absoluteUrl)
264
+ parsed = urltils.scrubAndParseParameters(urlObject)
265
+ obfuscatedPath = urltils.obfuscatePath(config, parsed.path)
266
+ segment.name += obfuscatedPath
267
+ } catch (err) {
268
+ logger.debug('Could not parse URL from request options: %s', err.message)
269
+ obfuscatedPath = request.path
270
+ }
255
271
 
256
272
  const proto = parsed.protocol || opts.protocol || 'http:'
257
- segment.name += obfuscatedPath
273
+
258
274
  segment.captureExternalAttributes({
259
275
  protocol: proto,
260
276
  hostname,
@@ -262,7 +278,7 @@ function applySegment({ opts, makeRequest, host, port, hostname, segment, config
262
278
  method: opts.method,
263
279
  port,
264
280
  path: obfuscatedPath,
265
- queryParams: parsed.parameters
281
+ queryParams: parsed?.parameters
266
282
  })
267
283
  request[symbols.segment] = segment
268
284
  return request
@@ -10,14 +10,11 @@ const logger = require('../../logger').child({ component: 'http' })
10
10
  const recordWeb = require('../../metrics/recorders/http')
11
11
  const cat = require('../../util/cat')
12
12
  const instrumentOutbound = require('./http-outbound')
13
- const url = require('url')
14
13
  const urltils = require('../../util/urltils')
15
- const headerAttributes = require('../../header-attributes')
16
14
  const headerProcessing = require('../../header-processing')
17
15
  const synthetics = require('../../synthetics')
18
16
 
19
17
  const NAMES = require('../../metrics/names')
20
- const DESTS = require('../../config/attribute-filter').DESTINATIONS
21
18
  const symbols = require('../../symbols')
22
19
 
23
20
  // For incoming requests this instrumentation functions by wrapping
@@ -41,14 +38,13 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
41
38
 
42
39
  transaction.nameState.setPrefix(NAMES.NODEJS.PREFIX)
43
40
  transaction.nameState.setDelimiter(NAMES.ACTION_DELIMITER)
41
+ if (request.method != null) {
42
+ transaction.nameState.setVerb(request.method)
43
+ }
44
44
 
45
45
  // Store the transaction information on the request and response.
46
46
  const txInfo = storeTxInfo(transaction, request, response)
47
47
 
48
- if (request) {
49
- initializeRequest(transaction, request)
50
- }
51
-
52
48
  // Create the transaction segment using the request URL for now. Once a
53
49
  // better name can be determined this segment will be renamed to that.
54
50
  const segment = tracer.createSegment({
@@ -58,10 +54,7 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
58
54
  transaction
59
55
  })
60
56
  segment.start()
61
-
62
- if (request.method != null) {
63
- segment.addSpanAttribute('request.method', request.method)
64
- }
57
+ transaction.baseSegment = segment
65
58
 
66
59
  if (txInfo) {
67
60
  // Seed segment stack to enable parenting logic leveraged by
@@ -69,8 +62,16 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
69
62
  txInfo.segmentStack.push(segment)
70
63
  }
71
64
 
72
- transaction.type = 'web'
73
- transaction.baseSegment = segment
65
+ let absoluteUrl = null
66
+ // handle a request that is to a proxy url
67
+ if (request?.url.startsWith('http://') || request?.url.startsWith('https://')) {
68
+ absoluteUrl = request.url
69
+ // attempt to construct the full URL with known attributes
70
+ } else {
71
+ absoluteUrl = `${transport}://${request.headers.host || 'localhost'}${request.url}`
72
+ }
73
+ const port = parsePort(this)
74
+ transaction.initializeWeb({ absoluteUrl, method: request.method, port, headers: request.headers, transport })
74
75
 
75
76
  /* Needed for Connect and Express middleware that monkeypatch request
76
77
  * and response via listeners.
@@ -78,40 +79,8 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) {
78
79
  tracer.bindEmitter(request, segment)
79
80
  tracer.bindEmitter(response, segment)
80
81
 
81
- // the error tracer needs a URL for tracing, even though naming overwrites
82
-
83
- transaction.parsedUrl = url.parse(request.url, true)
84
- transaction.url = urltils.obfuscatePath(agent.config, transaction.parsedUrl.pathname)
85
- transaction.verb = request.method
86
-
87
- // URL is sent as an agent attribute with transaction events
88
- transaction.trace.attributes.addAttribute(
89
- DESTS.TRANS_EVENT | DESTS.ERROR_EVENT,
90
- 'request.uri',
91
- transaction.url
92
- )
93
-
94
- segment.addSpanAttribute('request.uri', transaction.url)
95
-
96
- transaction.port = parsePort(this)
97
- // need to set any config-driven names early for RUM
98
- logger.trace(
99
- { url: request.url, transaction: transaction.id },
100
- 'Applying user naming rules for RUM.'
101
- )
102
- transaction.applyUserNamingRules(request.url)
103
-
104
- const queueTimeStamp = headerProcessing.getQueueTime(logger, request.headers)
105
- if (queueTimeStamp) {
106
- transaction.queueTime = Date.now() - queueTimeStamp
107
- }
108
-
109
- synthetics.assignHeadersToTransaction(agent.config, transaction, request.headers)
110
-
111
- maybeAddDtCatHeaders({ transaction, request, transport, agent })
112
-
113
- response.once('finish', instrumentedFinish.bind(response, segment, transaction))
114
- response.once('close', instrumentedFinish.bind(response, segment, transaction))
82
+ response.once('finish', instrumentedFinish.bind(response, transaction))
83
+ response.once('close', instrumentedFinish.bind(response, transaction))
115
84
 
116
85
  const newContext = context.enterSegment({ segment })
117
86
  return tracer.bindFunction(emit, newContext).apply(this, arguments)
@@ -136,76 +105,19 @@ function parsePort(server) {
136
105
  return serverPort
137
106
  }
138
107
 
139
- function maybeAddDtCatHeaders({ agent, request, transaction, transport }) {
140
- if (agent.config.distributed_tracing.enabled) {
141
- // Node http headers are automatically lowercase
142
- transaction.acceptDistributedTraceHeaders(transport, request.headers)
143
- } else if (agent.config.cross_application_tracer.enabled) {
144
- const { id, transactionId } = cat.extractCatHeaders(request.headers)
145
- const { externalId, externalTransaction } = cat.parseCatData(
146
- id,
147
- transactionId,
148
- agent.config.encoding_key
149
- )
150
- cat.assignCatToTransaction(externalId, externalTransaction, transaction)
151
- }
152
- }
153
-
154
108
  /**
155
109
  * Adds instrumentation to response on finish/close.
156
110
  * It will add `http.statusCode`, `http.statusText`
157
111
  * to the transaction trace and span.
158
112
  * It will also assign the response headers to the transaction
159
113
  *
160
- * @param {TraceSegment} segment active segment
161
114
  * @param {Transaction} transaction active transaction
162
115
  */
163
- function instrumentedFinish(segment, transaction) {
116
+ function instrumentedFinish(transaction) {
164
117
  // Remove listeners so this doesn't get called twice.
165
118
  this.removeListener('finish', instrumentedFinish)
166
119
  this.removeListener('close', instrumentedFinish)
167
-
168
- // Naming must happen before the segment and transaction are ended,
169
- // because metrics recording depends on naming's side effects.
170
-
171
- transaction.finalizeNameFromUri(transaction.parsedUrl, this.statusCode)
172
-
173
- if (this) {
174
- const { statusCode, statusMessage } = this
175
-
176
- if (statusCode != null) {
177
- const responseCode = String(statusCode)
178
-
179
- if (/^\d+$/.test(responseCode)) {
180
- transaction.trace.attributes.addAttribute(
181
- DESTS.TRANS_COMMON,
182
- 'http.statusCode',
183
- responseCode
184
- )
185
-
186
- segment.addSpanAttribute('http.statusCode', responseCode)
187
- }
188
- }
189
-
190
- if (statusMessage !== undefined) {
191
- transaction.trace.attributes.addAttribute(
192
- DESTS.TRANS_COMMON,
193
- 'http.statusText',
194
- statusMessage
195
- )
196
-
197
- segment.addSpanAttribute('http.statusText', statusMessage)
198
- }
199
-
200
- const headers = this.getHeaders()
201
- if (headers) {
202
- headerAttributes.collectResponseHeaders(headers, transaction)
203
- }
204
- }
205
-
206
- // And we are done! End the segment and transaction.
207
- segment.end()
208
- transaction.end()
120
+ transaction.finalizeWeb({ end: true, statusCode: this.statusCode, statusMessage: this.statusMessage, headers: this.getHeaders() })
209
121
  }
210
122
 
211
123
  function storeTxInfo(transaction, request, response) {
@@ -227,15 +139,6 @@ function storeTxInfo(transaction, request, response) {
227
139
  return txInfo
228
140
  }
229
141
 
230
- function initializeRequest(transaction, request) {
231
- headerAttributes.collectRequestHeaders(request.headers, transaction)
232
-
233
- if (request.method != null) {
234
- transaction.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'request.method', request.method)
235
- transaction.nameState.setVerb(request.method)
236
- }
237
- }
238
-
239
142
  function wrapResponseEnd(agent, proto) {
240
143
  const tracer = agent.tracer
241
144
 
@@ -419,11 +322,10 @@ function wrapRequest(agent, request) {
419
322
  const transaction = agent.tracer.getTransaction()
420
323
  if (!transaction || internalOnly) {
421
324
  if (!internalOnly && logger.traceEnabled()) {
422
- const logOpts = typeof options === 'string' ? url.parse(options) : options
423
325
  logger.trace(
424
326
  'No transaction, not recording external to %s:%s',
425
- logOpts.hostname || logOpts.host,
426
- logOpts.port
327
+ options?.hostname || options?.host,
328
+ options?.port
427
329
  )
428
330
  }
429
331
  return request.apply(this, reqArgs)
@@ -35,7 +35,6 @@ module.exports = function instrumentations() {
35
35
  mongodb: { type: InstrumentationDescriptor.TYPE_DATASTORE },
36
36
  mysql: { module: './instrumentation/mysql' },
37
37
  next: { module: './instrumentation/nextjs' },
38
- openai: { type: InstrumentationDescriptor.TYPE_GENERIC },
39
38
  pg: { type: InstrumentationDescriptor.TYPE_DATASTORE },
40
39
  q: { type: null },
41
40
  redis: { type: InstrumentationDescriptor.TYPE_DATASTORE },
@@ -59,18 +58,5 @@ module.exports = function instrumentations() {
59
58
  knex: { type: InstrumentationDescriptor.TYPE_TRACKING },
60
59
  loglevel: { type: InstrumentationDescriptor.TYPE_TRACKING },
61
60
  npmlog: { type: InstrumentationDescriptor.TYPE_TRACKING },
62
-
63
- /**
64
- * The modules below are listed here is a temporary solution to maintaining
65
- * the Supportability/Features/onRequire/<module> metrics for libraries
66
- * that have been migrated to use tracing chanel instrumentation.
67
- * Once orchestrion can emit the package version, these can be removed.
68
- * {@link https://github.com/newrelic/node-newrelic/issues/3308 Github Issue}
69
- */
70
- '@elastic/elasticsearch': { type: InstrumentationDescriptor.TYPE_TRACKING },
71
- '@modelcontextprotocol/sdk/client/index.js': { type: InstrumentationDescriptor.TYPE_TRACKING },
72
- '@opensearch-project/opensearch': { type: InstrumentationDescriptor.TYPE_TRACKING },
73
- ioredis: { type: InstrumentationDescriptor.TYPE_TRACKING },
74
- pino: { type: InstrumentationDescriptor.TYPE_TRACKING }
75
61
  }
76
62
  }
@@ -0,0 +1,80 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const logsApi = require('@opentelemetry/api-logs')
9
+ const logsSdk = require('@opentelemetry/sdk-logs')
10
+ const {
11
+ isApplicationLoggingEnabled,
12
+ isLogForwardingEnabled,
13
+ isMetricsEnabled,
14
+ incrementLoggingLinesMetrics
15
+ } = require('#agentlib/util/application-logging.js')
16
+
17
+ const defaultLogger = require('../../logger').child({ component: 'opentelemetry-metrics' })
18
+ const SetupSignal = require('../setup-signal.js')
19
+ const NewRelicLoggerProvider = require('./proxying-provider.js')
20
+ const NoOpExporter = require('./no-op-exporter.js')
21
+ const normalizeTimestamp = require('./normalize-timestamp.js')
22
+ const severityToString = require('./severity-to-string.js')
23
+
24
+ class SetupLogs extends SetupSignal {
25
+ constructor({ agent, logger = defaultLogger } = {}) {
26
+ super({ agent, logger })
27
+
28
+ if (isApplicationLoggingEnabled(agent.config) === false) {
29
+ logger.info('application logging disabled, skipping otel logs setup')
30
+ return
31
+ }
32
+
33
+ agent.metrics
34
+ .getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Logs')
35
+ .incrementCallCount()
36
+
37
+ const exporter = new NoOpExporter()
38
+ const processor = new logsSdk.BatchLogRecordProcessor(exporter)
39
+ const otelProvider = new logsSdk.LoggerProvider({
40
+ processors: [processor]
41
+ })
42
+ const provider = new NewRelicLoggerProvider({
43
+ agent,
44
+ provider: otelProvider,
45
+ emitHandler: nrEmitHandler
46
+ })
47
+ logsApi.logs.setGlobalLoggerProvider(provider)
48
+
49
+ function nrEmitHandler(record) {
50
+ const level = severityToString(record.severityNumber ?? 0)
51
+ if (isMetricsEnabled(agent.config) === true) {
52
+ incrementLoggingLinesMetrics(level, agent.metrics)
53
+ }
54
+
55
+ // TODO: if we decide to support local decorating, implement it here
56
+
57
+ if (isLogForwardingEnabled(agent.config, agent) === true) {
58
+ const meta = agent.getLinkingMetadata(true)
59
+ const timestamp = normalizeTimestamp(record.timestamp)
60
+ const logData = {
61
+ message: record.body,
62
+ level,
63
+ timestamp,
64
+ ...record.attributes,
65
+ ...meta
66
+ }
67
+
68
+ agent.logs.add(logData)
69
+ }
70
+ }
71
+
72
+ agent.emit('otelLogsBootstrapped')
73
+ }
74
+
75
+ teardown() {
76
+ logsApi.logs.disable()
77
+ }
78
+ }
79
+
80
+ module.exports = SetupLogs
@@ -0,0 +1,119 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const { resourceFromAttributes } = require('@opentelemetry/resources')
9
+ const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-proto')
10
+ const {
11
+ AggregationTemporality,
12
+ InMemoryMetricExporter,
13
+ MeterProvider,
14
+ PeriodicExportingMetricReader
15
+ } = require('@opentelemetry/sdk-metrics')
16
+
17
+ const defaultLogger = require('../../logger').child({ component: 'opentelemetry-metrics' })
18
+ const SetupSignal = require('../setup-signal.js')
19
+ const ProxyingExporter = require('./proxying-exporter.js')
20
+
21
+ class SetupMetrics extends SetupSignal {
22
+ constructor({ agent, logger = defaultLogger } = {}) {
23
+ super({ agent, logger })
24
+
25
+ const { config } = agent
26
+ const exportInterval = config.opentelemetry_bridge.metrics.exportInterval
27
+ const exportTimeout = config.opentelemetry_bridge.metrics.exportTimeout
28
+
29
+ const resource = resourceFromAttributes({ })
30
+ const memExporter = new InMemoryMetricExporter(AggregationTemporality.DELTA)
31
+ const proxyExporter = new ProxyingExporter({ exporter: memExporter })
32
+ const reader = new PeriodicExportingMetricReader({
33
+ exporter: proxyExporter,
34
+ exportIntervalMillis: exportInterval,
35
+ exportTimeoutMillis: exportTimeout
36
+ })
37
+ const provider = new MeterProvider({
38
+ readers: [reader],
39
+ resource
40
+ })
41
+
42
+ const getMeter = provider.getMeter
43
+ provider.getMeter = function nrGetMeter(...args) {
44
+ agent.metrics
45
+ .getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Metrics/getMeter')
46
+ .incrementCallCount()
47
+
48
+ const meter = getMeter.apply(provider, args)
49
+ const proto = Object.getPrototypeOf(meter)
50
+ const methods = Object.getOwnPropertyNames(proto).filter((name) => name.startsWith('create'))
51
+ const originals = {}
52
+ for (const method of methods) {
53
+ originals[method] = meter[method]
54
+ // As of 2025-06-17:
55
+ // + createGauge
56
+ // + createHistogram
57
+ // + createCounter
58
+ // + createUpDownCounter
59
+ // + createObservableGauge
60
+ // + createObservableCounter
61
+ // + createObservableUpDownCounter
62
+ meter[method] = function nrWrappedMethod(...args) {
63
+ agent.metrics
64
+ .getOrCreateMetric(`Supportability/Nodejs/OpenTelemetryBridge/Metrics/meter/${method}`)
65
+ .incrementCallCount()
66
+ return originals[method].apply(meter, args)
67
+ }
68
+ }
69
+
70
+ return meter
71
+ }
72
+
73
+ this.coreApi.metrics.setGlobalMeterProvider(provider)
74
+
75
+ agent.metrics
76
+ .getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Metrics')
77
+ .incrementCallCount()
78
+
79
+ // We need access to `agent.config.entity_guid` in order to attach metrics
80
+ // to the correct instrumentation entity. But that value is not available
81
+ // until either at least the first firing of `agent.config.on('change')`, or
82
+ // the `agent.on('started')` event. Which means that we can't finalize the
83
+ // metrics client configuration until after the `started` event.
84
+ agent.on('started', postReady)
85
+ function postReady() {
86
+ agent.removeListener('started', postReady)
87
+
88
+ reader.collect().then(({ resourceMetrics: collectedMetrics }) => {
89
+ proxyExporter.exporter = new OTLPMetricExporter({
90
+ url: `https://${config.host}:${config.port}/v1/metrics`,
91
+ headers: {
92
+ 'api-key': config.license_key
93
+ },
94
+ temporalityPreference: AggregationTemporality.DELTA
95
+ })
96
+
97
+ const resource = resourceFromAttributes({ 'entity.guid': config.entity_guid })
98
+ // Assigning the resource after having received the `entity.guid` from
99
+ // the server is a key detail of this implementation. Unfortunately,
100
+ // we don't have real public access to the object that retains the
101
+ // resource reference. If upstream ever hides this from us, we'll be
102
+ // in a bit of a bind.
103
+ provider._sharedState.resource = resource
104
+
105
+ // Attempt to ship any metrics recorded prior to the `started` event.
106
+ collectedMetrics.resource = resource
107
+ proxyExporter.exporter.export(collectedMetrics, () => {})
108
+
109
+ agent.emit('otelMetricsBootstrapped')
110
+ })
111
+ }
112
+ }
113
+
114
+ teardown() {
115
+ this.coreApi.metrics.disable()
116
+ }
117
+ }
118
+
119
+ module.exports = SetupMetrics