Skip to content

Commit faaee6b

Browse files
committed
Patch aws-lambda instrumentation to support ESM
commit d25c3c3 Author: Min Xia <[email protected]> Date: Tue Oct 8 15:45:36 2024 -0700 fix the lint error
1 parent 032677f commit faaee6b

File tree

5 files changed

+288
-3
lines changed

5 files changed

+288
-3
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, isWrapped } from '@opentelemetry/instrumentation';
5+
import { diag } from '@opentelemetry/api';
6+
7+
export class AwsLambdaInstrumentationPatch extends AwsLambdaInstrumentation {
8+
9+
override init() {
10+
// Custom logic before calling the original implementation
11+
diag.debug('Initializing AwsLambdaInstrumentationPatch');
12+
const taskRoot = process.env.LAMBDA_TASK_ROOT;
13+
const handlerDef = this._config.lambdaHandler ?? process.env._HANDLER;
14+
15+
// _HANDLER and LAMBDA_TASK_ROOT are always defined in Lambda but guard bail out if in the future this changes.
16+
if (!taskRoot || !handlerDef) {
17+
this._diag.debug(
18+
'Skipping lambda instrumentation: no _HANDLER/lambdaHandler or LAMBDA_TASK_ROOT.',
19+
{ taskRoot, handlerDef }
20+
);
21+
return [];
22+
}
23+
24+
const handler = path.basename(handlerDef);
25+
const moduleRoot = handlerDef.substr(0, handlerDef.length - handler.length);
26+
27+
const [module, functionName] = handler.split('.', 2);
28+
29+
// Lambda loads user function using an absolute path.
30+
let filename = path.resolve(taskRoot, moduleRoot, module);
31+
if (!filename.endsWith('.js')) {
32+
// its impossible to know in advance if the user has a cjs or js file.
33+
// check that the .js file exists otherwise fallback to next known possibility
34+
try {
35+
fs.statSync(`${filename}.js`);
36+
filename += '.js';
37+
} catch (e) {
38+
// fallback to .cjs
39+
try {
40+
fs.statSync(`${filename}.cjs`);
41+
filename += '.cjs';
42+
} catch (e) {
43+
// fall back to .mjs
44+
filename += '.mjs';
45+
}
46+
}
47+
}
48+
49+
diag.debug('Instrumenting lambda handler', {
50+
taskRoot,
51+
handlerDef,
52+
handler,
53+
moduleRoot,
54+
module,
55+
filename,
56+
functionName,
57+
});
58+
59+
if (filename.endsWith('.mjs') || process.env.IS_ESM) {
60+
return [
61+
new InstrumentationNodeModuleDefinition(
62+
// NB: The patching infrastructure seems to match names backwards, this must be the filename, while
63+
// InstrumentationNodeModuleFile must be the module name.
64+
filename,
65+
['*'],
66+
(moduleExports: any) => {
67+
diag.debug('Applying patch for lambda esm handler');
68+
if (isWrapped(moduleExports[functionName])) {
69+
this._unwrap(moduleExports, functionName);
70+
}
71+
this._wrap(
72+
moduleExports,
73+
functionName,
74+
(this as any)._getHandler()
75+
);
76+
return moduleExports;
77+
},
78+
(moduleExports?: any) => {
79+
if (moduleExports == null) return;
80+
diag.debug('Removing patch for lambda esm handler');
81+
this._unwrap(moduleExports, functionName);
82+
}
83+
)
84+
];
85+
} else {
86+
return [
87+
new InstrumentationNodeModuleDefinition(
88+
// NB: The patching infrastructure seems to match names backwards, this must be the filename, while
89+
// InstrumentationNodeModuleFile must be the module name.
90+
filename,
91+
['*'],
92+
undefined,
93+
undefined,
94+
[
95+
new InstrumentationNodeModuleFile(
96+
module,
97+
['*'],
98+
(moduleExports: any) => {
99+
diag.debug('Applying patch for lambda handler');
100+
if (isWrapped(moduleExports[functionName])) {
101+
this._unwrap(moduleExports, functionName);
102+
}
103+
this._wrap(
104+
moduleExports,
105+
functionName,
106+
(this as any)._getHandler()
107+
);
108+
return moduleExports;
109+
},
110+
(moduleExports?: any) => {
111+
if (moduleExports == null) return;
112+
diag.debug('Removing patch for lambda handler');
113+
this._unwrap(moduleExports, functionName);
114+
}
115+
),
116+
]
117+
),
118+
];
119+
}
120+
}
121+
}

aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
trace,
1313
} from '@opentelemetry/api';
1414
import { Instrumentation } from '@opentelemetry/instrumentation';
15-
import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
1615
import { AwsSdkInstrumentationConfig, NormalizedRequest } from '@opentelemetry/instrumentation-aws-sdk';
1716
import { AWSXRAY_TRACE_ID_HEADER, AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
1817
import { APIGatewayProxyEventHeaders, Context } from 'aws-lambda';
@@ -26,6 +25,7 @@ import {
2625
} from './aws/services/bedrock';
2726
import { KinesisServiceExtension } from './aws/services/kinesis';
2827
import { S3ServiceExtension } from './aws/services/s3';
28+
import { AwsLambdaInstrumentationPatch } from "./aws/services/aws-lambda";
2929

3030
export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID';
3131
const awsPropagator = new AWSXRayPropagator();
@@ -65,7 +65,7 @@ export function applyInstrumentationPatches(instrumentations: Instrumentation[])
6565
}
6666
} else if (instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-lambda') {
6767
diag.debug('Overriding aws lambda instrumentation');
68-
const lambdaInstrumentation = new AwsLambdaInstrumentation({
68+
const lambdaInstrumentation = new AwsLambdaInstrumentationPatch({
6969
eventContextExtractor: customExtractor,
7070
disableAwsContextPropagation: true,
7171
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import * as path from 'path';
4+
import * as fs from 'fs';
5+
import { diag } from '@opentelemetry/api';
6+
import { InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
7+
import { AwsLambdaInstrumentationPatch } from "../../../../src/patches/aws/services/aws-lambda";
8+
9+
describe('AwsLambdaInstrumentationPatch', () => {
10+
let instrumentation: AwsLambdaInstrumentationPatch;
11+
12+
beforeEach(() => {
13+
sinon.restore();
14+
instrumentation = new AwsLambdaInstrumentationPatch({});
15+
});
16+
17+
afterEach(() => {
18+
sinon.restore();
19+
});
20+
21+
describe('init', () => {
22+
it('should fallback to .cjs if .js does not exist', () => {
23+
process.env.LAMBDA_TASK_ROOT = '/var/task';
24+
process.env._HANDLER = 'src/index.handler';
25+
26+
sinon.stub(path, 'basename').returns('index.handler');
27+
sinon.stub(fs, 'statSync')
28+
.onFirstCall().throws(new Error('File not found')) // .js file does not exist
29+
.onSecondCall().returns({} as any); // .cjs file exists
30+
31+
const result = instrumentation.init();
32+
33+
assert.strictEqual(result[0].name, '/var/task/src/index.cjs');
34+
});
35+
36+
it('should fallback to .mjs when .js and .cjs do not exist', () => {
37+
process.env.LAMBDA_TASK_ROOT = '/var/task';
38+
process.env._HANDLER = 'src/index.handler';
39+
40+
sinon.stub(path, 'basename').returns('index.handler');
41+
sinon.stub(fs, 'statSync')
42+
.onFirstCall().throws(new Error('File not found')) // .js not found
43+
.onSecondCall().throws(new Error('File not found')) // .cjs not found
44+
45+
const result = instrumentation.init();
46+
47+
assert.strictEqual(result[0].name, '/var/task/src/index.mjs');
48+
});
49+
50+
it('should instrument CommonJS handler correctly', () => {
51+
process.env.LAMBDA_TASK_ROOT = '/var/task';
52+
process.env._HANDLER = 'src/index.handler';
53+
54+
sinon.stub(path, 'basename').returns('index.handler');
55+
sinon.stub(fs, 'statSync').returns({} as any); // Mock that the .js file exists
56+
const debugStub = sinon.stub(diag, 'debug');
57+
58+
const result = instrumentation.init();
59+
60+
assert.strictEqual(result.length, 1);
61+
assert.strictEqual(result[0].name, '/var/task/src/index.js');
62+
assert(result[0] instanceof InstrumentationNodeModuleDefinition);
63+
assert.strictEqual(result[0].files.length, 1);
64+
assert(debugStub.calledWithMatch('Instrumenting lambda handler', sinon.match.object));
65+
});
66+
67+
it('should return ESM instrumentation for .mjs files or when IS_ESM is set', () => {
68+
process.env.LAMBDA_TASK_ROOT = '/var/task';
69+
process.env._HANDLER = 'src/index.handler';
70+
process.env.IS_ESM = 'true'; // ESM environment variable set
71+
72+
sinon.stub(path, 'basename').returns('index.handler');
73+
sinon.stub(fs, 'statSync').throws(new Error('File not found')); // No .js or .cjs file exists
74+
75+
const result = instrumentation.init();
76+
77+
assert.strictEqual(result.length, 1);
78+
assert.strictEqual(result[0].name, '/var/task/src/index.mjs');
79+
assert(result[0] instanceof InstrumentationNodeModuleDefinition);
80+
assert.strictEqual(result[0].files.length, 0); //
81+
});
82+
});
83+
});

lambda-layer/packages/layer/scripts/otel-instrument

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
11
#!/bin/bash
2-
export NODE_OPTIONS="${NODE_OPTIONS} --require /opt/wrapper.js"
2+
isESMScript() {
3+
# Lambda function root directory
4+
TASK_DIR="/var/task"
5+
6+
# Flag variables to track conditions
7+
local found_mjs=false
8+
local is_module=false
9+
10+
# Check for any files ending with `.mjs`
11+
if ls "$TASK_DIR"/*.mjs &>/dev/null; then
12+
found_mjs=true
13+
fi
14+
15+
# Check if `package.json` exists and if it contains `"type": "module"`
16+
if [ -f "$TASK_DIR/package.json" ]; then
17+
# Check for the `"type": "module"` attribute in `package.json`
18+
if grep -q '"type": *"module"' "$TASK_DIR/package.json"; then
19+
is_module=true
20+
fi
21+
fi
22+
23+
# Return true if both conditions are met
24+
if $found_mjs || $is_module; then
25+
return 0 # 0 in bash means true
26+
else
27+
return 1 # 1 in bash means false
28+
fi
29+
}
30+
31+
if isESMScript; then
32+
export NODE_OPTIONS="${NODE_OPTIONS} --import @aws/aws-distro-opentelemetry-node-autoinstrumentation/register --experimental-loader=@opentelemetry/instrumentation/hook.mjs"
33+
export IS_ESM=true
34+
else
35+
export NODE_OPTIONS="${NODE_OPTIONS} --require /opt/wrapper.js"
36+
fi
37+
338
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";
439

540

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
export NODE_OPTIONS="${NODE_OPTIONS} --import @aws/aws-distro-opentelemetry-node-autoinstrumentation/register --experimental-loader=@opentelemetry/instrumentation/hook.mjs"
3+
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";
4+
export IS_ESM=true
5+
6+
# - If OTEL_EXPORTER_OTLP_PROTOCOL is not set by user, the default exporting protocol is http/protobuf.
7+
if [ -z "${OTEL_EXPORTER_OTLP_PROTOCOL}" ]; then
8+
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
9+
fi
10+
11+
# - If OTEL_NODE_ENABLED_INSTRUMENTATIONS is not set by user, use default instrumentation
12+
if [ -z "${OTEL_NODE_ENABLED_INSTRUMENTATIONS}" ]; then
13+
export OTEL_NODE_ENABLED_INSTRUMENTATIONS="aws-lambda,aws-sdk"
14+
fi
15+
16+
# - Set the service name
17+
if [ -z "${OTEL_SERVICE_NAME}" ]; then
18+
export OTEL_SERVICE_NAME=$AWS_LAMBDA_FUNCTION_NAME;
19+
fi
20+
21+
# - Set the propagators
22+
if [[ -z "$OTEL_PROPAGATORS" ]]; then
23+
export OTEL_PROPAGATORS="tracecontext,baggage,xray"
24+
fi
25+
26+
# - Set Application Signals configuration
27+
if [ -z "${OTEL_AWS_APPLICATION_SIGNALS_ENABLED}" ]; then
28+
export OTEL_AWS_APPLICATION_SIGNALS_ENABLED="true";
29+
fi
30+
31+
if [ -z "${OTEL_METRICS_EXPORTER}" ]; then
32+
export OTEL_METRICS_EXPORTER="none";
33+
fi
34+
35+
# - Append Lambda Resource Attributes to OTel Resource Attribute List
36+
if [ -z "${OTEL_RESOURCE_ATTRIBUTES}" ]; then
37+
export OTEL_RESOURCE_ATTRIBUTES=$LAMBDA_RESOURCE_ATTRIBUTES;
38+
else
39+
export OTEL_RESOURCE_ATTRIBUTES="$LAMBDA_RESOURCE_ATTRIBUTES,$OTEL_RESOURCE_ATTRIBUTES";
40+
fi
41+
42+
if [[ $OTEL_RESOURCE_ATTRIBUTES != *"service.name="* ]]; then
43+
export OTEL_RESOURCE_ATTRIBUTES="service.name=${AWS_LAMBDA_FUNCTION_NAME},${OTEL_RESOURCE_ATTRIBUTES}"
44+
fi
45+
46+
exec "$@"

0 commit comments

Comments
 (0)