newrelic 13.7.0 → 13.8.1

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 (31) hide show
  1. package/NEWS.md +38 -0
  2. package/lib/instrumentation/aws-sdk/v3/bedrock.js +16 -13
  3. package/lib/instrumentation/langchain/runnable.js +6 -1
  4. package/lib/instrumentations.js +0 -2
  5. package/lib/llm-events/aws-bedrock/index.js +1 -1
  6. package/lib/llm-events/error-message.js +32 -20
  7. package/lib/metrics/names.js +6 -1
  8. package/lib/samplers/always-on-sampler.js +1 -1
  9. package/lib/samplers/index.js +2 -1
  10. package/lib/samplers/ratio-based-sampler.js +0 -1
  11. package/lib/serverless/aws-lambda.js +18 -12
  12. package/lib/spans/span-event-aggregator.js +29 -8
  13. package/lib/spans/span-event.js +8 -2
  14. package/lib/spans/streaming-span-event-aggregator.js +4 -0
  15. package/lib/subscriber-configs.js +2 -0
  16. package/lib/subscribers/application-logs.js +5 -1
  17. package/lib/subscribers/bunyan/base.js +24 -0
  18. package/lib/subscribers/bunyan/config.js +35 -0
  19. package/lib/subscribers/bunyan/emit.js +31 -0
  20. package/lib/subscribers/bunyan/index.js +63 -0
  21. package/lib/subscribers/google-genai/base.js +123 -0
  22. package/lib/subscribers/google-genai/config.js +84 -0
  23. package/lib/subscribers/google-genai/embed-content.js +70 -0
  24. package/lib/subscribers/google-genai/generate-content-stream.js +125 -0
  25. package/lib/subscribers/google-genai/generate-content.js +55 -0
  26. package/lib/transaction/index.js +7 -0
  27. package/lib/transaction/trace/index.js +79 -2
  28. package/package.json +1 -1
  29. package/lib/instrumentation/@google/genai.js +0 -282
  30. package/lib/instrumentation/bunyan.js +0 -96
  31. package/lib/llm-events/aws-bedrock/error.js +0 -29
