newrelic 13.3.2 → 13.4.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 (111) hide show
  1. package/NEWS.md +43 -0
  2. package/THIRD_PARTY_NOTICES.md +11 -11
  3. package/esm-loader.mjs +2 -2
  4. package/lib/attributes.js +4 -4
  5. package/lib/collector/facts.js +1 -1
  6. package/lib/collector/http-agents.js +2 -2
  7. package/lib/collector/parse-response.js +9 -12
  8. package/lib/collector/remote-method.js +4 -5
  9. package/lib/config/formatters.js +2 -3
  10. package/lib/config/index.js +6 -6
  11. package/lib/harvester.js +1 -1
  12. package/lib/header-processing.js +3 -3
  13. package/lib/instrumentation/@node-redis/client.js +1 -1
  14. package/lib/instrumentation/aws-sdk/v2/instrumentation-helper.js +2 -2
  15. package/lib/instrumentation/aws-sdk/v3/common.js +5 -5
  16. package/lib/instrumentation/aws-sdk/v3/dynamodb.js +4 -4
  17. package/lib/instrumentation/aws-sdk/v3/sns.js +9 -9
  18. package/lib/instrumentation/aws-sdk/v3/sqs.js +6 -6
  19. package/lib/instrumentation/core/http-outbound.js +1 -2
  20. package/lib/instrumentation/fastify/spec-builders.js +11 -11
  21. package/lib/instrumentation/fastify.js +4 -4
  22. package/lib/instrumentation/memcached.js +4 -4
  23. package/lib/instrumentation/mongodb.js +2 -2
  24. package/lib/instrumentation/undici.js +3 -3
  25. package/lib/instrumentations.js +0 -2
  26. package/lib/llm-events/aws-bedrock/bedrock-response.js +6 -6
  27. package/lib/llm-events/aws-bedrock/converse-stream-handler.js +1 -1
  28. package/lib/llm-events/aws-bedrock/stream-handler.js +4 -4
  29. package/lib/metrics/index.js +2 -2
  30. package/lib/metrics/normalizer/rule.js +1 -1
  31. package/lib/metrics/normalizer.js +4 -5
  32. package/lib/metrics/recorders/message-transaction.js +3 -3
  33. package/lib/otel/context-manager.js +4 -4
  34. package/lib/otel/metrics/index.js +3 -3
  35. package/lib/otel/setup.js +6 -0
  36. package/lib/otel/trace-propagator.js +1 -1
  37. package/lib/otel/traces/index.js +1 -1
  38. package/lib/prioritized-attributes.js +2 -2
  39. package/lib/serverless/aws-lambda.js +4 -4
  40. package/lib/shim/datastore-shim.js +1 -1
  41. package/lib/shim/message-shim/consume.js +1 -1
  42. package/lib/shim/message-shim/subscribe-consume.js +1 -2
  43. package/lib/shim/promise-shim.js +1 -3
  44. package/lib/shim/shim.js +3 -4
  45. package/lib/shim/transaction-shim.js +0 -1
  46. package/lib/shim/webframework-shim/common.js +2 -4
  47. package/lib/shim/webframework-shim/middleware.js +8 -8
  48. package/lib/spans/span-context.js +1 -1
  49. package/lib/spans/span-event-aggregator.js +2 -2
  50. package/lib/spans/span-event.js +6 -5
  51. package/lib/spans/streaming-span-attributes.js +1 -1
  52. package/lib/spans/streaming-span-event-aggregator.js +1 -2
  53. package/lib/stats/index.js +1 -1
  54. package/lib/subscriber-configs.js +2 -0
  55. package/lib/subscribers/amqplib/accept-message.js +14 -0
  56. package/lib/subscribers/amqplib/callback-model.js +39 -0
  57. package/lib/subscribers/amqplib/channel-model.js +39 -0
  58. package/lib/subscribers/amqplib/config.js +695 -0
  59. package/lib/subscribers/amqplib/connect.js +74 -0
  60. package/lib/subscribers/amqplib/consume.js +54 -0
  61. package/lib/subscribers/amqplib/get-cb.js +16 -0
  62. package/lib/subscribers/amqplib/get.js +50 -0
  63. package/lib/subscribers/amqplib/purge-queue-cb.js +16 -0
  64. package/lib/subscribers/amqplib/purge-queue.js +37 -0
  65. package/lib/subscribers/amqplib/send-message.js +44 -0
  66. package/lib/subscribers/amqplib/send-or-enqueue.js +20 -0
  67. package/lib/subscribers/amqplib/utils.js +53 -0
  68. package/lib/subscribers/application-logs.js +1 -1
  69. package/lib/subscribers/base.js +167 -9
  70. package/lib/subscribers/cassandra-driver/client-batch.js +49 -0
  71. package/lib/subscribers/cassandra-driver/client-connect.js +35 -0
  72. package/lib/subscribers/cassandra-driver/client-each-row.js +36 -0
  73. package/lib/subscribers/cassandra-driver/client-execute.js +36 -0
  74. package/lib/subscribers/cassandra-driver/client-shutdown.js +32 -0
  75. package/lib/subscribers/cassandra-driver/config.js +145 -0
  76. package/lib/subscribers/cassandra-driver/legacy-client-batch.js +22 -0
  77. package/lib/subscribers/cassandra-driver/legacy-client-connect.js +22 -0
  78. package/lib/subscribers/cassandra-driver/legacy-client-each-row.js +22 -0
  79. package/lib/subscribers/cassandra-driver/legacy-client-execute.js +21 -0
  80. package/lib/subscribers/cassandra-driver/legacy-client-shutdown.js +32 -0
  81. package/lib/subscribers/create-config.js +2 -4
  82. package/lib/subscribers/db-query.js +5 -1
  83. package/lib/subscribers/message-consumer.js +146 -0
  84. package/lib/subscribers/message-producer.js +60 -0
  85. package/lib/subscribers/meta-subscriber.js +69 -0
  86. package/lib/subscribers/openai/base.js +27 -1
  87. package/lib/subscribers/openai/config.js +6 -6
  88. package/lib/subscribers/openai/utils.js +9 -11
  89. package/lib/subscribers/propagation.js +43 -0
  90. package/lib/timer.js +1 -1
  91. package/lib/transaction/dt-payload.js +1 -1
  92. package/lib/transaction/handle.js +8 -9
  93. package/lib/transaction/name-state.js +11 -11
  94. package/lib/transaction/trace/segment.js +2 -3
  95. package/lib/util/attribute-types.js +1 -1
  96. package/lib/util/byte-limit.js +1 -1
  97. package/lib/util/cat.js +13 -13
  98. package/lib/util/flatten.js +2 -3
  99. package/lib/util/is-absolute-path.js +1 -1
  100. package/lib/util/is-string.js +15 -0
  101. package/lib/util/logger.js +6 -6
  102. package/lib/util/stream-sink.js +1 -1
  103. package/lib/utilization/docker-info.js +4 -3
  104. package/lib/w3c/traceparent.js +1 -1
  105. package/package.json +2 -2
  106. package/lib/instrumentation/amqplib/amqplib.js +0 -122
  107. package/lib/instrumentation/amqplib/channel-model.js +0 -124
  108. package/lib/instrumentation/amqplib/channel.js +0 -72
  109. package/lib/instrumentation/amqplib/nr-hooks.js +0 -21
  110. package/lib/instrumentation/amqplib/utils.js +0 -143
  111. package/lib/instrumentation/cassandra-driver.js +0 -131
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const Subscriber = require('../base')
7
+ const { amqpConnection } = require('#agentlib/symbols.js')
8
+ const isString = require('#agentlib/util/is-string.js')
9
+
10
+ /**
11
+ * This subscriber is two-fold:
12
+ * 1. capture the connection arguments and assign to symbol on connection to be used for all other amqplib subscribers
13
+ * 2. create a segment for the connect. Not this will attempt to create a segment but the base subscriber will decide if it is created or not, otherwise it'll return the current context
14
+ */
15
+ class ConnectSubscriber extends Subscriber {
16
+ constructor({ agent, logger }) {
17
+ super({ agent, logger, packageName: 'amqplib', channelName: 'nr_connect' })
18
+ this.events = ['asyncStart', 'asyncEnd']
19
+ this.requireActiveTx = false
20
+ this.callback = -1
21
+ }
22
+
23
+ handler(data, ctx) {
24
+ this.parameters = this.parseConnectionArgs(data.arguments)
25
+ return this.createSegment({
26
+ name: 'amqplib.connect',
27
+ ctx
28
+ })
29
+ }
30
+
31
+ asyncStart(data) {
32
+ if (this.parameters && data.result) {
33
+ data.result[amqpConnection] = this.parameters
34
+ }
35
+ super.asyncStart(data)
36
+ }
37
+
38
+ /**
39
+ * Parses the connection args to return host/port
40
+ *
41
+ * @param {array} args arguments passed to connect
42
+ * @returns {object} {host, port }
43
+ */
44
+ parseConnectionArgs(args = []) {
45
+ const [connArgs] = args
46
+ const params = {}
47
+ if (isString(connArgs)) {
48
+ try {
49
+ const parsedUrl = new URL(connArgs)
50
+ params.host = parsedUrl.hostname
51
+ if (parsedUrl.port) {
52
+ params.port = parseInt(parsedUrl.port, 10)
53
+ }
54
+ } catch (err) {
55
+ this.logger.error('Failed to parse connection string: %s', err.message)
56
+ }
57
+ } else {
58
+ params.port = connArgs.port || (connArgs.protocol === 'amqp' ? 5672 : 5671)
59
+ params.host = connArgs.hostname
60
+ }
61
+
62
+ return params
63
+ }
64
+
65
+ addAttributes(segment) {
66
+ for (let [key, value] of Object.entries(this.parameters)) {
67
+ // eslint-disable-next-line sonarjs/updated-loop-counter
68
+ key = key === 'port' ? 'port_path_or_id' : key
69
+ segment.addAttribute(key, value)
70
+ }
71
+ }
72
+ }
73
+
74
+ module.exports = ConnectSubscriber
@@ -0,0 +1,54 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const MessageConsumer = require('../message-consumer')
7
+ const { getParameters, getParametersFromMessage, TEMP_RE } = require('./utils')
8
+
9
+ class ConsumeSubscriber extends MessageConsumer {
10
+ constructor({ agent, logger, channelName = 'nr_consume' }) {
11
+ super({ agent, logger, packageName: 'amqplib', channelName, system: 'RabbitMQ', type: 'Exchange', callback: 1, transport: 'AMQP' })
12
+ this.segmentName = 'amqplib.Channel#consume'
13
+ }
14
+
15
+ /**
16
+ * Handles the consumption of a message(s),
17
+ * this will create a new transaction every time this function is called.
18
+ * @param {object} data the data associated with the `asyncStart` event
19
+ */
20
+ asyncStart(data) {
21
+ const [queue] = data?.arguments
22
+ const message = data?.error
23
+ this.consumerParameters = getParametersFromMessage.call(this, { channel: data?.self, message })
24
+ this.headers = message?.properties?.headers
25
+ this.destination = this.getExchange(message?.fields)
26
+ this.queue = queue
27
+
28
+ super.asyncStart(data)
29
+ }
30
+
31
+ getExchange(fields = {}) {
32
+ const isDefault = fields.exchange === ''
33
+ let exchange = 'Default'
34
+ if (!isDefault) {
35
+ exchange = TEMP_RE.test(fields.exchange) ? null : fields.exchange
36
+ }
37
+
38
+ return exchange
39
+ }
40
+
41
+ /**
42
+ * Handles the creation of the segment if `channel.consume` is made within
43
+ * an active transaction.
44
+ * @param {object} data the data associated with the subscribed event
45
+ * @param {object} ctx context
46
+ */
47
+ handler(data, ctx) {
48
+ this.parameters = {}
49
+ getParameters.call(this, { params: this.parameters, channel: data.self })
50
+ return super.handler(data, ctx)
51
+ }
52
+ }
53
+
54
+ module.exports = ConsumeSubscriber
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const GetMessageSubscriber = require('./get')
7
+
8
+ class GetMessageCbSubscriber extends GetMessageSubscriber {
9
+ constructor({ agent, logger }) {
10
+ super({ agent, logger, packageName: 'amqplib', channelName: 'cb_get' })
11
+ this.events = ['asyncStart', 'asyncEnd']
12
+ this.callback = -1
13
+ }
14
+ }
15
+
16
+ module.exports = GetMessageCbSubscriber
@@ -0,0 +1,50 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const Subscriber = require('../base')
7
+ const genericRecorder = require('#agentlib/metrics/recorders/generic.js')
8
+ const { getParametersFromMessage, EXCHANGE_PREFIX } = require('./utils')
9
+
10
+ class GetMessageSubscriber extends Subscriber {
11
+ constructor({ agent, logger, channelName = 'nr_get' }) {
12
+ super({ agent, logger, packageName: 'amqplib', channelName })
13
+ this.events = ['asyncEnd']
14
+ }
15
+
16
+ handler(data, ctx) {
17
+ const queue = this.extractQueue(data.arguments)
18
+ const name = `${EXCHANGE_PREFIX}/Consume/${queue}`
19
+ return this.createSegment({
20
+ name,
21
+ ctx,
22
+ recorder: genericRecorder
23
+ })
24
+ }
25
+
26
+ asyncEnd(data) {
27
+ const ctx = this.agent.tracer.getContext()
28
+ const segment = ctx?.segment
29
+ const { result: message } = data
30
+ if (!(message?.fields || message?.properties) || !segment) {
31
+ this.logger.trace('No message from consume, not capturing segment parameters')
32
+ super.asyncEnd(data)
33
+ return
34
+ }
35
+
36
+ const params = getParametersFromMessage.call(this, { message, channel: data?.self })
37
+ for (const [key, value] of Object.entries(params)) {
38
+ segment.addAttribute(key, value)
39
+ }
40
+
41
+ super.asyncEnd(data)
42
+ }
43
+
44
+ extractQueue(args) {
45
+ const [queue] = args
46
+ return `Named/${queue}`
47
+ }
48
+ }
49
+
50
+ module.exports = GetMessageSubscriber
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const PurgeQueueSubscriber = require('./purge-queue')
7
+
8
+ class PurgeQueueCbSubscriber extends PurgeQueueSubscriber {
9
+ constructor({ agent, logger }) {
10
+ super({ agent, logger, channelName: 'cb_purgeQueue' })
11
+ this.events = ['asyncStart', 'asyncEnd']
12
+ this.callback = -1
13
+ }
14
+ }
15
+
16
+ module.exports = PurgeQueueCbSubscriber
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const Subscriber = require('../base')
7
+ const genericRecorder = require('#agentlib/metrics/recorders/generic.js')
8
+ const { TEMP_RE, QUEUE_PREFIX } = require('./utils')
9
+
10
+ class PurgeQueueSubscriber extends Subscriber {
11
+ constructor({ agent, logger, channelName = 'nr_purgeQueue' }) {
12
+ super({ agent, logger, packageName: 'amqplib', channelName })
13
+ this.events = ['asyncEnd']
14
+ }
15
+
16
+ handler(data, ctx) {
17
+ const queue = this.extractQueue(data.arguments)
18
+ const name = `${QUEUE_PREFIX}/Purge/${queue}`
19
+ return this.createSegment({
20
+ name,
21
+ ctx,
22
+ recorder: genericRecorder
23
+ })
24
+ }
25
+
26
+ extractQueue(args) {
27
+ let [queue] = args
28
+
29
+ if (TEMP_RE.test(queue)) {
30
+ queue = null
31
+ }
32
+
33
+ return queue ? `Named/${queue}` : 'Temp'
34
+ }
35
+ }
36
+
37
+ module.exports = PurgeQueueSubscriber
@@ -0,0 +1,44 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const MessageProducerSubscriber = require('../message-producer')
7
+ const { getParameters, TEMP_RE } = require('./utils')
8
+
9
+ class ChannelSubscriber extends MessageProducerSubscriber {
10
+ constructor({ agent, logger }) {
11
+ super({ agent, logger, packageName: 'amqplib', channelName: 'nr_sendMessage', system: 'RabbitMQ', type: 'Exchange' })
12
+ this.events = ['end']
13
+ }
14
+
15
+ handler(data, ctx) {
16
+ const [fields] = data.arguments
17
+
18
+ if (!fields) {
19
+ return ctx
20
+ }
21
+
22
+ this.destination = this.getExchange(fields)
23
+ this.parameters = this.getParameters({ fields, channel: data.self })
24
+ this.headers = fields?.headers
25
+ return super.handler(data, ctx)
26
+ }
27
+
28
+ getParameters({ fields = {}, channel = {} }) {
29
+ const params = {}
30
+ getParameters.call(this, { fields, channel, params })
31
+ return params
32
+ }
33
+
34
+ getExchange(fields = {}) {
35
+ const isDefault = fields.exchange === ''
36
+ let exchange = 'Default'
37
+ if (!isDefault) {
38
+ exchange = TEMP_RE.test(fields.exchange) ? null : fields.exchange
39
+ }
40
+ return exchange
41
+ }
42
+ }
43
+
44
+ module.exports = ChannelSubscriber
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const PropagationSubscriber = require('../propagation')
7
+
8
+ /**
9
+ * This subscriber is purely for propagation of async context.
10
+ * It breaks without binding the transaction and segment from the result of the previous handler.
11
+ * It attaches transaction because `propagateTx` is true
12
+ *
13
+ */
14
+ class SendOrEnqueueSubscriber extends PropagationSubscriber {
15
+ constructor({ agent, logger }) {
16
+ super({ agent, logger, packageName: 'amqplib', channelName: 'nr_sendOrEnqueue', callback: -1 })
17
+ }
18
+ }
19
+
20
+ module.exports = SendOrEnqueueSubscriber
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const { amqpConnection } = require('#agentlib/symbols.js')
8
+
9
+ const TEMP_RE = /^amq\./
10
+ const RMQ_PREFIX = 'MessageBroker/RabbitMQ'
11
+ const EXCHANGE_PREFIX = `${RMQ_PREFIX}/Exchange`
12
+ const QUEUE_PREFIX = `${RMQ_PREFIX}/Queue`
13
+
14
+ function getParametersFromMessage({ message, channel }) {
15
+ const params = {}
16
+ getParameters.call(this, { channel, fields: message.fields, params })
17
+ getParameters.call(this, { channel, fields: message.properties, params })
18
+ return params
19
+ }
20
+
21
+ function getParameters({ fields = {}, channel = {}, params }) {
22
+ if (this.config.message_tracer.segment_parameters.enabled === false) {
23
+ this.logger.trace('Not capturing segment parameters, `message_tracer.segments_parameters.enabled` is false')
24
+ return
25
+ }
26
+
27
+ const { host, port } = channel?.connection?.[amqpConnection]
28
+
29
+ if (fields.routingKey) {
30
+ params.routing_key = fields.routingKey
31
+ }
32
+ if (fields.correlationId) {
33
+ params.correlation_id = fields.correlationId
34
+ }
35
+ if (fields.replyTo) {
36
+ params.reply_to = fields.replyTo
37
+ }
38
+ if (host) {
39
+ params.host = host
40
+ }
41
+
42
+ if (port) {
43
+ params.port = port
44
+ }
45
+ }
46
+
47
+ module.exports = {
48
+ getParameters,
49
+ getParametersFromMessage,
50
+ EXCHANGE_PREFIX,
51
+ QUEUE_PREFIX,
52
+ TEMP_RE
53
+ }
@@ -22,7 +22,7 @@ class ApplicationLogsSubscriber extends Subscriber {
22
22
  * but only once. You should call this in your handler.
23
23
  * We cannot recreate creating this metric on require of a logging library
24
24
  * because we no longer monkey patch but subscribe to events.
25
- * @param lib
25
+ * @param {string} lib name of library
26
26
  */
27
27
  createModuleUsageMetric(lib) {
28
28
  if (this.libMetricCreated === false) {
@@ -7,7 +7,14 @@
7
7
  // eslint-disable-next-line n/no-unsupported-features/node-builtins
8
8
  const { tracingChannel } = require('node:diagnostics_channel')
9
9
  const semver = require('semver')
10
- const NAMES = require('../metrics/names')
10
+ const NAMES = require('#agentlib/metrics/names.js')
11
+ const cat = require('#agentlib/util/cat.js')
12
+ // Used for the `traceCallback` work.
13
+ // This can be removed when we add true support into orchestrion
14
+ const makeCall = (fn) => (...args) => fn.call(...args)
15
+ const ArrayPrototypeAt = makeCall(Array.prototype.at)
16
+ const ArrayPrototypeSplice = makeCall(Array.prototype.splice)
17
+ // End temp work
11
18
 
12
19
  /**
13
20
  * The baseline parameters available to all subscribers.
@@ -15,9 +22,8 @@ const NAMES = require('../metrics/names')
15
22
  * @typedef {object} SubscriberParams
16
23
  * @property {object} agent A New Relic Node.js agent instance.
17
24
  * @property {object} logger An agent logger instance.
18
- * @property {string} packageName The npm installable name for the package
19
- * being instrumented. This is what a developer would provide to the `require`
20
- * function.
25
+ * @property {string} packageName The package name being instrumented.
26
+ * This is what a developer would provide to the `require` function.
21
27
  * @property {string} channelName A unique name for the diagnostics channel
22
28
  * that will be created and monitored.
23
29
  */
@@ -41,9 +47,11 @@ const NAMES = require('../metrics/names')
41
47
  * channel event names. This provides a namespace for the events we are
42
48
  * injecting into a module.
43
49
  * @property {boolean} [requireActiveTx=true] If true, the subscriber will only handle events when there is an active transaction.
50
+ * @property {boolean} [propagateTx=false] If true, it will assign the transaction from the `start.bindStore` to the context that is passed to all tracing channel events
44
51
  * @property {string} id A unique identifier for the subscriber, combining the prefix, package name, and channel name.
45
52
  * @property {TracingChannel} channel The tracing channel instance this subscriber will be monitoring.
46
53
  * @property {AsyncLocalStorage} store The async local storage instance used for context management.
54
+ * @property {number} callback position of callback if it needs to be wrapped for instrumentation. -1 means last argument
47
55
  */
48
56
  class Subscriber {
49
57
  constructor({ agent, logger, packageName, channelName }) {
@@ -57,9 +65,11 @@ class Subscriber {
57
65
  this.internal = false
58
66
  this.prefix = 'orchestrion:'
59
67
  this.requireActiveTx = true
68
+ this.propagateTx = false
60
69
  this.id = `${this.prefix}${this.packageName}:${this.channelName}`
61
70
  this.channel = tracingChannel(this.id)
62
71
  this.store = agent.tracer._contextManager._asyncLocalStorage
72
+ this.callback = null
63
73
  }
64
74
 
65
75
  shouldCreateSegment(parent) {
@@ -68,6 +78,49 @@ class Subscriber {
68
78
  )
69
79
  }
70
80
 
81
+ /**
82
+ * Note: This is a temporary patch until we can get the correct implementation
83
+ * of `tracingChannel.traceCallback` into orchestrion-js.
84
+ *
85
+ * This will wrap a callback at a given position and reassign the callback argument to the wrapped one
86
+ *
87
+ * @param {number} position index of the callback, you can specify -1 to be the last
88
+ * @param {object} context the event passed to the tracing channel hooks
89
+ */
90
+ traceCallback(position, context) {
91
+ this.logger.trace('Wrapping the callback at position %s', position)
92
+ const { asyncStart, asyncEnd, error } = this.channel
93
+ function wrappedCallback(err, res) {
94
+ // assigning a boolean to the context so we know that the
95
+ // `error`, `asyncStart`, and `asyncEnd` are coming from the wrapped callback
96
+ context.callback = true
97
+ if (err) {
98
+ context.error = err
99
+ error.publish(context)
100
+ } else {
101
+ context.result = res
102
+ }
103
+
104
+ // Using runStores here enables manual context failure recovery
105
+ asyncStart.runStores(context, () => {
106
+ try {
107
+ if (callback) {
108
+ return Reflect.apply(callback, this, arguments)
109
+ }
110
+ } finally {
111
+ asyncEnd.publish(context)
112
+ }
113
+ })
114
+ }
115
+
116
+ const callback = ArrayPrototypeAt(context.arguments, position)
117
+ if (typeof callback !== 'function') {
118
+ this.logger.trace('Callback is not present, not wrapping')
119
+ } else {
120
+ ArrayPrototypeSplice(context.arguments, position, 1, wrappedCallback)
121
+ }
122
+ }
123
+
71
124
  /**
72
125
  * Creates the `Supportability/Features/Instrumentation/OnRequire/<packageName>`
73
126
  * and `Supportability/Features/Instrumentation/OnRequire/<packageName>/Version/<majorVersion>`
@@ -123,12 +176,12 @@ class Subscriber {
123
176
  segment.opaque = this.opaque
124
177
  segment.shimId = this.packageName
125
178
  segment.start()
126
- this.logger.trace('Created segment %s', name)
179
+ this.logger.trace('Created segment %s, returning new context', name)
127
180
  this.addAttributes(segment)
128
181
  const newCtx = ctx.enterSegment({ segment })
129
182
  return newCtx
130
183
  } else {
131
- this.logger.trace('Failed to create segment for %s', name)
184
+ this.logger.trace('Failed to create segment for %s, returning existing context', name)
132
185
  return ctx
133
186
  }
134
187
  }
@@ -142,8 +195,20 @@ class Subscriber {
142
195
 
143
196
  }
144
197
 
198
+ /**
199
+ * Not all subscribers need change the context on `start`.
200
+ * This is defined on base to fulfill those use cases
201
+ * @param {object} data event passed to handler
202
+ * @param {Context} ctx context passed to handler
203
+ * @returns {Context} either new context or existing
204
+ */
205
+ handler(data, ctx) {
206
+ return ctx
207
+ }
208
+
145
209
  /**
146
210
  * Checks if the subscriber is enabled based on the agent's configuration.
211
+ * @returns boolean if subscriber is enabled
147
212
  */
148
213
  get enabled() {
149
214
  return this.config.instrumentation[this.packageName].enabled === true
@@ -156,14 +221,31 @@ class Subscriber {
156
221
  */
157
222
  enable() {
158
223
  this.channel.start.bindStore(this.store, (data) => {
224
+ // only wrap the callback if a subscriber has a callback property defined
225
+ if (this.callback !== null) {
226
+ this.traceCallback(this.callback, data)
227
+ }
159
228
  this.trackInstrumentationUsage(data?.moduleVersion)
160
229
  const ctx = this.agent.tracer.getContext()
161
230
  if (this.requireActiveTx && !ctx?.transaction?.isActive()) {
162
- this.logger.debug('Not recording event for %s, transaction is not active', this.package)
231
+ this.logger.trace('Not recording event for %s, transaction is not active', this.package)
163
232
  return ctx
164
233
  }
165
234
 
166
- return this.handler(data, ctx)
235
+ const result = this.handler(data, ctx)
236
+ // we cannot rely on the context manager to obtain the active segment
237
+ // in the `asyncStart` and `asyncEnd` events. This is because other instrumented
238
+ // functions are being executed at times. so we assign the active segment on the data
239
+ // so it can be used later to properly touch the segment in `asyncStart` and `asyncEnd`
240
+ if (this.callback !== null) {
241
+ data.segment = result?.segment
242
+ this.logger.trace('Adding segment %s to event context', data?.segment?.name)
243
+ }
244
+
245
+ if (this.propagateTx) {
246
+ data.transaction = result?.transaction
247
+ }
248
+ return result
167
249
  })
168
250
  }
169
251
 
@@ -174,15 +256,91 @@ class Subscriber {
174
256
  this.channel.start.unbindStore(this.store)
175
257
  }
176
258
 
259
+ /**
260
+ * This should only be used for callback based functions to touch the segment for the function
261
+ * that implements a callback.
262
+ * @param {object} data event passed to asyncStart hook
263
+ */
264
+ asyncStart(data) {
265
+ const ctx = this.agent.tracer.getContext()
266
+ if (data.callback !== true || this.internal === true || (this.requireActiveTx && !ctx?.transaction?.isActive())) {
267
+ this.logger.trace('Not touching parent in asyncStart for %s, transaction is not active? %s, segment is internal? %s, or no callback to bind', this.id, ctx?.transaction?.isActive(), this.internal)
268
+ return
269
+ }
270
+
271
+ this.logger.trace('touching segment %s, in asyncStart', data?.segment?.name)
272
+ data?.segment?.touch()
273
+ }
274
+
177
275
  /**
178
276
  * Common handler for when async events end.
179
277
  * It gets the context and touches the segment if it exists.
278
+ * @param {object} data event passed to asyncEnd hook
180
279
  */
181
- asyncEnd() {
280
+ asyncEnd(data) {
281
+ const ctx = this.agent.tracer.getContext()
282
+ if (this.internal === true) {
283
+ this.logger.trace('asyncEnd occurring for %s internal event, not touching segment', this.id)
284
+ return
285
+ }
286
+
287
+ if (data?.callback === true) {
288
+ this.logger.trace('touching callback segment %s, in asyncEnd', data?.segment?.name)
289
+ data?.segment?.touch()
290
+ } else {
291
+ this.logger.trace('touching segment %s, in asyncEnd', ctx?.segment?.name)
292
+ ctx?.segment?.touch()
293
+ }
294
+ }
295
+
296
+ end() {
182
297
  const ctx = this.agent.tracer.getContext()
183
298
  ctx?.segment?.touch()
184
299
  }
185
300
 
301
+ /**
302
+ * Handles injecting w3c tracecontext in outgoing headers. If DT is disabled, and CAT is enabled
303
+ * it properly handles CAT.
304
+ *
305
+ * **Note**: This passes in the trace, segment and trace flags manually because this is called in the `start`
306
+ * right before a function is bound to context but segment is created for the function.
307
+ *
308
+ * @param {object} params to function
309
+ * @param {Context} params.ctx current context, not yet bound to context manager
310
+ * @param {object} params.headers headers for outgoing call
311
+ * @param {boolean} params.useMqNames flag to indicate use the MQ specific CAT header names
312
+ * @returns {void}
313
+ */
314
+ insertDTHeaders({ ctx, headers, useMqNames } = {}) {
315
+ const crossAppTracingEnabled = this.config.cross_application_tracer.enabled
316
+ const distributedTracingEnabled = this.config.distributed_tracing.enabled
317
+
318
+ if (!distributedTracingEnabled && !crossAppTracingEnabled) {
319
+ this.logger.trace('Distributed Tracing and CAT are both disabled, not adding headers.')
320
+ return
321
+ }
322
+
323
+ if (!headers) {
324
+ this.logger.debug('Missing headers object, not adding headers!')
325
+ return
326
+ }
327
+
328
+ const tx = ctx?.transaction
329
+ if (!tx?.isActive()) {
330
+ this.logger.trace('No active transaction found, not adding headers.')
331
+ return
332
+ }
333
+
334
+ if (distributedTracingEnabled) {
335
+ // we have to pass in traceId, segment id, and hard code traceFlags to 1
336
+ // because we're inserting headers right before the original function is bound.
337
+ const traceFlags = tx.sampled === true ? 1 : 0
338
+ tx.insertDistributedTraceHeaders(headers, null, { traceId: tx.traceId, spanId: ctx?.segment?.id, traceFlags })
339
+ } else {
340
+ cat.addCatHeaders(this.config, tx, headers, useMqNames)
341
+ }
342
+ }
343
+
186
344
  /*
187
345
  * Subscribes to the events defined in the `events` array.
188
346
  */
@@ -0,0 +1,49 @@
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 DbQuerySubscriber = require('../db-query')
9
+
10
+ /**
11
+ * Given the arguments for Cassandra's `batch` method, this finds the first
12
+ * query in the batch.
13
+ *
14
+ * @param {Array} args - original arguments passed to the batch function
15
+ * @returns {string} The query for this batch request.
16
+ */
17
+ function findBatchQueryArg(args) {
18
+ const sql = (args[0] && args[0][0]) || ''
19
+ return sql.query || sql
20
+ }
21
+
22
+ /**
23
+ * Subscribes to the `batch` event in `cassandra-driver`
24
+ * and extracts relevant information from the queries.
25
+ */
26
+ class CassandraBatchSubscriber extends DbQuerySubscriber {
27
+ constructor({ agent, logger, channelName = 'nr_batch' }) {
28
+ super({ agent, logger, channelName, packageName: 'cassandra-driver', system: 'Cassandra' })
29
+ this.events = ['asyncEnd']
30
+ this.isBatch = true
31
+ }
32
+
33
+ handler(data, ctx) {
34
+ const { self, arguments: args } = data
35
+ this.queryString = findBatchQueryArg(args)
36
+ this.setParameters(self)
37
+ return super.handler(data, ctx)
38
+ }
39
+
40
+ setParameters(self) {
41
+ this.parameters = {}
42
+ this.parameters.product = this.system
43
+ this.parameters.database_name = self?.keyspace
44
+ this.parameters.host = self?.options?.contactPoints?.[0]
45
+ this.parameters.port_path_or_id = self?.options?.protocolOptions?.port
46
+ }
47
+ }
48
+
49
+ module.exports = CassandraBatchSubscriber