Skip to content

Commit

Permalink
Patch aws-lambda instrumentation to support ESM
Browse files Browse the repository at this point in the history
commit d25c3c3
Author: Min Xia <[email protected]>
Date:   Tue Oct 8 15:45:36 2024 -0700

    fix the lint error
  • Loading branch information
mxiamxia committed Oct 10, 2024
1 parent 032677f commit faaee6b
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
import * as path from 'path';
import * as fs from 'fs';
import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, isWrapped } from '@opentelemetry/instrumentation';
import { diag } from '@opentelemetry/api';

export class AwsLambdaInstrumentationPatch extends AwsLambdaInstrumentation {

override init() {
// Custom logic before calling the original implementation
diag.debug('Initializing AwsLambdaInstrumentationPatch');
const taskRoot = process.env.LAMBDA_TASK_ROOT;
const handlerDef = this._config.lambdaHandler ?? process.env._HANDLER;

// _HANDLER and LAMBDA_TASK_ROOT are always defined in Lambda but guard bail out if in the future this changes.
if (!taskRoot || !handlerDef) {
this._diag.debug(
'Skipping lambda instrumentation: no _HANDLER/lambdaHandler or LAMBDA_TASK_ROOT.',
{ taskRoot, handlerDef }
);
return [];
}

const handler = path.basename(handlerDef);
const moduleRoot = handlerDef.substr(0, handlerDef.length - handler.length);

const [module, functionName] = handler.split('.', 2);

// Lambda loads user function using an absolute path.
let filename = path.resolve(taskRoot, moduleRoot, module);
if (!filename.endsWith('.js')) {
// its impossible to know in advance if the user has a cjs or js file.
// check that the .js file exists otherwise fallback to next known possibility
try {
fs.statSync(`${filename}.js`);
filename += '.js';
} catch (e) {
// fallback to .cjs
try {
fs.statSync(`${filename}.cjs`);
filename += '.cjs';
} catch (e) {
// fall back to .mjs
filename += '.mjs';
}
}
}

diag.debug('Instrumenting lambda handler', {
taskRoot,
handlerDef,
handler,
moduleRoot,
module,
filename,
functionName,
});

if (filename.endsWith('.mjs') || process.env.IS_ESM) {
return [
new InstrumentationNodeModuleDefinition(
// NB: The patching infrastructure seems to match names backwards, this must be the filename, while
// InstrumentationNodeModuleFile must be the module name.
filename,
['*'],
(moduleExports: any) => {
diag.debug('Applying patch for lambda esm handler');
if (isWrapped(moduleExports[functionName])) {
this._unwrap(moduleExports, functionName);
}
this._wrap(
moduleExports,
functionName,
(this as any)._getHandler()
);
return moduleExports;
},
(moduleExports?: any) => {
if (moduleExports == null) return;
diag.debug('Removing patch for lambda esm handler');
this._unwrap(moduleExports, functionName);
}
)
];
} else {
return [
new InstrumentationNodeModuleDefinition(
// NB: The patching infrastructure seems to match names backwards, this must be the filename, while
// InstrumentationNodeModuleFile must be the module name.
filename,
['*'],
undefined,
undefined,
[
new InstrumentationNodeModuleFile(
module,
['*'],
(moduleExports: any) => {
diag.debug('Applying patch for lambda handler');
if (isWrapped(moduleExports[functionName])) {
this._unwrap(moduleExports, functionName);
}
this._wrap(
moduleExports,
functionName,
(this as any)._getHandler()
);
return moduleExports;
},
(moduleExports?: any) => {
if (moduleExports == null) return;
diag.debug('Removing patch for lambda handler');
this._unwrap(moduleExports, functionName);
}
),
]
),
];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
trace,
} from '@opentelemetry/api';
import { Instrumentation } from '@opentelemetry/instrumentation';
import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
import { AwsSdkInstrumentationConfig, NormalizedRequest } from '@opentelemetry/instrumentation-aws-sdk';
import { AWSXRAY_TRACE_ID_HEADER, AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
import { APIGatewayProxyEventHeaders, Context } from 'aws-lambda';
Expand All @@ -26,6 +25,7 @@ import {
} from './aws/services/bedrock';
import { KinesisServiceExtension } from './aws/services/kinesis';
import { S3ServiceExtension } from './aws/services/s3';
import { AwsLambdaInstrumentationPatch } from "./aws/services/aws-lambda";

export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID';
const awsPropagator = new AWSXRayPropagator();
Expand Down Expand Up @@ -65,7 +65,7 @@ export function applyInstrumentationPatches(instrumentations: Instrumentation[])
}
} else if (instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-lambda') {
diag.debug('Overriding aws lambda instrumentation');
const lambdaInstrumentation = new AwsLambdaInstrumentation({
const lambdaInstrumentation = new AwsLambdaInstrumentationPatch({
eventContextExtractor: customExtractor,
disableAwsContextPropagation: true,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import * as path from 'path';
import * as fs from 'fs';
import { diag } from '@opentelemetry/api';
import { InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
import { AwsLambdaInstrumentationPatch } from "../../../../src/patches/aws/services/aws-lambda";

describe('AwsLambdaInstrumentationPatch', () => {
let instrumentation: AwsLambdaInstrumentationPatch;

beforeEach(() => {
sinon.restore();
instrumentation = new AwsLambdaInstrumentationPatch({});
});

afterEach(() => {
sinon.restore();
});

describe('init', () => {
it('should fallback to .cjs if .js does not exist', () => {
process.env.LAMBDA_TASK_ROOT = '/var/task';
process.env._HANDLER = 'src/index.handler';

sinon.stub(path, 'basename').returns('index.handler');
sinon.stub(fs, 'statSync')
.onFirstCall().throws(new Error('File not found')) // .js file does not exist
.onSecondCall().returns({} as any); // .cjs file exists

const result = instrumentation.init();

assert.strictEqual(result[0].name, '/var/task/src/index.cjs');
});

it('should fallback to .mjs when .js and .cjs do not exist', () => {
process.env.LAMBDA_TASK_ROOT = '/var/task';
process.env._HANDLER = 'src/index.handler';

sinon.stub(path, 'basename').returns('index.handler');
sinon.stub(fs, 'statSync')
.onFirstCall().throws(new Error('File not found')) // .js not found
.onSecondCall().throws(new Error('File not found')) // .cjs not found

const result = instrumentation.init();

assert.strictEqual(result[0].name, '/var/task/src/index.mjs');
});

it('should instrument CommonJS handler correctly', () => {
process.env.LAMBDA_TASK_ROOT = '/var/task';
process.env._HANDLER = 'src/index.handler';

sinon.stub(path, 'basename').returns('index.handler');
sinon.stub(fs, 'statSync').returns({} as any); // Mock that the .js file exists
const debugStub = sinon.stub(diag, 'debug');

const result = instrumentation.init();

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, '/var/task/src/index.js');
assert(result[0] instanceof InstrumentationNodeModuleDefinition);
assert.strictEqual(result[0].files.length, 1);
assert(debugStub.calledWithMatch('Instrumenting lambda handler', sinon.match.object));
});

it('should return ESM instrumentation for .mjs files or when IS_ESM is set', () => {
process.env.LAMBDA_TASK_ROOT = '/var/task';
process.env._HANDLER = 'src/index.handler';
process.env.IS_ESM = 'true'; // ESM environment variable set

sinon.stub(path, 'basename').returns('index.handler');
sinon.stub(fs, 'statSync').throws(new Error('File not found')); // No .js or .cjs file exists

const result = instrumentation.init();

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, '/var/task/src/index.mjs');
assert(result[0] instanceof InstrumentationNodeModuleDefinition);
assert.strictEqual(result[0].files.length, 0); //
});
});
});
37 changes: 36 additions & 1 deletion lambda-layer/packages/layer/scripts/otel-instrument
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
#!/bin/bash
export NODE_OPTIONS="${NODE_OPTIONS} --require /opt/wrapper.js"
isESMScript() {
# Lambda function root directory
TASK_DIR="/var/task"

# Flag variables to track conditions
local found_mjs=false
local is_module=false

# Check for any files ending with `.mjs`
if ls "$TASK_DIR"/*.mjs &>/dev/null; then
found_mjs=true
fi

# Check if `package.json` exists and if it contains `"type": "module"`
if [ -f "$TASK_DIR/package.json" ]; then
# Check for the `"type": "module"` attribute in `package.json`
if grep -q '"type": *"module"' "$TASK_DIR/package.json"; then
is_module=true
fi
fi

# Return true if both conditions are met
if $found_mjs || $is_module; then
return 0 # 0 in bash means true
else
return 1 # 1 in bash means false
fi
}

if isESMScript; then
export NODE_OPTIONS="${NODE_OPTIONS} --import @aws/aws-distro-opentelemetry-node-autoinstrumentation/register --experimental-loader=@opentelemetry/instrumentation/hook.mjs"
export IS_ESM=true
else
export NODE_OPTIONS="${NODE_OPTIONS} --require /opt/wrapper.js"
fi

export LAMBDA_RESOURCE_ATTRIBUTES="cloud.region=$AWS_REGION,cloud.provider=aws,faas.name=$AWS_LAMBDA_FUNCTION_NAME,faas.version=$AWS_LAMBDA_FUNCTION_VERSION,faas.instance=$AWS_LAMBDA_LOG_STREAM_NAME,aws.log.group.names=$AWS_LAMBDA_LOG_GROUP_NAME";


Expand Down
46 changes: 46 additions & 0 deletions lambda-layer/packages/layer/scripts/otel-instrument-esm
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash
export NODE_OPTIONS="${NODE_OPTIONS} --import @aws/aws-distro-opentelemetry-node-autoinstrumentation/register --experimental-loader=@opentelemetry/instrumentation/hook.mjs"
export LAMBDA_RESOURCE_ATTRIBUTES="cloud.region=$AWS_REGION,cloud.provider=aws,faas.name=$AWS_LAMBDA_FUNCTION_NAME,faas.version=$AWS_LAMBDA_FUNCTION_VERSION,faas.instance=$AWS_LAMBDA_LOG_STREAM_NAME,aws.log.group.names=$AWS_LAMBDA_LOG_GROUP_NAME";
export IS_ESM=true

# - If OTEL_EXPORTER_OTLP_PROTOCOL is not set by user, the default exporting protocol is http/protobuf.
if [ -z "${OTEL_EXPORTER_OTLP_PROTOCOL}" ]; then
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
fi

# - If OTEL_NODE_ENABLED_INSTRUMENTATIONS is not set by user, use default instrumentation
if [ -z "${OTEL_NODE_ENABLED_INSTRUMENTATIONS}" ]; then
export OTEL_NODE_ENABLED_INSTRUMENTATIONS="aws-lambda,aws-sdk"
fi

# - Set the service name
if [ -z "${OTEL_SERVICE_NAME}" ]; then
export OTEL_SERVICE_NAME=$AWS_LAMBDA_FUNCTION_NAME;
fi

# - Set the propagators
if [[ -z "$OTEL_PROPAGATORS" ]]; then
export OTEL_PROPAGATORS="tracecontext,baggage,xray"
fi

# - Set Application Signals configuration
if [ -z "${OTEL_AWS_APPLICATION_SIGNALS_ENABLED}" ]; then
export OTEL_AWS_APPLICATION_SIGNALS_ENABLED="true";
fi

if [ -z "${OTEL_METRICS_EXPORTER}" ]; then
export OTEL_METRICS_EXPORTER="none";
fi

# - Append Lambda Resource Attributes to OTel Resource Attribute List
if [ -z "${OTEL_RESOURCE_ATTRIBUTES}" ]; then
export OTEL_RESOURCE_ATTRIBUTES=$LAMBDA_RESOURCE_ATTRIBUTES;
else
export OTEL_RESOURCE_ATTRIBUTES="$LAMBDA_RESOURCE_ATTRIBUTES,$OTEL_RESOURCE_ATTRIBUTES";
fi

if [[ $OTEL_RESOURCE_ATTRIBUTES != *"service.name="* ]]; then
export OTEL_RESOURCE_ATTRIBUTES="service.name=${AWS_LAMBDA_FUNCTION_NAME},${OTEL_RESOURCE_ATTRIBUTES}"
fi

exec "$@"

0 comments on commit faaee6b

Please sign in to comment.