package/NEWS.md CHANGED
@@ -1,3 +1,41 @@
1
+ ### v13.8.1 (2025-12-15)
2
+
3
+ #### Bug fixes
4
+
5
+ * Updated AWS Lambda instrumentation to skip wrapping handler callback if not present ([#3602](https://github.com/newrelic/node-newrelic/pull/3602)) ([d56e27d](https://github.com/newrelic/node-newrelic/commit/d56e27d5764b4c0a7d513fffa3310148d7cebea6))
6
+
7
+ #### Documentation
8
+
9
+ * Updated compatibility report ([#3596](https://github.com/newrelic/node-newrelic/pull/3596)) ([ed24527](https://github.com/newrelic/node-newrelic/commit/ed24527cfd2e836de67a769cbeaa63683b207506))
10
+
11
+ ### v13.8.0 (2025-12-11)
12
+
13
+ #### Features
14
+
15
+ * Added support for `@langchain/aws` ([#3563](https://github.com/newrelic/node-newrelic/pull/3563)) ([56c4a78](https://github.com/newrelic/node-newrelic/commit/56c4a782c7e05d94dc78ec0bd5eed19bfcc8a835))
16
+ * Additional support for partial granularity traces (Not available for production use)
17
+ * Added partial granularity tracking metrics ([#3566](https://github.com/newrelic/node-newrelic/pull/3566)) ([061f0fc](https://github.com/newrelic/node-newrelic/commit/061f0fca7e61b24fe08cb55538df4c4326c7dd69))
18
+ * updated `AlwaysOn` sampler to set priority to 3 for full trace and 2 for partial trace ([#3577](https://github.com/newrelic/node-newrelic/pull/3577)) ([e7bcff4](https://github.com/newrelic/node-newrelic/commit/e7bcff4364b5ea8a8af7a7ee58ff05e9e2edb977))
19
+ * Updated the span generation process ([#3585](https://github.com/newrelic/node-newrelic/pull/3585)) ([74777d5](https://github.com/newrelic/node-newrelic/commit/74777d5f7bfc182f5e36480a9bce1b67db13ce45))
20
+
21
+ #### Code refactoring
22
+
23
+ * Converted AWS Bedrock's `LlmError` to use `LlmErrorMessage` ([#3567](https://github.com/newrelic/node-newrelic/pull/3567)) ([8b2bb42](https://github.com/newrelic/node-newrelic/commit/8b2bb428eb3830b7cee4fa30e77a9562628fdd01))
24
+ * Updated `@google/genai` instrumentation to subscribe to events emitted ([#3467](https://github.com/newrelic/node-newrelic/pull/3467)) ([e8d9ba1](https://github.com/newrelic/node-newrelic/commit/e8d9ba188b4399604c6410b2fca406f8d7f338b5))
25
+ * Updated `bunyan` instrumentation to subscribe to events emitted ([#3589](https://github.com/newrelic/node-newrelic/pull/3589)) ([f1e5f12](https://github.com/newrelic/node-newrelic/commit/f1e5f129594a4c3acd8075604bae52013555cf52))
26
+
27
+ #### Documentation
28
+
29
+ * Updated compatibility report ([#3592](https://github.com/newrelic/node-newrelic/pull/3592)) ([7455533](https://github.com/newrelic/node-newrelic/commit/7455533e64d88233adf5aa0246261417049c2122))
30
+
31
+ #### Miscellaneous chores
32
+
33
+ * Refactor span link serialization ([#3574](https://github.com/newrelic/node-newrelic/pull/3574)) ([103044d](https://github.com/newrelic/node-newrelic/commit/103044d25a968649bdb0e39df054757163cd986f))
34
+
35
+ #### Tests
36
+
37
+ * Moved mock aws bedrock server creation to `test/lib/aws-server-stubs/index.js` ([#3593](https://github.com/newrelic/node-newrelic/pull/3593)) ([270df81](https://github.com/newrelic/node-newrelic/commit/270df8109a2adbcb3ff405030e2be7104bee6511))
38
+
1
39
  ### v13.7.0 (2025-12-08)
2
40
 
3
41
  #### Features
@@ -8,7 +8,7 @@ const {
8
8
  LlmChatCompletionMessage,
9
9
  LlmChatCompletionSummary,
10
10
  LlmEmbedding,
11
- LlmError,
11
+ LlmErrorMessage,
12
12
  BedrockCommand,
13
13
  BedrockResponse,
14
14
  StreamHandler
@@ -174,8 +174,13 @@ function recordChatCompletionMessages({
174
174
  recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: summary })
175
175
 
176
176
  if (err) {
177
- const llmError = new LlmError({ bedrockResponse, err, summary })
178
- agent.errors.add(transaction, err, llmError)
177
+ const llmErrorMessage = new LlmErrorMessage(
178
+ { response: bedrockResponse,
179
+ cause: err,
180
+ summary,
181
+ useNameAsCode: true }
182
+ )
183
+ agent.errors.add(transaction, err, llmErrorMessage)
179
184
  }
180
185
  }
181
186
 
@@ -221,8 +226,13 @@ function recordEmbeddingMessage({
221
226
  }
222
227
 
223
228
  if (err) {
224
- const llmError = new LlmError({ bedrockResponse, err, embedding: embeddings.length === 1 ? embeddings[0] : undefined })
225
- agent.errors.add(transaction, err, llmError)
229
+ const llmErrorMessage = new LlmErrorMessage(
230
+ { response: bedrockResponse,
231
+ cause: err,
232
+ embedding: embeddings.length === 1 ? embeddings[0] : undefined,
233
+ useNameAsCode: true }
234
+ )
235
+ agent.errors.add(transaction, err, llmErrorMessage)
226
236
  }
227
237
  }
228
238
 
@@ -293,14 +303,7 @@ function getBedrockSpec({ commandName }, shim, _original, _name, args) {
293
303
  onComplete: handleResponse,
294
304
  passThroughParams
295
305
  })
296
- response.output = {
297
- ...response.output ?? {},
298
- output: {
299
- message: {
300
- content: handler.generator(handleResponse)
301
- }
302
- }
303
- }
306
+ response.output.stream = handler.generator(handleResponse)
304
307
  } else {
305
308
  const handler = new StreamHandler({
306
309
  stream: response.output.body,
@@ -153,7 +153,12 @@ function wrapNextHandler({ shim, output, segment, transaction, request, metadata
153
153
  shim
154
154
  })
155
155
  } else {
156
- content += result.value
156
+ if (typeof result?.value?.content === 'string') {
157
+ // if result.value is a AIMessageChunk
158
+ content += result.value.content
159
+ } else {
160
+ content += result.value
161
+ }
157
162
  }
158
163
  return result
159
164
  } catch (error) {
@@ -11,7 +11,6 @@ const InstrumentationDescriptor = require('./instrumentation-descriptor')
11
11
  module.exports = function instrumentations() {
12
12
  return {
13
13
  '@azure/functions': { type: InstrumentationDescriptor.TYPE_GENERIC },
14
- '@google/genai': { type: InstrumentationDescriptor.TYPE_GENERIC },
15
14
  '@grpc/grpc-js': { module: './instrumentation/grpc-js' },
16
15
  '@hapi/hapi': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
17
16
  '@hapi/vision': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
@@ -21,7 +20,6 @@ module.exports = function instrumentations() {
21
20
  '@redis/client': { type: InstrumentationDescriptor.TYPE_DATASTORE },
22
21
  'aws-sdk': { module: './instrumentation/aws-sdk' },
23
22
  bluebird: { type: InstrumentationDescriptor.TYPE_PROMISE },
24
- bunyan: { type: InstrumentationDescriptor.TYPE_GENERIC },
25
23
  connect: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
26
24
  fastify: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
27
25
  'generic-pool': { type: InstrumentationDescriptor.TYPE_GENERIC },
@@ -12,7 +12,7 @@ module.exports = {
12
12
  LlmChatCompletionSummary: require('./chat-completion-summary'),
13
13
  LlmEmbedding: require('./embedding'),
14
14
  LlmEvent: require('./event'),
15
- LlmError: require('./error'),
15
+ LlmErrorMessage: require('../error-message'),
16
16
  StreamHandler: require('./stream-handler'),
17
17
  ConverseStreamHandler: require('./converse-stream-handler')
18
18
  }
@@ -12,8 +12,8 @@
12
12
  module.exports = class LlmErrorMessage {
13
13
  /**
14
14
  * @param {object} params Constructor parameters
15
- * @param {object} [params.response] Instance of an incoming message.
16
- * @param {object} [params.cause] An instance of the OpenAI error object.
15
+ * @param {object} params.response Instance of an incoming message.
16
+ * @param {object} params.cause An instance of the LLM error object.
17
17
  * @param {LlmChatCompletionSummary} [params.summary] Details about the
18
18
  * conversation if it was a chat completion conversation.
19
19
  * @param {LlmEmbedding} [params.embedding] Details about the conversation
@@ -21,34 +21,46 @@ module.exports = class LlmErrorMessage {
21
21
  * @param {LlmVectorStoreSearch} [params.vectorsearch] Details about the vector
22
22
  * search if it was a vector search event.
23
23
  * @param {LlmTool} [params.tool] Details about the tool event if it was a tool event.
24
+ * @param {boolean} [params.useNameAsCode] defaults to false, only Bedrock sets it to true so far
24
25
  */
25
- constructor({ response, cause, summary, embedding, vectorsearch, tool } = {}) {
26
- // For @google/genai only, cause does not have the `error` or `status` fields,
27
- // but it does have `message` with the info we need. So, we need to parse
28
- // the relevant fields from cause.message to get `status` and `error`.
29
- let parsedError
30
- const isGeminiVendor = embedding?.vendor === 'gemini' || summary?.vendor === 'gemini'
31
- if (isGeminiVendor && cause?.message) {
32
- try {
33
- // Extract the JSON portion of the cause.message
34
- const jsonStartIndex = cause.message.indexOf('{')
35
- const jsonString = cause.message.substring(jsonStartIndex)
36
- parsedError = JSON.parse(jsonString)?.error
37
- } catch {
38
- parsedError = undefined
39
- }
40
- }
41
- this['http.statusCode'] = response?.status ?? cause?.status ?? parsedError?.code
26
+ constructor({ response, cause, summary = {}, embedding = {}, vectorsearch = {}, tool = {}, useNameAsCode = false } = {}) {
27
+ this['http.statusCode'] = response?.statusCode ?? response?.status ?? cause?.status
42
28
  this['error.message'] = cause?.message
43
- this['error.code'] = response?.code ?? cause?.error?.code ?? parsedError?.code
29
+ this['error.code'] = response?.code ?? cause?.error?.code
30
+ if (useNameAsCode) {
31
+ this['error.code'] = cause?.name
32
+ }
44
33
  this['error.param'] = response?.param ?? cause?.error?.param
45
34
  this.completion_id = summary?.id
46
35
  this.embedding_id = embedding?.id
47
36
  this.vector_store_id = vectorsearch?.id
48
37
  this.tool_id = tool?.id
38
+
39
+ if (embedding?.vendor === 'gemini' || summary?.vendor === 'gemini') {
40
+ this._handleGemini(cause)
41
+ }
49
42
  }
50
43
 
51
44
  get [Symbol.toStringTag]() {
52
45
  return 'LlmErrorMessage'
53
46
  }
47
+
48
+ /**
49
+ * For `@google/genai` only, `cause` does not have the `error` or `status` fields,
50
+ * but it does have `message` with the info we need. So, we need to parse
51
+ * the relevant fields from cause.message to get `status` and `error`.
52
+ * @param {object} cause error object
53
+ */
54
+ _handleGemini(cause) {
55
+ if (cause?.message) {
56
+ try {
57
+ const jsonStartIndex = cause.message.indexOf('{')
58
+ const jsonString = cause.message.substring(jsonStartIndex)
59
+ const parsedError = JSON.parse(jsonString)?.error
60
+
61
+ this['http.statusCode'] = parsedError?.code
62
+ this['error.code'] = parsedError?.code
63
+ } catch { }
64
+ }
65
+ }
54
66
  }
@@ -291,11 +291,16 @@ const DISTRIBUTED_TRACE = {
291
291
 
292
292
  const SPAN_EVENT_PREFIX = 'SpanEvent/'
293
293
 
294
+ const PARTIAL_GRANULARITY = 'DistributedTrace/PartialGranularity'
295
+
294
296
  const SPAN_EVENTS = {
295
297
  SEEN: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'TotalEventsSeen',
296
298
  SENT: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'TotalEventsSent',
297
299
  DROPPED: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'Discarded',
298
- LIMIT: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'Limit'
300
+ LIMIT: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'Limit',
301
+ PARTIAL_PREFIX: SUPPORTABILITY.PREFIX + PARTIAL_GRANULARITY,
302
+ KEPT: '/Span/Kept',
303
+ INSTRUMENTED: '/Span/Instrumented',
299
304
  }
300
305
 
301
306
  const INFINITE_TRACING = {
@@ -9,7 +9,7 @@ class AlwaysOnSampler extends Sampler {
9
9
  applySamplingDecision({ transaction, partialType }) {
10
10
  if (!transaction) return
11
11
  transaction.partialType = partialType
12
- transaction.priority = 2.0
12
+ transaction.priority = partialType ? 2.0 : 3.0
13
13
  transaction.sampled = true
14
14
  }
15
15
  }
@@ -10,6 +10,7 @@ const AlwaysOffSampler = require('./always-off-sampler')
10
10
  const AlwaysOnSampler = require('./always-on-sampler')
11
11
  const TraceIdRatioBasedSampler = require('./ratio-based-sampler')
12
12
  const logger = require('../logger').child({ component: 'samplers' })
13
+ const { PARTIAL_TYPES } = require('../transaction')
13
14
 
14
15
  /**
15
16
  * Manages the different samplers used for distributed tracing sampling decisions.
@@ -30,7 +31,7 @@ class Samplers {
30
31
  constructor(agent) {
31
32
  this.fullEnabled = agent.config.distributed_tracing.enabled && agent.config.distributed_tracing.sampler.full_granularity.enabled
32
33
  this.partialEnabled = agent.config.distributed_tracing.enabled && agent.config.distributed_tracing.sampler.partial_granularity.enabled
33
- this.partialType = agent.config.distributed_tracing.sampler.partial_granularity.type
34
+ this.partialType = PARTIAL_TYPES[agent.config.distributed_tracing.sampler.partial_granularity.type.toUpperCase()]
34
35
  this.adaptiveSampler = null
35
36
  this.root = this.determineSampler({ agent, sampler: 'root' })
36
37
  this.remoteParentSampled = this.determineSampler({ agent, sampler: 'remote_parent_sampled' })
@@ -13,7 +13,6 @@ const Sampler = require('./sampler')
13
13
  class TraceIdRatioBasedSampler extends Sampler {
14
14
  /**
15
15
  * @param {object} opts Sampler options.
16
- * @param {Agent} opts.agent - The New Relic agent instance.
17
16
  * @param {number} opts.ratio - The sampling ratio (0 to 1).
18
17
  */
19
18
  constructor(opts) {
@@ -200,14 +200,13 @@ class AwsLambda {
200
200
  return handler.apply(this, args)
201
201
  }
202
202
 
203
- const event = args[0]
204
- const context = args[2]
203
+ const [event, responseStream, context] = args
205
204
  logger.trace('In stream handler, lambda function name', context?.functionName)
206
205
  const { segment, txnEnder } = awsLambda.createSegment({ context, event, transaction, recorder: recordBackground })
207
206
  args[1] = awsLambda.wrapStreamAndCaptureError(
208
207
  transaction,
209
208
  txnEnder,
210
- args[1]
209
+ responseStream
211
210
  )
212
211
 
213
212
  let res
@@ -238,8 +237,7 @@ class AwsLambda {
238
237
  return handler.apply(this, args)
239
238
  }
240
239
 
241
- const event = args[0]
242
- const context = args[1]
240
+ const [event, context, callback] = args
243
241
  logger.trace('Lambda function name', context?.functionName)
244
242
  const isApiGatewayLambdaProxy = apiGateway.isLambdaProxyEvent(event)
245
243
  logger.trace('Is this Lambda event an API Gateway or ALB web proxy?', isApiGatewayLambdaProxy)
@@ -256,16 +254,24 @@ class AwsLambda {
256
254
  setWebRequest(transaction, webRequest)
257
255
  resultProcessor = getApiGatewayLambdaProxyResultProcessor(transaction)
258
256
  }
259
- const cbIndex = args.length - 1
260
- args[cbIndex] = awsLambda.wrapCallbackAndCaptureError(
261
- transaction,
262
- txnEnder,
263
- args[cbIndex],
264
- resultProcessor
265
- )
257
+
258
+ if (typeof callback === 'function') {
259
+ logger.trace('Wrapping Lambda handler callback')
260
+ const cbIndex = args.length - 1
261
+ args[cbIndex] = awsLambda.wrapCallbackAndCaptureError(
262
+ transaction,
263
+ txnEnder,
264
+ callback,
265
+ resultProcessor
266
+ )
267
+ } else {
268
+ logger.trace('Lambda handler callback not present, skip wrapping')
269
+ }
266
270
 
267
271
  // context.{done,fail,succeed} are all considered deprecated by
268
272
  // AWS, but are considered functional.
273
+ // TODO: Remove when we drop Node.js 22
274
+ // see: https://aws.amazon.com/blogs/compute/node-js-24-runtime-now-available-in-aws-lambda/
269
275
  context.done = awsLambda.wrapCallbackAndCaptureError(transaction, txnEnder, context.done)
270
276
  context.fail = awsLambda.wrapCallbackAndCaptureError(transaction, txnEnder, context.fail)
271
277
  shim.wrap(context, 'succeed', function wrapSucceed(shim, original) {
@@ -35,8 +35,23 @@ class SpanEventAggregator extends EventAggregator {
35
35
  reservoir_size: events.limit,
36
36
  events_seen: events.seen
37
37
  }
38
+
38
39
  const eventData = events.toArray()
39
- return [this.runId, metrics, eventData]
40
+ const eventsPayload = []
41
+ for (const event of eventData) {
42
+ eventsPayload.push(event)
43
+
44
+ if (event.spanLinks.length > 0) {
45
+ Array.prototype.push.apply(eventsPayload, event.spanLinks)
46
+ delete event.spanLinks
47
+ }
48
+ }
49
+
50
+ return [this.runId, metrics, eventsPayload]
51
+ }
52
+
53
+ toString() {
54
+ return 'SpanEventAggregator'
40
55
  }
41
56
 
42
57
  start() {
@@ -75,16 +90,22 @@ class SpanEventAggregator extends EventAggregator {
75
90
 
76
91
  return false
77
92
  }
78
- const span = SpanEvent.fromSegment({ segment, transaction, parentId, isRoot, inProcessSpans: this.inProcessSpans })
79
93
 
80
- if (span) {
81
- this.add(span, transaction.priority)
82
- }
94
+ const span = SpanEvent.fromSegment({ segment, transaction, parentId, isRoot, inProcessSpans: this.inProcessSpans })
83
95
 
84
- if (segment.spanLinks.length > 0) {
85
- for (const link of segment.spanLinks) {
86
- this.add(link, transaction.priority)
96
+ // Do not add span to aggregator if it is part of a partial trace
97
+ // We need to reparent spans or in the case of `compact`, associate
98
+ // all exit spans dropped to the same entity as `nr.ids` and `nr.durations`
99
+ if (transaction.partialType) {
100
+ const prefix = `${this._metricNames.PARTIAL_PREFIX}/${transaction.partialType}`
101
+ this._metrics.getOrCreateMetric(prefix).incrementCallCount()
102
+ this._metrics.getOrCreateMetric(`${prefix}${this._metricNames.INSTRUMENTED}`).incrementCallCount()
103
+ transaction.trace.addSpan({ span, id: segment.id, parentId })
104
+ if (span) {
105
+ this._metrics.getOrCreateMetric(`${prefix}${this._metricNames.KEPT}`).incrementCallCount()
87
106
  }
107
+ } else if (span) {
108
+ this.add(span, transaction.priority)
88
109
  }
89
110
  }
90
111
 
@@ -13,6 +13,7 @@ const { addSpanKind, isEntryPointSpan, reparentSpan, shouldCreateSpan, HTTP_LIBR
13
13
  const EMPTY_USER_ATTRS = Object.freeze(Object.create(null))
14
14
  const SERVER_ADDRESS = 'server.address'
15
15
  const logger = require('../logger').child({ component: 'span-event' })
16
+ const { PARTIAL_TYPES } = require('../transaction')
16
17
 
17
18
  /**
18
19
  * This keeps a static list of attributes that are used by
@@ -85,6 +86,10 @@ class SpanEvent {
85
86
  }
86
87
  }
87
88
 
89
+ get parentId() {
90
+ return this.intrinsics.parentId
91
+ }
92
+
88
93
  getIntrinsicAttributes() {
89
94
  return this.intrinsics
90
95
  }
@@ -180,13 +185,13 @@ class SpanEvent {
180
185
  const attributes = span.attributes
181
186
  const attrKeys = Object.keys(attributes)
182
187
  const entityRelationshipAttrs = SPAN_ENTITY_RELATIONSHIP_ATTRIBUTES.filter((item) => attrKeys.includes(item))
183
- if (partialType === 'reduced') {
188
+ if (partialType === PARTIAL_TYPES.REDUCED) {
184
189
  if (entityRelationshipAttrs.length === 0) {
185
190
  logger.trace('Span %s does not contain any entity relationship attributes %j and trace is partial granularity type: %s, dropping span.', span.intrinsics.name, span.attributes, partialType)
186
191
  return null
187
192
  }
188
193
  logger.trace('Span %s contains entity relationship attributes and trace is partial granularity type: %s, keeping span unchanged.', span.intrinsics.name, partialType)
189
- } else if (partialType === 'essential') {
194
+ } else if (partialType === PARTIAL_TYPES.ESSENTIAL) {
190
195
  const attributesToKeep = Object.create(null)
191
196
  for (const item in attributes) {
192
197
  if (entityRelationshipAttrs.includes(item) || item.startsWith('error.')) {
@@ -216,6 +221,7 @@ class SpanEvent {
216
221
  } else {
217
222
  span = new SpanEvent(attributes, customAttributes)
218
223
  }
224
+ span.spanLinks = segment.spanLinks ?? []
219
225
  return span
220
226
  }
221
227
 
@@ -47,6 +47,10 @@ class StreamingSpanEventAggregator extends Aggregator {
47
47
  this.inProcessSpans = agent.config.distributed_tracing.in_process_spans.enabled
48
48
  }
49
49
 
50
+ toString() {
51
+ return 'StreamingSpanEventAggregator'
52
+ }
53
+
50
54
  /**
51
55
  * Start streaming spans to the collector.
52
56
  *
@@ -9,10 +9,12 @@
9
9
  // 'package-name': [ { path: 'subscriberPath', instrumentations: [] }, ... ]
10
10
  const subscribers = {
11
11
  ...require('./subscribers/amqplib/config'),
12
+ ...require('./subscribers/bunyan/config'),
12
13
  ...require('./subscribers/cassandra-driver/config'),
13
14
  ...require('./subscribers/elasticsearch/config'),
14
15
  ...require('./subscribers/express/config'),
15
16
  ...require('./subscribers/fastify/config'),
17
+ ...require('./subscribers/google-genai/config'),
16
18
  ...require('./subscribers/ioredis/config'),
17
19
  ...require('./subscribers/mcp-sdk/config'),
18
20
  ...require('./subscribers/mysql/config'),
@@ -21,6 +21,10 @@ class ApplicationLogsSubscriber extends Subscriber {
21
21
  return super.enabled && isApplicationLoggingEnabled(this.config)
22
22
  }
23
23
 
24
+ isLogForwardingEnabled() {
25
+ return isLogForwardingEnabled(this.config, this.agent)
26
+ }
27
+
24
28
  /**
25
29
  * The intent of this method is to create a module usage metric
26
30
  * but only once. You should call this in your handler.
@@ -48,7 +52,7 @@ class ApplicationLogsSubscriber extends Subscriber {
48
52
  }
49
53
 
50
54
  forwardLogLine(data) {
51
- if (isLogForwardingEnabled(this.config, this.agent)) {
55
+ if (this.isLogForwardingEnabled()) {
52
56
  const ctx = this.agent.tracer.getContext()
53
57
  const formatLogLine = this.reformatLogLine(data, ctx)
54
58
  this.agent.logs.add(formatLogLine)
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const ApplicationLogsSubscriber = require('../application-logs')
8
+
9
+ class BunyanBaseSubscriber extends ApplicationLogsSubscriber {
10
+ NAME_FROM_LEVEL = {
11
+ 10: 'trace',
12
+ 20: 'debug',
13
+ 30: 'info',
14
+ 40: 'warn',
15
+ 50: 'error',
16
+ 60: 'fatal'
17
+ }
18
+
19
+ constructor({ agent, logger, channelName }) {
20
+ super({ agent, logger, packageName: 'bunyan', channelName })
21
+ }
22
+ }
23
+
24
+ module.exports = BunyanBaseSubscriber
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ module.exports = {
7
+ bunyan: [
8
+ {
9
+ path: './bunyan',
10
+ instrumentations: [
11
+ {
12
+ channelName: 'nr_logger',
13
+ module: { name: 'bunyan', versionRange: '>=1.8.12', filePath: 'lib/bunyan.js' },
14
+ functionQuery: {
15
+ functionName: 'Logger',
16
+ kind: 'Sync'
17
+ }
18
+ }
19
+ ]
20
+ },
21
+ {
22
+ path: './bunyan/emit',
23
+ instrumentations: [
24
+ {
25
+ channelName: 'nr_emit',
26
+ module: { name: 'bunyan', versionRange: '>=1.8.12', filePath: 'lib/bunyan.js' },
27
+ functionQuery: {
28
+ expressionName: '_emit',
29
+ kind: 'Sync'
30
+ }
31
+ }
32
+ ]
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const BunyanSubscriber = require('./base')
8
+ const { truncate } = require('../../util/application-logging')
9
+
10
+ class BunyanEmitSubscriber extends BunyanSubscriber {
11
+ constructor({ agent, logger }) {
12
+ super({ agent, logger, channelName: 'nr_emit' })
13
+ }
14
+
15
+ handler(data, ctx) {
16
+ const [line] = data.arguments
17
+ if (!line) {
18
+ return ctx
19
+ }
20
+
21
+ this.incrementLinesMetric(this.NAME_FROM_LEVEL[line.level])
22
+ const decoratedLine = this.decorateLogLine()
23
+ if (decoratedLine) {
24
+ line.message = truncate(line.msg) + decoratedLine
25
+ data.arguments[0] = line
26
+ }
27
+ return ctx
28
+ }
29
+ }
30
+
31
+ module.exports = BunyanEmitSubscriber
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const BunyanSubscriber = require('./base')
8
+ const { truncate } = require('../../util/application-logging')
9
+
10
+ class BunyanLoggerSubscriber extends BunyanSubscriber {
11
+ constructor({ agent, logger }) {
12
+ super({ agent, logger, channelName: 'nr_logger' })
13
+ this.events = ['end']
14
+ }
15
+
16
+ handler(data, ctx) {
17
+ this.createModuleUsageMetric('bunyan')
18
+ return ctx
19
+ }
20
+
21
+ end(data) {
22
+ const self = this
23
+ if (this.isLogForwardingEnabled()) {
24
+ const logger = data.self
25
+ logger.addStream({
26
+ name: 'NRLogForwarder',
27
+ type: 'raw',
28
+ level: logger.level(),
29
+ stream: {
30
+ write: function nrLogWrite(logLine) {
31
+ self.forwardLogLine(logLine)
32
+ }
33
+ }
34
+ })
35
+ }
36
+ }
37
+
38
+ reformatLogLine(logLine) {
39
+ // shallow copy, since we're modifying things
40
+ const newLog = Object.assign({}, logLine)
41
+ newLog.timestamp = Date.now()
42
+ newLog.level = this.NAME_FROM_LEVEL[logLine.level]
43
+
44
+ // put log message into a consistent spot and ensure it's not too long
45
+ newLog.message = truncate(newLog.msg)
46
+
47
+ // tidy up the error output to help with max length restrictions
48
+ if (newLog.err) {
49
+ newLog['error.message'] = truncate(newLog.err.message)
50
+ newLog['error.stack'] = truncate(newLog.err.stack)
51
+ newLog['error.class'] =
52
+ newLog.err.name === 'Error' ? newLog.err.constructor.name : newLog.err.name
53
+ // clear out the old error message
54
+ delete newLog.err
55
+ }
56
+
57
+ // Add the metadata to the object being logged
58
+ const metadata = this.agent.getLinkingMetadata(true)
59
+ return Object.assign({}, newLog, metadata)
60
+ }
61
+ }
62
+
63
+ module.exports = BunyanLoggerSubscriber