Skip to content

Commit b31b501

Browse files
authored
Add a new ESM lambda wrapper (#319)
1 parent c3e2d64 commit b31b501

20 files changed

+8201
-7580
lines changed

.github/workflows/publish-node.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
env:
3232
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
3333
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
34-
run: make publish-nodejs${{ matrix.node-version }}x-ci
34+
run: make publish-nodejs${{ matrix.node-version }}-ci
3535
- name: Set up QEMU
3636
uses: docker/setup-qemu-action@v3
3737
with:
@@ -43,7 +43,7 @@ jobs:
4343
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
4444
run: |
4545
cd nodejs
46-
./publish-layers.sh build-publish-nodejs${{ matrix.node-version }}x-ecr-image
46+
./publish-layers.sh build-publish-{{ matrix.node-version }}-ecr-image
4747
- name: Upload Unit Test Coverage
4848
if: steps.node-check-tag.outputs.match == 'true'
4949
uses: codecov/[email protected]

Makefile

+21-21
Original file line numberDiff line numberDiff line change
@@ -74,62 +74,62 @@ publish-java21-local: build-java21
7474
-v "${HOME}/.aws:/home/newrelic-lambda-layers/.aws" \
7575
newrelic-lambda-layers-java21
7676

77-
build-nodejs18x:
77+
build-nodejs18:
7878
docker build \
7979
--no-cache \
80-
-t newrelic-lambda-layers-nodejs18x \
81-
-f ./dockerfiles/Dockerfile.nodejs18x \
80+
-t newrelic-lambda-layers-nodejs18 \
81+
-f ./dockerfiles/Dockerfile.nodejs18 \
8282
.
8383

84-
publish-nodejs18x-ci: build-nodejs18x
84+
publish-nodejs18-ci: build-nodejs18
8585
docker run \
8686
-e AWS_ACCESS_KEY_ID \
8787
-e AWS_SECRET_ACCESS_KEY \
88-
newrelic-lambda-layers-nodejs18x
88+
newrelic-lambda-layers-nodejs18
8989

90-
publish-nodejs18x-local: build-nodejs18x
90+
publish-nodejs18-local: build-nodejs18
9191
docker run \
9292
-e AWS_PROFILE \
9393
-v "${HOME}/.aws:/home/newrelic-lambda-layers/.aws" \
94-
newrelic-lambda-layers-nodejs18x
94+
newrelic-lambda-layers-nodejs18
9595

96-
build-nodejs20x:
96+
build-nodejs20:
9797
docker build \
9898
--no-cache \
99-
-t newrelic-lambda-layers-nodejs20x \
100-
-f ./dockerfiles/Dockerfile.nodejs20x \
99+
-t newrelic-lambda-layers-nodejs20 \
100+
-f ./dockerfiles/Dockerfile.nodejs20 \
101101
.
102102

103-
publish-nodejs20x-ci: build-nodejs20x
103+
publish-nodejs20-ci: build-nodejs20
104104
docker run \
105105
-e AWS_ACCESS_KEY_ID \
106106
-e AWS_SECRET_ACCESS_KEY \
107-
newrelic-lambda-layers-nodejs20x
107+
newrelic-lambda-layers-nodejs20
108108

109-
publish-nodejs20x-local: build-nodejs20x
109+
publish-nodejs20-local: build-nodejs20
110110
docker run \
111111
-e AWS_PROFILE \
112112
-v "${HOME}/.aws:/home/newrelic-lambda-layers/.aws" \
113-
newrelic-lambda-layers-nodejs20x
113+
newrelic-lambda-layers-nodejs20
114114

115-
build-nodejs22x:
115+
build-nodejs22:
116116
docker build \
117117
--no-cache \
118-
-t newrelic-lambda-layers-nodejs22x \
119-
-f ./dockerfiles/Dockerfile.nodejs22x \
118+
-t newrelic-lambda-layers-nodejs22 \
119+
-f ./dockerfiles/Dockerfile.nodejs22 \
120120
.
121121

122-
publish-nodejs22x-ci: build-nodejs22x
122+
publish-nodejs22-ci: build-nodejs22
123123
docker run \
124124
-e AWS_ACCESS_KEY_ID \
125125
-e AWS_SECRET_ACCESS_KEY \
126-
newrelic-lambda-layers-nodejs22x
126+
newrelic-lambda-layers-nodejs22
127127

128-
publish-nodejs22x-local: build-nodejs22x
128+
publish-nodejs22-local: build-nodejs22
129129
docker run \
130130
-e AWS_PROFILE \
131131
-v "${HOME}/.aws:/home/newrelic-lambda-layers/.aws" \
132-
newrelic-lambda-layers-nodejs22x
132+
newrelic-lambda-layers-nodejs22
133133

134134
build-ruby32:
135135
docker build \

README.md

+9-35
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ cd ..
2626

2727
```
2828
cd nodejs
29-
./publish-layers.sh nodejs20x
29+
./publish-layers.sh nodejs20
3030
cd ..
3131
```
3232

@@ -78,7 +78,9 @@ These steps will help you configure the layers correctly:
7878
* Using Cloudformation, this refers to adding your layer arn to the Layers property of a [AWS::Lambda::Function resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html).
7979
3. Update your functions handler to point to the newly attached layer in the console for your function:
8080
* Python: `newrelic_lambda_wrapper.handler`
81-
* Node: `newrelic-lambda-wrapper.handler`
81+
* Node:
82+
* CommonJS: `newrelic-lambda-wrapper.handler`
83+
* ESM: `/opt/nodejs/node_modules/newrelic-esm-lambda-wrapper/index.handler` - You must specify the full path to the wrapper in the layer because the AWS Lambda Runtime doesn't support importing from a layer.
8284
* Ruby: `newrelic_lambda_wrapper.handler`
8385
* Java:
8486
* RequestHandler implementation: `com.newrelic.java.HandlerWrapper::handleRequest`
@@ -97,42 +99,14 @@ Refer to the [New Relic AWS Lambda Monitoring Documentation](https://docs.newrel
9799

98100
## Support for ES Modules (Node.js)
99101

100-
AWS announced support for Node 18 as a Lambda runtime in late 2022, introducing `aws-sdk` version 3 for Node 18 only. This version of `aws-sdk` patches `NODE_PATH`, so ESM-supporting functions using `import` and top-level `await` should work as expected with Lambda Layer releases `v9.8.1.1` and above (Numerical layer versions vary by region and runtime). To configure the layer to leverage `import`, add the environment variable `NEW_RELIC_USE_ESM: true`, and add this environment variable to use our ESM loader: `NODE_OPTIONS: --experimental-loader newrelic/esm-loader.mjs`.
102+
AWS announced support for Node 18 as a Lambda runtime in late 2022, introducing `aws-sdk` version 3 for Node 18 only. This version of `aws-sdk` patches `NODE_PATH`, so ESM-supporting functions using `import` and top-level `await` should work as expected with Lambda Layer releases `v9.8.1.1` and above (Numerical layer versions vary by region and runtime). To configure the layer to leverage `import`, add this environment variable to use our ESM loader: `NODE_OPTIONS: --experimental-loader newrelic/esm-loader.mjs`. **Note**: The function handler should also use `newrelic-esm-lambda-wrapper.handler`.
101103

102-
Note that if you use layer-installed instrumentation with the `NEW_RELIC_USE_ESM` environment variable, your function must use promises or async/await; callback based functions are not supported. The Node wrapper uses a [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) to attach to your function, which is an asynchronous operation. If you still need support for callback based functions, you will have to use the CommonJS based wrapper, which can be done by removing the `NEW_RELIC_USE_ESM` environment variable.
104+
### Note on performance for ES Module functions
103105

104-
You may see some warnings from the Extension in CloudWatch logs referring to a non-standard handler; these warnings may be ignored.
106+
If you are using the legacy `NEW_RELIC_USE_ESM` flag on the `newrelic-lambda-wrapper.handler`, and your ESM function depends on a large number of dependency and file imports, you may see long cold start times as a result. As a workaround, we recommend using the ESM wrapper. Set your handler to `/opt/nodejs/node_modules/newrelic-esm-lambda-wrapper/index.handler`.
105107

106-
If your Node functions use `import` and top-level `await` in Node 16 or Node 14 runtimes, layer-installed instrumentation will be unable to find imported modules, as [`import` specifiers don't resolve with `NODE_PATH`](https://nodejs.org/docs/latest-v16.x/api/esm.html#no-node_path). You can still instrument your functions with New Relic, but you will need to do the following:
107-
108-
1. [instrument your function manually](#manual-instrumentation-for-es-modules) using our [Node Agent](https://github.com/newrelic/node-newrelic/)
109-
2. On deploying your function, don't set the function handler to our Node wrapper; instead, use your regular handler function, which you've wrapped with `newrelic.setLambdaHandler()`.
110-
3. If you're using Node 18 or above, apply the latest Lambda Layer for your runtime. It will install both the Node agent and our Lambda Extension.
111-
4. If you're using Node 14 or Node 16, you will have to deploy our agent with your function code, but you could use our Extension-only Lambda Layer for delivering telemetry. Use our [layer discovery website](https://layers.newrelic-external.com/) to find the ARN for your region. Look for either NewRelicLambdaExtension or NewRelicLambdaExtensionARM64 (depending on your function's architecture).
112-
4. Add your `NEW_RELIC_LICENSE_KEY` as an environment variable.
113-
114-
## Note on performance for ES Module functions
115-
116-
In order to wrap ESM functions without a code change, our wrapper awaits the completion of a dynamic import. If your ESM function depends on a large number of dependency and file imports, you may see long cold start times as a result. As a workaround, we recommend instrumenting manually, following the instructions below.
117-
118-
## Manual instrumentation for ES Modules
119-
120-
First import the New Relic Node agent into your handler file:
121-
122-
```javascript
123-
import newrelic from 'newrelic'
124-
```
125-
126-
Then wrap your handler function using the `.setLambdaHandler` method:
127-
```javascript
128-
export const handler = newrelic.setLambdaHandler(async (event, context) => {
129-
// TODO implement
130-
return {
131-
statusCode: 200,
132-
body: JSON.stringify('Hello from Lambda!')
133-
}
134-
})
135-
```
108+
### Note on legacy `NEW_RELIC_USE_ESM` environment variable
109+
Prior to Lambda Layer releases `v12.16.0`, the wrapper for CommonJS and ESM were the same. If you wanted to wrap a ESM lambda handler you had to set `NEW_RELIC_USE_ESM` to `true`. This functionality still exists but has been deprecated. If you have a ESM lambda handler set the function handler to point to `newrelic-esm-lambda-wrapper.handler`. We will be removing `NEW_RELIC_USE_ESM` at a future date.
136110

137111
## Support
138112

dockerfiles/Dockerfile.nodejs18x renamed to dockerfiles/Dockerfile.nodejs18

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ COPY --chown=newrelic-lambda-layers libBuild.sh .
1111
COPY --chown=newrelic-lambda-layers nodejs nodejs/
1212

1313
WORKDIR nodejs
14-
RUN ./publish-layers.sh build-nodejs18x
14+
RUN ./publish-layers.sh build-18
1515

1616
FROM python:3.8
1717

@@ -26,4 +26,4 @@ COPY nodejs nodejs/
2626
COPY --from=builder /home/newrelic-lambda-layers/nodejs/dist nodejs/dist
2727

2828
WORKDIR nodejs
29-
CMD ./publish-layers.sh publish-nodejs18x
29+
CMD ./publish-layers.sh publish-18

dockerfiles/Dockerfile.nodejs20x renamed to dockerfiles/Dockerfile.nodejs20

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ COPY --chown=newrelic-lambda-layers libBuild.sh .
1111
COPY --chown=newrelic-lambda-layers nodejs nodejs/
1212

1313
WORKDIR nodejs
14-
RUN ./publish-layers.sh build-nodejs20x
14+
RUN ./publish-layers.sh build-20
1515

1616
FROM python:3.8
1717

@@ -26,4 +26,4 @@ COPY nodejs nodejs/
2626
COPY --from=builder /home/newrelic-lambda-layers/nodejs/dist nodejs/dist
2727

2828
WORKDIR nodejs
29-
CMD ./publish-layers.sh publish-nodejs20x
29+
CMD ./publish-layers.sh publish-20

dockerfiles/Dockerfile.nodejs22x renamed to dockerfiles/Dockerfile.nodejs22

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ COPY --chown=newrelic-lambda-layers libBuild.sh .
1111
COPY --chown=newrelic-lambda-layers nodejs nodejs/
1212

1313
WORKDIR nodejs
14-
RUN ./publish-layers.sh build-nodejs22x
14+
RUN ./publish-layers.sh build-22
1515

1616
FROM python:3.8
1717

@@ -26,4 +26,4 @@ COPY nodejs nodejs/
2626
COPY --from=builder /home/newrelic-lambda-layers/nodejs/dist nodejs/dist
2727

2828
WORKDIR nodejs
29-
CMD ./publish-layers.sh publish-nodejs22x
29+
CMD ./publish-layers.sh publish-22

nodejs/esm.mjs

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import newrelic from 'newrelic'
2+
import fs from 'node:fs'
3+
import path from 'node:path'
4+
5+
process.env.NEW_RELIC_APP_NAME = process.env.NEW_RELIC_APP_NAME || process.env.AWS_LAMBDA_FUNCTION_NAME
6+
process.env.NEW_RELIC_DISTRIBUTED_TRACING_ENABLED = process.env.NEW_RELIC_DISTRIBUTED_TRACING_ENABLED || 'true'
7+
process.env.NEW_RELIC_NO_CONFIG_FILE = process.env.NEW_RELIC_NO_CONFIG_FILE || 'true'
8+
process.env.NEW_RELIC_TRUSTED_ACCOUNT_KEY =
9+
process.env.NEW_RELIC_TRUSTED_ACCOUNT_KEY || process.env.NEW_RELIC_ACCOUNT_ID
10+
11+
if (process.env.LAMBDA_TASK_ROOT && typeof process.env.NEW_RELIC_SERVERLESS_MODE_ENABLED !== 'undefined') {
12+
delete process.env.NEW_RELIC_SERVERLESS_MODE_ENABLED
13+
}
14+
15+
function getHandlerPath() {
16+
let handler
17+
const { NEW_RELIC_LAMBDA_HANDLER } = process.env
18+
19+
if (!NEW_RELIC_LAMBDA_HANDLER) {
20+
throw new Error('No NEW_RELIC_LAMBDA_HANDLER environment variable set.')
21+
} else {
22+
handler = NEW_RELIC_LAMBDA_HANDLER
23+
}
24+
25+
const parts = handler.split('.')
26+
27+
if (parts.length < 2) {
28+
throw new Error(
29+
`Improperly formatted handler environment variable: ${handler}`
30+
)
31+
}
32+
33+
const handlerToWrap = parts[parts.length - 1]
34+
const moduleToImport = handler.slice(0, handler.lastIndexOf('.'))
35+
return { moduleToImport, handlerToWrap }
36+
}
37+
38+
function handleRequireImportError(e, moduleToImport) {
39+
if (e.code === 'MODULE_NOT_FOUND') {
40+
return new Error(`Unable to import module '${moduleToImport}'`)
41+
}
42+
return e
43+
}
44+
45+
function getFullyQualifiedModulePath(modulePath, extensions) {
46+
let fullModulePath
47+
48+
extensions.forEach((extension) => {
49+
const filePath = modulePath + extension
50+
if (fs.existsSync(filePath)) {
51+
fullModulePath = filePath
52+
return
53+
}
54+
})
55+
56+
if (!fullModulePath) {
57+
throw new Error(
58+
`Unable to resolve module file at ${modulePath} with the following extensions: ${extensions.join(',')}`
59+
)
60+
}
61+
62+
return fullModulePath
63+
}
64+
65+
async function getModuleWithImport(appRoot, moduleToImport) {
66+
const modulePath = path.resolve(appRoot, moduleToImport)
67+
const validExtensions = ['.mjs', '.js']
68+
const fullModulePath = getFullyQualifiedModulePath(modulePath, validExtensions)
69+
70+
try {
71+
return await import(fullModulePath)
72+
} catch (err) {
73+
throw handleRequireImportError(err, moduleToImport)
74+
}
75+
}
76+
77+
function validateHandlerDefinition(userHandler, handlerName, moduleName) {
78+
if (typeof userHandler === 'undefined') {
79+
throw new Error(
80+
`Handler '${handlerName}' missing on module '${moduleName}'`
81+
)
82+
}
83+
84+
if (typeof userHandler !== 'function') {
85+
throw new Error(
86+
`Handler '${handlerName}' from '${moduleName}' is not a function`
87+
)
88+
}
89+
}
90+
91+
const { LAMBDA_TASK_ROOT = '.' } = process.env
92+
const { moduleToImport, handlerToWrap } = getHandlerPath()
93+
94+
const userHandler = await getHandler()
95+
const handler = newrelic.setLambdaHandler(userHandler)
96+
97+
async function getHandler() {
98+
const userHandler = (await getModuleWithImport(LAMBDA_TASK_ROOT, moduleToImport))[handlerToWrap]
99+
validateHandlerDefinition(userHandler, handlerToWrap, moduleToImport)
100+
101+
return userHandler
102+
}
103+
104+
export { handler, getHandlerPath }
105+

nodejs/index.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,17 @@ async function patchHandler() {
135135
return patchedHandlerPromise
136136
.then(_wrappedHandler => _wrappedHandler.apply(this, args))
137137
}
138-
function patchHandlerSync() {
139-
const args = Array.prototype.slice.call(arguments)
140-
return wrappedHandler.apply(this, args)
138+
139+
let handler
140+
141+
if (process.env.NEW_RELIC_USE_ESM === 'true') {
142+
handler = patchHandler
143+
} else {
144+
handler = wrappedHandler
141145
}
142146

147+
143148
module.exports = {
144-
handler: process.env.NEW_RELIC_USE_ESM === 'true' ? patchHandler : patchHandlerSync,
149+
handler,
145150
getHandlerPath
146151
}

0 commit comments

Comments
 (0)