newrelic / newrelic-lambda-layers

Source code and utilities to build and publish New Relic's public AWS Lambda layers.
https://newrelic.com/products/serverless-aws-lambda
Apache License 2.0
34 stars 42 forks source link

Support for ESM Modules #97

Closed anthonyc4 closed 1 year ago

anthonyc4 commented 2 years ago

Description

good morning....

We are using New Relic's Lambda Extension (layer) to report metrics back to NR. Everything is working great....

We now want to migrate our Node14 Lambdas from commonjs to esm . The lambdas are working well as ESM but as soon as I change the handler to newrelic-lambda-wrapper.handler to go through the New Relic Lambda Extension instead of directly (testEsm.default), the lambda fails.

Judging by the errors, it seems like it is using a cjs loader to try to load the default handler testEsm.default for instance and it says it does not exist.

Has anyone run into this and is there a way to use the New Relic Lambda Extension with ESM build NodeJs14 modules?

If this is the wrong channel, please let me know....

Steps to Reproduce

In cloudFormation when deploying, you will receive Provisioned Concurrency configuration failed to be applied. Reason: FUNCTION_ERROR_INIT_FAILURE. If you do this manually via the console, you will see

We are using this layer: arn:aws:lambda:us-east-1:451483290750:layer:NewRelicNodeJS14X:42

Expected Behavior

The default entry point should work through the Extension as it does if specified directly as the Handler. Changing the handler to testEsm.default in our case the lambda works when testing on the console.

Relevant Logs / Console output

Test Event Name
test

Response
{
  "errorType": "Error",
  "errorMessage": "Unable to import module 'testEsm'",
  "trace": [
    "Error: Unable to import module 'testEsm'",
    "    at getHandler (/opt/nodejs/node_modules/newrelic-lambda-wrapper/index.js:40:13)",
    "    at Object.<anonymous> (/opt/nodejs/node_modules/newrelic-lambda-wrapper/index.js:62:50)",
    "    at Module._compile (internal/modules/cjs/loader.js:1085:14)",
    "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)",
    "    at Module.load (internal/modules/cjs/loader.js:950:32)",
    "    at Function.Module._load (internal/modules/cjs/loader.js:790:12)",
    "    at Module.require (internal/modules/cjs/loader.js:974:19)",
    "    at require (internal/modules/cjs/helpers.js:101:18)",
    "    at _tryRequire (/var/runtime/UserFunction.js:186:10)",
    "    at _loadUserApp (/var/runtime/UserFunction.js:197:12)"
  ]
}

Your Environment

Additional context

NOTE: logger/logger is code to add data to console.log in a structured format. It is bundled with esbuild when deploying the lambda.

import { Handler, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { logger } from '../../logger/logger';
import { StatusCodes } from 'http-status-codes';

logger.addDefaultTags(['testEsm']);

// await a function here
const fn = async () => {
    logger.log({
        message: 'Starting fake sleep',
        meta: {}
    });

    return new Promise((resolve) => {
        logger.log({
            message: 'Resolving Promise',
            meta: {}
        });

        setTimeout(resolve, 3000);
    });
};

logger.log({
    message: 'Starting Await',
    meta: {}
});

await fn();

logger.log({
    message: 'Finishing Await',
    meta: {}
});

const handler: Handler<APIGatewayProxyEvent> = async (event, context): Promise<APIGatewayProxyResult> => {
    context.callbackWaitsForEmptyEventLoop = false;

    const { queryStringParameters, pathParameters, path, headers: requestHeaders } = event;

    // returns to api gateway
    const result: APIGatewayProxyResult = {
        statusCode: StatusCodes.NOT_FOUND,
        body: 'Not Found',
        headers: {
            'Content-Type': 'application/json'
        }
    };

    logger.log({
        message: 'NOT_FOUND',
        meta: { path, queryStringParameters, pathParameters, requestHeaders, result }
    });

    return result;
};

export default handler;
kolanos commented 2 years ago

@mrickard I transferred this from newrelic-lambda-extension.

mrickard commented 1 year ago

@anthonyc4 This was an enhancement we investigated, and unfortunately there's a fundamental blocker: NODE_PATH is not used in resolving import specifiers. See the Node ESM docs for this.

The no-code-change instrumentation we have leverages AWS's layers standard for installing our agent in /opt/nodejs while being able to instrument dependencies in /var/task, finding them by using NODE_PATH. Without an ambient path available, the agent in /opt/nodejs can't find your dependencies or handler.

While that rules out using the Lambda Layer, you can still instrument ESM Lambda functions with New Relic, by following these instructions.