Skip to content

Commit 8d8e317

Browse files
authored
Activate ESM loader hook of the "import-in-the-middle" library for ES (EcmaScript) based user handlers (#1574)
* Activate ESM loader hook of the "import-in-the-middle" library for ES (EcmaScript) based user handlers * Add tests
1 parent 990e881 commit 8d8e317

File tree

15 files changed

+506
-168
lines changed

15 files changed

+506
-168
lines changed

nodejs/package-lock.json

Lines changed: 77 additions & 107 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodejs/packages/layer/package-lock.json

Lines changed: 8 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodejs/packages/layer/package.json

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .ts",
1010
"lint:fix": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .ts --fix",
1111
"build": "npm run clean && npm run compile && npm run postcompile",
12+
"copy-esm-files": "copyfiles 'src/**/*.mjs' build && copyfiles 'test/**/*.mjs' build",
1213
"compile": "tsc -p .",
13-
"postcompile": "copyfiles 'package*.json' build/workspace/nodejs && npm install --production --ignore-scripts --prefix build/workspace/nodejs && rm build/workspace/nodejs/package.json build/workspace/nodejs/package-lock.json && copyfiles -f 'scripts/*' build/workspace && copyfiles -f 'build/src/*' build/workspace && cd build/workspace && bestzip ../layer.zip *",
14-
"test": "mocha"
14+
"postcompile": "npm run copy-esm-files && copyfiles 'package*.json' build/workspace/nodejs && npm install --production --ignore-scripts --prefix build/workspace/nodejs && rm build/workspace/nodejs/package.json build/workspace/nodejs/package-lock.json && copyfiles -f 'scripts/*' build/workspace && copyfiles -f 'build/src/*' build/workspace && cd build/workspace && bestzip ../layer.zip *",
15+
"pretest": "npm run compile",
16+
"test:cjs": "mocha 'test/**/*.spec.ts' --exclude 'test/**/*.spec.mjs' --timeout 10000",
17+
"test:esm": "mocha 'test/**/*.spec.mjs' --exclude 'test/**/*.spec.ts' --timeout 10000",
18+
"test": "npm run test:cjs && npm run test:esm"
1519
},
1620
"keywords": [
1721
"opentelemetry",
@@ -28,21 +32,21 @@
2832
},
2933
"dependencies": {
3034
"@opentelemetry/api": "^1.9.0",
31-
"@opentelemetry/api-logs": "^0.54.0",
32-
"@opentelemetry/exporter-logs-otlp-proto": "^0.54.0",
35+
"@opentelemetry/api-logs": "^0.54.2",
36+
"@opentelemetry/exporter-logs-otlp-proto": "^0.54.2",
3337
"@opentelemetry/auto-configuration-propagators": "^0.3.1",
3438
"@opentelemetry/core": "^1.27.0",
35-
"@opentelemetry/exporter-metrics-otlp-proto": "^0.54.0",
36-
"@opentelemetry/exporter-trace-otlp-proto": "^0.54.0",
37-
"@opentelemetry/instrumentation": "^0.54.0",
38-
"@opentelemetry/instrumentation-aws-lambda": "^0.46.0",
39-
"@opentelemetry/instrumentation-aws-sdk": "^0.45.0",
39+
"@opentelemetry/exporter-metrics-otlp-proto": "^0.54.2",
40+
"@opentelemetry/exporter-trace-otlp-proto": "^0.54.2",
41+
"@opentelemetry/instrumentation": "^0.54.2",
42+
"@opentelemetry/instrumentation-aws-lambda": "^0.47.0",
43+
"@opentelemetry/instrumentation-aws-sdk": "^0.46.0",
4044
"@opentelemetry/instrumentation-dns": "^0.40.0",
4145
"@opentelemetry/instrumentation-express": "^0.44.0",
4246
"@opentelemetry/instrumentation-graphql": "^0.44.0",
43-
"@opentelemetry/instrumentation-grpc": "^0.54.0",
47+
"@opentelemetry/instrumentation-grpc": "^0.54.2",
4448
"@opentelemetry/instrumentation-hapi": "^0.42.0",
45-
"@opentelemetry/instrumentation-http": "^0.54.0",
49+
"@opentelemetry/instrumentation-http": "^0.54.2",
4650
"@opentelemetry/instrumentation-ioredis": "^0.44.0",
4751
"@opentelemetry/instrumentation-koa": "^0.44.0",
4852
"@opentelemetry/instrumentation-mongodb": "^0.48.0",
@@ -53,7 +57,7 @@
5357
"@opentelemetry/propagator-aws-xray": "^1.26.0",
5458
"@opentelemetry/resource-detector-aws": "^1.7.0",
5559
"@opentelemetry/resources": "^1.27.0",
56-
"@opentelemetry/sdk-logs": "^0.54.0",
60+
"@opentelemetry/sdk-logs": "^0.54.2",
5761
"@opentelemetry/sdk-metrics": "^1.27.0",
5862
"@opentelemetry/sdk-trace-base": "^1.27.0",
5963
"@opentelemetry/sdk-trace-node": "^1.27.0"

nodejs/packages/layer/scripts/otel-handler

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
set -ef -o pipefail
44

5-
export NODE_OPTIONS="${NODE_OPTIONS} --require /opt/wrapper.js"
5+
export NODE_OPTIONS="${NODE_OPTIONS} --import /opt/loader.mjs --require /opt/wrapper.js"
66

77
if [[ $OTEL_RESOURCE_ATTRIBUTES != *"service.name="* ]]; then
88
export OTEL_RESOURCE_ATTRIBUTES="service.name=${AWS_LAMBDA_FUNCTION_NAME},${OTEL_RESOURCE_ATTRIBUTES}"
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { register } from 'module';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
5+
function _hasFolderPackageJsonTypeModule(folder) {
6+
if (folder.endsWith('/node_modules')) {
7+
return false;
8+
}
9+
let pj = path.join(folder, '/package.json');
10+
if (fs.existsSync(pj)) {
11+
try {
12+
let pkg = JSON.parse(fs.readFileSync(pj).toString());
13+
if (pkg) {
14+
if (pkg.type === 'module') {
15+
return true;
16+
} else {
17+
return false;
18+
}
19+
}
20+
} catch (e) {
21+
console.warn(`${pj} cannot be read, it will be ignored for ES module detection purposes.`, e);
22+
return false;
23+
}
24+
}
25+
if (folder === '/') {
26+
return false;
27+
}
28+
return _hasFolderPackageJsonTypeModule(path.resolve(folder, '..'));
29+
}
30+
31+
function _hasPackageJsonTypeModule(file) {
32+
let jsPath = file + '.js';
33+
if (fs.existsSync(jsPath)) {
34+
return _hasFolderPackageJsonTypeModule(path.resolve(path.dirname(jsPath)));
35+
}
36+
return false;
37+
}
38+
39+
function _resolveHandlerFileName() {
40+
const taskRoot = process.env.LAMBDA_TASK_ROOT;
41+
const handlerDef = process.env._HANDLER;
42+
if (!taskRoot || !handlerDef) {
43+
return null;
44+
}
45+
const handler = path.basename(handlerDef);
46+
const moduleRoot = handlerDef.substr(0, handlerDef.length - handler.length);
47+
const [module, _] = handler.split('.', 2);
48+
return path.resolve(taskRoot, moduleRoot, module);
49+
}
50+
51+
function _isHandlerAnESModule() {
52+
try {
53+
const handlerFileName = _resolveHandlerFileName();
54+
if (!handlerFileName) {
55+
return false;
56+
}
57+
if (fs.existsSync(handlerFileName + '.mjs')) {
58+
return true;
59+
} else if (fs.existsSync(handlerFileName + '.cjs')) {
60+
return false;
61+
} else {
62+
return _hasPackageJsonTypeModule(handlerFileName);
63+
}
64+
} catch (e) {
65+
console.error('Unknown error occurred while checking whether handler is an ES module', e);
66+
return false;
67+
}
68+
}
69+
70+
let registered = false;
71+
72+
export function registerLoader() {
73+
if (!registered) {
74+
register('import-in-the-middle/hook.mjs', import.meta.url);
75+
registered = true;
76+
}
77+
}
78+
79+
if (_isHandlerAnESModule()) {
80+
/*
81+
We could activate ESM loader hook of the "import-in-the-middle" library,
82+
- by "--loader=import-in-the-middle/hook.mjs" Node CLI option, but "--loader" option has been deprecated
83+
- or by "--import=import-in-the-middle/hook.mjs" Node CLI option, but in this case,
84+
there will always be "import-in-the-middle" hook initialization overhead even for non-ESM (CommonJS) modules
85+
86+
Hence, instead, we initialize "import-in-the-middle" hook only for ES (EcmaScript) based user handlers
87+
to prevent redundant "import-in-the-middle" hook initialization overhead during coldstart
88+
of the CommonJS based user handlers.
89+
*/
90+
registerLoader();
91+
}

nodejs/packages/layer/src/wrapper.ts

Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ import {
2727
AwsLambdaInstrumentationConfig,
2828
} from '@opentelemetry/instrumentation-aws-lambda';
2929
import {
30+
context,
3031
diag,
3132
DiagConsoleLogger,
3233
DiagLogLevel,
3334
metrics,
35+
propagation,
36+
trace,
3437
} from '@opentelemetry/api';
3538
import { getEnv } from '@opentelemetry/core';
3639
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
@@ -51,8 +54,8 @@ import {
5154
import { logs } from '@opentelemetry/api-logs';
5255

5356
function defaultConfigureInstrumentations() {
54-
// Use require statements for instrumentation to avoid having to have transitive dependencies on all the typescript
55-
// definitions.
57+
// Use require statements for instrumentation
58+
// to avoid having to have transitive dependencies on all the typescript definitions.
5659
const { DnsInstrumentation } = require('@opentelemetry/instrumentation-dns');
5760
const {
5861
ExpressInstrumentation,
@@ -102,7 +105,7 @@ function defaultConfigureInstrumentations() {
102105
}
103106

104107
declare global {
105-
// in case of downstream configuring span processors etc
108+
// In case of downstream configuring span processors etc
106109
function configureAwsInstrumentation(
107110
defaultConfig: AwsSdkInstrumentationConfig,
108111
): AwsSdkInstrumentationConfig;
@@ -123,34 +126,25 @@ declare global {
123126
function configureInstrumentations(): Instrumentation[];
124127
}
125128

126-
console.log('Registering OpenTelemetry');
127-
128-
const instrumentations = [
129-
new AwsInstrumentation(
130-
typeof configureAwsInstrumentation === 'function'
131-
? configureAwsInstrumentation({ suppressInternalInstrumentation: true })
132-
: { suppressInternalInstrumentation: true },
133-
),
134-
new AwsLambdaInstrumentation(
135-
typeof configureLambdaInstrumentation === 'function'
136-
? configureLambdaInstrumentation({})
137-
: {},
138-
),
139-
...(typeof configureInstrumentations === 'function'
140-
? configureInstrumentations
141-
: defaultConfigureInstrumentations)(),
142-
];
143-
144-
// configure lambda logging
145-
const logLevel = getEnv().OTEL_LOG_LEVEL;
146-
diag.setLogger(new DiagConsoleLogger(), logLevel);
147-
148-
// Register instrumentations synchronously to ensure code is patched even before provider is ready.
149-
registerInstrumentations({
150-
instrumentations,
151-
});
129+
function createInstrumentations() {
130+
return [
131+
new AwsInstrumentation(
132+
typeof configureAwsInstrumentation === 'function'
133+
? configureAwsInstrumentation({ suppressInternalInstrumentation: true })
134+
: { suppressInternalInstrumentation: true },
135+
),
136+
new AwsLambdaInstrumentation(
137+
typeof configureLambdaInstrumentation === 'function'
138+
? configureLambdaInstrumentation({})
139+
: {},
140+
),
141+
...(typeof configureInstrumentations === 'function'
142+
? configureInstrumentations
143+
: defaultConfigureInstrumentations)(),
144+
];
145+
}
152146

153-
async function initializeProvider() {
147+
function initializeProvider() {
154148
const resource = detectResourcesSync({
155149
detectors: [awsLambdaDetector, envDetector, processDetector],
156150
});
@@ -166,12 +160,12 @@ async function initializeProvider() {
166160
if (typeof configureTracerProvider === 'function') {
167161
configureTracerProvider(tracerProvider);
168162
} else {
169-
// defaults
163+
// Defaults
170164
tracerProvider.addSpanProcessor(
171165
new BatchSpanProcessor(new OTLPTraceExporter()),
172166
);
173167
}
174-
// logging for debug
168+
// Logging for debug
175169
if (logLevel === DiagLogLevel.DEBUG) {
176170
tracerProvider.addSpanProcessor(
177171
new SimpleSpanProcessor(new ConsoleSpanExporter()),
@@ -182,7 +176,7 @@ async function initializeProvider() {
182176
if (typeof configureSdkRegistration === 'function') {
183177
sdkRegistrationConfig = configureSdkRegistration(sdkRegistrationConfig);
184178
}
185-
// auto-configure propagator if not provided
179+
// Auto-configure propagator if not provided
186180
if (!sdkRegistrationConfig.propagator) {
187181
sdkRegistrationConfig.propagator = getPropagator();
188182
}
@@ -223,20 +217,58 @@ async function initializeProvider() {
223217
logs.setGlobalLoggerProvider(loggerProvider);
224218
}
225219

226-
// logging for debug
220+
// Logging for debug
227221
if (logLevel === DiagLogLevel.DEBUG) {
228222
loggerProvider.addLogRecordProcessor(
229223
new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()),
230224
);
231225
}
232226

227+
// Create instrumentations if they have not been created before
228+
// to prevent additional coldstart overhead
229+
// caused by creations and initializations of instrumentations.
230+
if (!instrumentations || !instrumentations.length) {
231+
instrumentations = createInstrumentations();
232+
}
233+
233234
// Re-register instrumentation with initialized provider. Patched code will see the update.
234-
registerInstrumentations({
235+
disableInstrumentations = registerInstrumentations({
235236
instrumentations,
236237
tracerProvider,
237238
meterProvider,
238239
loggerProvider,
239240
});
240241
}
241-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
242-
initializeProvider();
242+
243+
export function wrap() {
244+
initializeProvider();
245+
}
246+
247+
export function unwrap() {
248+
if (disableInstrumentations) {
249+
disableInstrumentations();
250+
disableInstrumentations = () => {};
251+
}
252+
instrumentations = [];
253+
context.disable();
254+
propagation.disable();
255+
trace.disable();
256+
metrics.disable();
257+
logs.disable();
258+
}
259+
260+
console.log('Registering OpenTelemetry');
261+
262+
// Configure lambda logging
263+
const logLevel = getEnv().OTEL_LOG_LEVEL;
264+
diag.setLogger(new DiagConsoleLogger(), logLevel);
265+
266+
let instrumentations = createInstrumentations();
267+
let disableInstrumentations: () => void;
268+
269+
// Register instrumentations synchronously to ensure code is patched even before provider is ready.
270+
disableInstrumentations = registerInstrumentations({
271+
instrumentations,
272+
});
273+
274+
wrap();

0 commit comments

Comments
 (0)