Skip to content

Commit ed853ac

Browse files
committed
Merge branch 'NR-2904/gcp-pubsub' of https://github.com/amychisholm03/node-newrelic into NR-2904/gcp-pubsub
2 parents d8185fa + 94c297c commit ed853ac

File tree

12 files changed

+669
-146
lines changed

12 files changed

+669
-146
lines changed

NEWS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
### v12.16.0 (2025-03-17)
2+
3+
#### Features
4+
5+
* Added support for response streaming Lambda functions ([#2981](https://github.com/newrelic/node-newrelic/pull/2981)) ([61dbbf9](https://github.com/newrelic/node-newrelic/commit/61dbbf9b4a6cf69f5378387fc9c17c31671e9da4))
6+
* Added AWS entity linking segment attributes for otel bridge ([#2978](https://github.com/newrelic/node-newrelic/pull/2978)) ([6bf1ccc](https://github.com/newrelic/node-newrelic/commit/6bf1ccc657a955b4064a7a3a473bf24948d4ff56))
7+
* Added error handling on transactions for otel spans ([#2985](https://github.com/newrelic/node-newrelic/pull/2985)) ([4e61e09](https://github.com/newrelic/node-newrelic/commit/4e61e0935394744345c39f6b581ee86e66d0f82c))
8+
9+
#### Code refactoring
10+
11+
* Updated span event generation to assign the appropriate `span.kind` based on the segment name ([#2976](https://github.com/newrelic/node-newrelic/pull/2976)) ([697b17e](https://github.com/newrelic/node-newrelic/commit/697b17e0553111aa494d08bc33eb7043cdfa8ca6))
12+
13+
#### Documentation
14+
15+
* Updated compatibility report ([#2988](https://github.com/newrelic/node-newrelic/pull/2988)) ([ed17a6d](https://github.com/newrelic/node-newrelic/commit/ed17a6df1152a8e54cb8c8570fec0015990a4247)
16+
17+
#### Miscellaneous chores
18+
19+
* Clarified supported next.js middleware versions in docs ([#2984](https://github.com/newrelic/node-newrelic/pull/2984)) ([15cb454](https://github.com/newrelic/node-newrelic/commit/15cb454f9cc38ccc22089d62aaeea54713159aa7))
20+
* Clarified system metrics sampler naming ([#2987](https://github.com/newrelic/node-newrelic/pull/2987)) ([8647d43](https://github.com/newrelic/node-newrelic/commit/8647d43f097d6d3d68a372824d9feb325604be96))
21+
* Refactored loops to be simpler ([#2990](https://github.com/newrelic/node-newrelic/pull/2990)) ([79fb8e9](https://github.com/newrelic/node-newrelic/commit/79fb8e90802954b617c1c00aecc866aa065aee12))
22+
* Removed unused transaction method ([#2986](https://github.com/newrelic/node-newrelic/pull/2986)) ([cb4e2f7](https://github.com/newrelic/node-newrelic/commit/cb4e2f7a8b84adb6d744a2083b083c92e306fbd5))
23+
* Reverted restriction in NestJS versioned tests ([#2979](https://github.com/newrelic/node-newrelic/pull/2979)) ([ffddcab](https://github.com/newrelic/node-newrelic/commit/ffddcab6d77bfc10c0df9cbfa724bc1c8f5fb251))
24+
25+
#### Tests
26+
27+
* Fixed fastify assertions around span kind while running security agent ([#2983](https://github.com/newrelic/node-newrelic/pull/2983)) ([c641645](https://github.com/newrelic/node-newrelic/commit/c6416451f1fa6126b7dfd59f6b9267f9d2188ad0))
28+
129
### v12.15.0 (2025-03-03)
230

331
#### Features

changelog.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
{
22
"repository": "newrelic/node-newrelic",
33
"entries": [
4+
{
5+
"version": "12.16.0",
6+
"changes": {
7+
"security": [],
8+
"bugfixes": [],
9+
"features": [
10+
"Added support for response streaming Lambda functions",
11+
"Added AWS entity linking segment attributes for otel bridge",
12+
"Added error handling on transactions for otel spans"
13+
]
14+
}
15+
},
416
{
517
"version": "12.15.0",
618
"changes": {

compatibility.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@ version.
1414
| `@apollo/gateway` | 2.3.0 | 2.10.0 | `@newrelic/apollo-server-plugin@1.0.0` |
1515
| `@apollo/server` | 4.0.0 | 4.11.3 | `@newrelic/apollo-server-plugin@2.1.0` |
1616
| `@aws-sdk/client-bedrock-runtime` | 3.474.0 | 3.758.0 | 11.13.0 |
17-
| `@aws-sdk/client-dynamodb` | 3.0.0 | 3.758.0 | 8.7.1 |
17+
| `@aws-sdk/client-dynamodb` | 3.0.0 | 3.767.0 | 8.7.1 |
1818
| `@aws-sdk/client-sns` | 3.0.0 | 3.758.0 | 8.7.1 |
1919
| `@aws-sdk/client-sqs` | 3.0.0 | 3.758.0 | 8.7.1 |
20-
| `@aws-sdk/lib-dynamodb` | 3.377.0 | 3.758.0 | 8.7.1 |
20+
| `@aws-sdk/lib-dynamodb` | 3.377.0 | 3.767.0 | 8.7.1 |
2121
| `@aws-sdk/smithy-client` | 3.47.0 | 3.374.0 | 8.7.1 |
2222
| `@elastic/elasticsearch` | 7.16.0 | 8.17.1 | 11.9.0 |
23-
| `@grpc/grpc-js` | 1.4.0 | 1.12.6 | 8.17.0 |
24-
| `@hapi/hapi` | 20.1.2 | 21.3.12 | 9.0.0 |
23+
| `@grpc/grpc-js` | 1.4.0 | 1.13.0 | 8.17.0 |
24+
| `@hapi/hapi` | 20.1.2 | 21.4.0 | 9.0.0 |
2525
| `@koa/router` | 11.0.2 | 13.1.0 | 3.2.0 |
2626
| `@langchain/core` | 0.1.17 | 0.3.42 | 11.13.0 |
2727
| `@nestjs/cli` | 9.0.0 | 11.0.5 | 10.1.0 |
2828
| `@opensearch-project/opensearch` | 2.1.0 | 3.4.0 | 12.10.0 |
29-
| `@prisma/client` | 5.0.0 | 6.4.1 | 11.0.0 |
29+
| `@prisma/client` | 5.0.0 | 6.5.0 | 11.0.0 |
3030
| `@smithy/smithy-client` | 2.0.0 | 4.1.6 | 11.0.0 |
3131
| `amqplib` | 0.5.0 | 0.10.5 | 2.0.0 |
3232
| `apollo-server` | 3.0.0 | 3.13.0 | `@newrelic/apollo-server-plugin@1.0.0` |
@@ -39,25 +39,25 @@ version.
3939
| `express` | 4.6.0 | 4.21.2 | 2.6.0 |
4040
| `fastify` | 2.0.0 | 5.2.1 | 8.5.0 |
4141
| `generic-pool` | 3.0.0 | 3.9.0 | 0.9.0 |
42-
| `ioredis` | 4.0.0 | 5.5.0 | 1.26.2 |
42+
| `ioredis` | 4.0.0 | 5.6.0 | 1.26.2 |
4343
| `kafkajs` | 2.0.0 | 2.2.4 | 11.19.0 |
4444
| `koa` | 2.0.0 | 2.16.0 | 3.2.0 |
4545
| `koa-route` | 3.0.0 | 4.0.1 | 3.2.0 |
4646
| `koa-router` | 11.0.2 | 13.0.1 | 3.2.0 |
4747
| `memcached` | 2.2.0 | 2.2.2 | 1.26.2 |
48-
| `mongodb` | 4.1.4 | 6.14.1 | 1.32.0 |
48+
| `mongodb` | 4.1.4 | 6.14.2 | 1.32.0 |
4949
| `mysql` | 2.2.0 | 2.18.1 | 1.32.0 |
50-
| `mysql2` | 2.0.0 | 3.12.0 | 1.32.0 |
51-
| `next` | 13.4.19 | 15.2.1 | 12.0.0 |
52-
| `openai` | 4.0.0 | 4.86.1 | 11.13.0 |
53-
| `pg` | 8.2.0 | 8.13.3 | 9.0.0 |
54-
| `pg-native` | 3.0.0 | 3.2.2 | 9.0.0 |
50+
| `mysql2` | 2.0.0 | 3.13.0 | 1.32.0 |
51+
| `next` | 13.4.19 | 15.2.2 | 12.0.0 |
52+
| `openai` | 4.0.0 | 4.87.3 | 11.13.0 |
53+
| `pg` | 8.2.0 | 8.14.1 | 9.0.0 |
54+
| `pg-native` | 3.0.0 | 3.3.0 | 9.0.0 |
5555
| `pino` | 7.0.0 | 9.6.0 | 8.11.0 |
5656
| `q` | 1.3.0 | 1.5.1 | 1.26.2 |
5757
| `redis` | 3.1.0 | 4.7.0 | 1.31.0 |
5858
| `restify` | 11.0.0 | 11.1.0 | 2.6.0 |
59-
| `superagent` | 3.0.0 | 10.1.1 | 4.9.0 |
60-
| `undici` | 5.0.0 | 7.4.0 | 11.1.0 |
59+
| `superagent` | 3.0.0 | 10.2.0 | 4.9.0 |
60+
| `undici` | 5.0.0 | 7.5.0 | 11.1.0 |
6161
| `when` | 3.7.0 | 3.7.8 | 1.26.2 |
6262
| `winston` | 3.0.0 | 3.17.0 | 8.11.0 |
6363

lib/agent.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const MetricNormalizer = require('./metrics/normalizer')
2121
const MetricAggregator = require('./metrics/metric-aggregator')
2222
const NAMES = require('./metrics/names')
2323
const QueryTraceAggregator = require('./db/query-trace-aggregator')
24-
const sampler = require('./sampler')
24+
const systemMetricsSampler = require('./system-metrics-sampler')
2525
const TransactionTraceAggregator = require('./transaction/trace/aggregator')
2626
const TransactionEventAggregator = require('./transaction/transaction-event-aggregator')
2727
const Tracer = require('./transaction/tracer')
@@ -334,7 +334,7 @@ Agent.prototype.start = function start(callback) {
334334
return process.nextTick(callback)
335335
}
336336

337-
sampler.start(agent)
337+
systemMetricsSampler.start(agent)
338338

339339
if (this.config.serverless_mode.enabled) {
340340
return this._serverlessModeStart(callback)
@@ -349,7 +349,7 @@ Agent.prototype.start = function start(callback) {
349349
this.healthReporter.setStatus(HealthReporter.STATUS_LICENSE_KEY_MISSING)
350350

351351
this.setState('errored')
352-
sampler.stop()
352+
systemMetricsSampler.stop()
353353
return process.nextTick(function onNextTick() {
354354
agent.healthReporter.stop(() => {
355355
callback(new Error('Not starting without license key!'))
@@ -362,7 +362,7 @@ Agent.prototype.start = function start(callback) {
362362
if (error || response.shouldShutdownRun()) {
363363
agent.healthReporter.setStatus(HealthReporter.STATUS_CONNECT_ERROR)
364364
agent.setState('errored')
365-
sampler.stop()
365+
systemMetricsSampler.stop()
366366
callback(error || new Error('Failed to connect to collector'), response && response.payload)
367367
return
368368
}
@@ -482,7 +482,7 @@ Agent.prototype.stop = function stop(callback) {
482482

483483
this.harvester.stop()
484484

485-
sampler.stop()
485+
systemMetricsSampler.stop()
486486

487487
this.healthReporter.setStatus(HealthReporter.STATUS_AGENT_SHUTDOWN)
488488
this.healthReporter.stop(() => {

lib/serverless/aws-lambda.js

Lines changed: 119 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const EVENT_SOURCE_TYPE_KEY = `${EVENT_SOURCE_PREFIX}.eventType`
2424
const NAMES = require('../metrics/names')
2525

2626
const EVENT_SOURCE_INFO = require('./event-sources')
27+
const HANDLER_STREAMING = Symbol.for('aws.lambda.runtime.handler.streaming')
2728

2829
// A function with no references used to stub out closures
2930
function cleanClosure() {}
@@ -35,7 +36,7 @@ let transactionEnders = []
3536
// the invocation transaction.
3637
let uncaughtException = null
3738

38-
// Tracking the first time patchLambdaHandler is called for one off functionality
39+
// Tracking the first time patchLambdaHandler is called for one-off functionality
3940
let patchCalled = false
4041
let coldStartRecorded = false
4142

@@ -105,6 +106,45 @@ class AwsLambda {
105106
})
106107
}
107108

109+
/**
110+
* Response-streaming handlers are identified by symbol properties on the function.
111+
* We propagate any symbols if they're present, so that the handler keeps its signatue for any AWS features that rely on symbols
112+
* @param handler
113+
* @param nrHandler
114+
*/
115+
propagateSymbols(handler, nrHandler) {
116+
for (const symbol of Object.getOwnPropertySymbols(handler)) {
117+
logger.trace(`Setting symbol ${symbol.toString()} on handler`)
118+
nrHandler[symbol] = handler[symbol]
119+
}
120+
}
121+
122+
createSegment({ event, context, transaction, recorder }) {
123+
const shim = this.shim
124+
const functionName = context.functionName
125+
const group = NAMES.FUNCTION.PREFIX
126+
const transactionName = group + functionName
127+
128+
const activeSegment = shim.tracer.getSegment()
129+
130+
transaction.setPartialName(transactionName)
131+
const txnEnder = endTransaction.bind(null, transaction, transactionEnders.length)
132+
133+
transactionEnders.push(txnEnder)
134+
const segment = shim.createSegment(functionName, recorder, activeSegment)
135+
transaction.baseSegment = segment
136+
const awsAttributes = this._getAwsAgentAttributes(event, context)
137+
transaction.trace.attributes.addAttributes(ATTR_DEST.TRANS_COMMON, awsAttributes)
138+
139+
shim.agent.setLambdaArn(context.invokedFunctionArn)
140+
141+
shim.agent.setLambdaFunctionVersion(context.functionVersion)
142+
segment.addSpanAttributes(awsAttributes)
143+
144+
segment.start()
145+
return { segment, txnEnder }
146+
}
147+
108148
patchLambdaHandler(handler) {
109149
const awsLambda = this
110150
const shim = this.shim
@@ -114,6 +154,11 @@ class AwsLambda {
114154
return handler
115155
}
116156

157+
const isStreamHandler = handler[HANDLER_STREAMING] === 'response'
158+
if (isStreamHandler) {
159+
this.agent.recordSupportability('Nodejs/Serverless/Lambda/ResponseStreaming')
160+
}
161+
117162
if (!patchCalled) {
118163
// Only wrap emit on process the first time patch is called.
119164
patchCalled = true
@@ -122,52 +167,87 @@ class AwsLambda {
122167
this.wrapFatal()
123168
}
124169

125-
return shim.bindCreateTransaction(wrappedHandler, new specs.TransactionSpec({ type: shim.BG }))
170+
const wrapper = isStreamHandler ? wrappedStreamHandler : wrappedHandler
171+
const nrHandler = shim.bindCreateTransaction(wrapper, new specs.TransactionSpec({ type: shim.BG }))
172+
awsLambda.propagateSymbols(handler, nrHandler)
173+
174+
return nrHandler
175+
176+
/**
177+
* Wraps a response streaming lambda handler.
178+
*
179+
* Creates and applies segment based on function name, assigns attributes to transaction trace,
180+
* listen when stream errors(log error), ends(end transaction)
181+
*
182+
* **Note**: AWS doesn't support response streaming with API gateway invoked lambdas.
183+
* This means we do not handle that as it would require intercepting the stream to parse
184+
* the response code and headers.
185+
*/
186+
function wrappedStreamHandler() {
187+
const transaction = shim.tracer.getTransaction()
188+
if (!transaction) {
189+
logger.trace('No active transaction, not wrapping streaming handler')
190+
return handler.apply(this, arguments)
191+
}
126192

127-
function wrappedHandler() {
128193
const args = shim.argsToArray.apply(shim, arguments)
129-
130194
const event = args[0]
131-
const context = args[1]
195+
const context = args[2]
196+
logger.trace('In stream handler, lambda function name', context?.functionName)
197+
const { segment, txnEnder } = awsLambda.createSegment({ context, event, transaction, recorder: recordBackground })
198+
args[1] = awsLambda.wrapStreamAndCaptureError(
199+
transaction,
200+
txnEnder,
201+
args[1]
202+
)
132203

133-
const functionName = context.functionName
134-
const group = NAMES.FUNCTION.PREFIX
135-
const transactionName = group + functionName
204+
let res
205+
try {
206+
res = shim.applySegment(handler, segment, false, this, args)
207+
} catch (err) {
208+
uncaughtException = err
209+
txnEnder()
210+
throw err
211+
}
212+
213+
return res
214+
}
136215

216+
/**
217+
* Wraps a non response streaming lambda handler.
218+
*
219+
* Creates and applies segment based on function name, assigns attributes to transaction trace,
220+
* adds handlers if api gateway to wrap request/response
221+
* wraps the callback(if present), wraps the context `done`, `succeed`, `fail methods`, intercepts promise
222+
* and properly ends transaction
223+
*/
224+
function wrappedHandler() {
137225
const transaction = shim.tracer.getTransaction()
138226
if (!transaction) {
227+
logger.trace('No active transaction, not wrapping handler')
139228
return handler.apply(this, arguments)
140229
}
141-
const activeSegment = shim.tracer.getSegment()
142-
143-
transaction.setPartialName(transactionName)
230+
const args = shim.argsToArray.apply(shim, arguments)
144231

232+
const event = args[0]
233+
const context = args[1]
234+
logger.trace('Lambda function name', context?.functionName)
145235
const isApiGatewayLambdaProxy = apiGateway.isLambdaProxyEvent(event)
236+
logger.trace('Is this Lambda event an API Gateway or ALB web proxy?', isApiGatewayLambdaProxy)
237+
logger.trace('Lambda event keys', Object.keys(event))
146238
const segmentRecorder = isApiGatewayLambdaProxy ? recordWeb : recordBackground
147-
const segment = shim.createSegment(functionName, segmentRecorder, activeSegment)
148-
transaction.baseSegment = segment
239+
const { segment, txnEnder } = awsLambda.createSegment({ context, event, transaction, recorder: segmentRecorder })
240+
149241
// resultProcessor is used to execute additional logic based on the
150242
// payload supplied to the callback.
151243
let resultProcessor
152244

153-
logger.trace('Is this Lambda event an API Gateway or ALB web proxy?', isApiGatewayLambdaProxy)
154-
logger.trace('Lambda event keys', Object.keys(event))
155-
156245
if (isApiGatewayLambdaProxy) {
157246
const webRequest = new apiGateway.LambdaProxyWebRequest(event)
158247
setWebRequest(shim, transaction, webRequest)
159248
resultProcessor = getApiGatewayLambdaProxyResultProcessor(transaction)
160249
}
161-
162250
const cbIndex = args.length - 1
163-
164-
// Add transaction ending closure to the list of functions to be called on
165-
// beforeExit (i.e. in the case that context.{done,fail,succeed} or callback
166-
// were not called).
167-
const txnEnder = endTransaction.bind(null, transaction, transactionEnders.length)
168-
169-
transactionEnders.push(txnEnder)
170-
171251
args[cbIndex] = awsLambda.wrapCallbackAndCaptureError(
172252
transaction,
173253
txnEnder,
@@ -186,16 +266,6 @@ class AwsLambda {
186266
}
187267
})
188268

189-
const awsAttributes = awsLambda._getAwsAgentAttributes(event, context)
190-
transaction.trace.attributes.addAttributes(ATTR_DEST.TRANS_COMMON, awsAttributes)
191-
192-
shim.agent.setLambdaArn(context.invokedFunctionArn)
193-
194-
shim.agent.setLambdaFunctionVersion(context.functionVersion)
195-
segment.addSpanAttributes(awsAttributes)
196-
197-
segment.start()
198-
199269
let res
200270
try {
201271
res = shim.applySegment(handler, segment, false, this, args)
@@ -251,6 +321,18 @@ class AwsLambda {
251321
}
252322
}
253323

324+
wrapStreamAndCaptureError(transaction, txnEnder, stream) {
325+
const shim = this.shim
326+
stream.on('error', (error) => {
327+
shim.agent.errors.add(transaction, error)
328+
})
329+
330+
stream.on('close', () => {
331+
txnEnder()
332+
})
333+
return stream
334+
}
335+
254336
_getAwsAgentAttributes(event, context) {
255337
const attributes = {
256338
'aws.lambda.arn': context.invokedFunctionArn,
@@ -372,7 +454,7 @@ function lowercaseObjectKeys(original) {
372454
}
373455

374456
function endTransaction(transaction, enderIndex) {
375-
if (transactionEnders[enderIndex] === cleanClosure) {
457+
if (transactionEnders.length === 0 || transactionEnders[enderIndex] === cleanClosure) {
376458
// In the case where we have already been called, we return early. There may be a
377459
// case where this is called more than once, given the lambda is left in a dirty
378460
// state after thread suspension (e.g. timeouts)
@@ -411,7 +493,7 @@ function setWebResponse(transaction, response) {
411493

412494
// We are adding http.statusCode to base segment as
413495
// we found in testing async invoked lambdas, the
414-
// active segement is not available at this point.
496+
// active segment is not available at this point.
415497
const segment = transaction.baseSegment
416498

417499
segment.addSpanAttribute('http.statusCode', responseCode)

0 commit comments

Comments
 (0)