diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/README.md b/plugins/node/opentelemetry-instrumentation-aws-lambda/README.md index 3893a552fb..7e12b26732 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/README.md +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/README.md @@ -35,6 +35,9 @@ const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const provider = new NodeTracerProvider(); provider.register(); +// Note that this needs to appear after the tracer provider is registered, +// otherwise the instrumentation will not properly flush data after each +// lambda invocation. registerInstrumentations({ instrumentations: [ new AwsLambdaInstrumentation({ @@ -46,7 +49,7 @@ registerInstrumentations({ In your Lambda function configuration, add or update the `NODE_OPTIONS` environment variable to require the wrapper, e.g., -`NODE_OPTIONS=--require lambda-wrapper` +`NODE_OPTIONS=--require lambda-wrapper --experimental-loader @opentelemetry/instrumentation/hook.mjs` ## AWS Lambda Instrumentation Options diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/package.json b/plugins/node/opentelemetry-instrumentation-aws-lambda/package.json index 02b86d9a9b..fd4aa1d5be 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/package.json +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/package.json @@ -44,6 +44,7 @@ }, "devDependencies": { "@opentelemetry/api": "^1.3.0", + "@opentelemetry/contrib-test-utils": "^0.40.0", "@opentelemetry/core": "^1.8.0", "@opentelemetry/sdk-metrics": "^1.8.0", "@opentelemetry/sdk-trace-base": "^1.8.0", diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts index 0d8eb8a5e2..26068f1074 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import * as path from 'path'; -import * as fs from 'fs'; +import { env } from 'process'; import { InstrumentationBase, @@ -58,8 +57,13 @@ import { import { AwsLambdaInstrumentationConfig, EventContextExtractor } from './types'; import { PACKAGE_NAME, PACKAGE_VERSION } from './version'; -import { env } from 'process'; -import { LambdaModule } from './internal-types'; +import { + isInvalidHandler, + moduleRootAndHandler, + resolveHandler, + splitHandlerString, + tryPath, +} from './user-function'; const awsPropagator = new AWSXRayPropagator(); const headerGetter: TextMapGetter = { @@ -92,6 +96,7 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { this._config.disableAwsContextPropagation = true; } } + this._traceForceFlusher = this._traceForceFlush(trace.getTracerProvider()); } override setConfig(config: AwsLambdaInstrumentationConfig = {}) { @@ -110,61 +115,106 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { ); return []; } + if (isInvalidHandler(handlerDef)) { + this._diag.debug( + 'Skipping lambda instrumentation: _HANDLER/lambdaHandler is invalid.', + { 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 - filename += '.cjs'; - } + const [moduleRoot, moduleAndHandler] = moduleRootAndHandler(handlerDef); + const [module, handlerPath] = splitHandlerString(moduleAndHandler); + + if (!module || !handlerPath) { + this._diag.debug( + 'Skipping lambda instrumentation: _HANDLER/lambdaHandler is invalid.', + { taskRoot, handlerDef, moduleRoot, module, handlerPath } + ); + return []; + } + + const filename = tryPath(taskRoot, moduleRoot, module); + if (!filename) { + this._diag.debug( + 'Skipping lambda instrumentation: _HANDLER/lambdaHandler and LAMBDA_TASK_ROOT did not resolve to a file.', + { + taskRoot, + handlerDef, + moduleRoot, + module, + handlerPath, + } + ); + return []; } diag.debug('Instrumenting lambda handler', { taskRoot, handlerDef, - handler, + filename, moduleRoot, module, - filename, - functionName, + handlerPath, }); + const patch = (moduleExports: object) => { + const [container, functionName] = resolveHandler( + moduleExports, + handlerPath + ); + if ( + container == null || + functionName == null || + typeof container[functionName] !== 'function' + ) { + this._diag.debug( + 'Skipping lambda instrumentation: _HANDLER/lambdaHandler did not resolve to a function.', + { + taskRoot, + handlerDef, + filename, + moduleRoot, + module, + handlerPath, + } + ); + return moduleExports; + } + + if (isWrapped(container[functionName])) { + this._unwrap(container, functionName); + } + this._wrap(container, functionName, this._getHandler()); + return moduleExports; + }; + const unpatch = (moduleExports?: object) => { + if (moduleExports == null) return; + const [container, functionName] = resolveHandler( + moduleExports, + handlerPath + ); + if ( + container == null || + functionName == null || + typeof container[functionName] !== 'function' + ) { + return; + } + + this._unwrap(container, functionName); + }; + return [ new InstrumentationNodeModuleDefinition( - // NB: The patching infrastructure seems to match names backwards, this must be the filename, while - // InstrumentationNodeModuleFile must be the module name. + // The patching infrastructure properly supports absolute paths when registering hooks but not when + // actually matching against filenames when patching, so we need to provide a file instrumentation + // that will actually match by using a relative path. filename, ['*'], - undefined, - undefined, - [ - new InstrumentationNodeModuleFile( - module, - ['*'], - (moduleExports: LambdaModule) => { - if (isWrapped(moduleExports[functionName])) { - this._unwrap(moduleExports, functionName); - } - this._wrap(moduleExports, functionName, this._getHandler()); - return moduleExports; - }, - (moduleExports?: LambdaModule) => { - if (moduleExports == null) return; - this._unwrap(moduleExports, functionName); - } - ), - ] + patch, + unpatch, + [new InstrumentationNodeModuleFile(module, ['*'], patch, unpatch)] ), ]; } diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/src/user-function.ts b/plugins/node/opentelemetry-instrumentation-aws-lambda/src/user-function.ts new file mode 100644 index 0000000000..2beecc4942 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/src/user-function.ts @@ -0,0 +1,107 @@ +/** + * Adapted from https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.1.0/src/UserFunction.js + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { LambdaModule } from './internal-types'; + +const FUNCTION_EXPR = /^([^.]*)\.(.*)$/; +const RELATIVE_PATH_SUBSTRING = '..'; + +/** + * Break the full handler string into two pieces, the module root and the actual + * handler string. + * Given './somepath/something/module.nestedobj.handler' this returns + * ['./somepath/something', 'module.nestedobj.handler'] + */ +export function moduleRootAndHandler( + fullHandlerString: string +): [moduleRoot: string, handler: string] { + const handlerString = path.basename(fullHandlerString); + const moduleRoot = fullHandlerString.substring( + 0, + fullHandlerString.indexOf(handlerString) + ); + return [moduleRoot, handlerString]; +} + +/** + * Split the handler string into two pieces: the module name and the path to + * the handler function. + */ +export function splitHandlerString( + handler: string +): [module: string | undefined, functionPath: string | undefined] { + const match = handler.match(FUNCTION_EXPR); + if (match && match.length === 3) { + return [match[1], match[2]]; + } else { + return [undefined, undefined]; + } +} + +/** + * Resolve the user's handler function key and its containing object from the module. + */ +export function resolveHandler( + object: object, + nestedProperty: string +): [container: LambdaModule | undefined, handlerKey: string | undefined] { + const nestedPropertyKeys = nestedProperty.split('.'); + const handlerKey = nestedPropertyKeys.pop(); + + const container = nestedPropertyKeys.reduce( + (nested, key) => { + return nested && (nested as Partial>)[key]; + }, + object + ); + + if (container) { + return [container as LambdaModule, handlerKey]; + } else { + return [undefined, undefined]; + } +} + +/** + * Attempt to determine the user's module path. + */ +export function tryPath( + appRoot: string, + moduleRoot: string, + module: string +): string | undefined { + const lambdaStylePath = path.resolve(appRoot, moduleRoot, module); + + const extensionless = fs.existsSync(lambdaStylePath); + if (extensionless) { + return lambdaStylePath; + } + + const extensioned = + (fs.existsSync(lambdaStylePath + '.js') && lambdaStylePath + '.js') || + (fs.existsSync(lambdaStylePath + '.mjs') && lambdaStylePath + '.mjs') || + (fs.existsSync(lambdaStylePath + '.cjs') && lambdaStylePath + '.cjs'); + if (extensioned) { + return extensioned; + } + + try { + const nodeStylePath = require.resolve(module, { + paths: [appRoot, moduleRoot], + }); + return nodeStylePath; + } catch { + return undefined; + } +} + +export function isInvalidHandler(fullHandlerString: string): boolean { + if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) { + return true; + } else { + return false; + } +} diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.force-flush.test.ts b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.force-flush.test.ts index 0a067dc758..31cffb00c3 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.force-flush.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.force-flush.test.ts @@ -20,6 +20,7 @@ import * as path from 'path'; import { AwsLambdaInstrumentation } from '../../src'; +import { load } from '../vendor/UserFunction'; import { BatchSpanProcessor, InMemorySpanExporter, @@ -71,8 +72,8 @@ describe('force flush', () => { instrumentation.setMeterProvider(provider); }; - const lambdaRequire = (module: string) => - require(path.resolve(__dirname, '..', module)); + const lambdaLoadHandler = (handler: string = process.env._HANDLER!) => + load(process.env.LAMBDA_TASK_ROOT!, handler); beforeEach(() => { oldEnv = { ...process.env }; @@ -100,18 +101,15 @@ describe('force flush', () => { provider.forceFlush = forceFlush; initializeHandlerTracing('lambda-test/sync.handler', provider); + const handler = await lambdaLoadHandler(); await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); }); assert.strictEqual(forceFlushed, true); @@ -134,18 +132,15 @@ describe('force flush', () => { nodeTracerProvider.forceFlush = forceFlush; initializeHandlerTracing('lambda-test/sync.handler', provider); + const handler = await lambdaLoadHandler(); await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); }); assert.strictEqual(forceFlushed, true); @@ -165,18 +160,15 @@ describe('force flush', () => { provider.forceFlush = forceFlush; initializeHandlerMetrics('lambda-test/sync.handler', provider); + const handler = await lambdaLoadHandler(); await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); }); assert.strictEqual(forceFlushed, true); @@ -217,19 +209,16 @@ describe('force flush', () => { instrumentation.setMeterProvider(meterProvider); let callbackCount = 0; + const handler = await lambdaLoadHandler(); await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - callbackCount++; - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + callbackCount++; + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); }); assert.strictEqual(tracerForceFlushed, true); diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts index 2088c4316f..8d0ed12f98 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts @@ -24,6 +24,8 @@ import { AwsLambdaInstrumentationConfig, traceContextEnvironmentKey, } from '../../src'; +import { load } from '../vendor/UserFunction'; +import { runTestFixture } from '@opentelemetry/contrib-test-utils'; import { BatchSpanProcessor, InMemorySpanExporter, @@ -126,8 +128,8 @@ describe('lambda handler', () => { return provider; }; - const lambdaRequire = (module: string) => - require(path.resolve(__dirname, '..', module)); + const lambdaLoadHandler = (handler: string = process.env._HANDLER!) => + load(process.env.LAMBDA_TASK_ROOT!, handler); const sampledAwsSpanContext: SpanContext = { traceId: '8a3c60f7d188f8fa79d48a391a778fa6', @@ -200,10 +202,21 @@ describe('lambda handler', () => { it('should export a valid span', async () => { initializeHandler('lambda-test/async.handler'); - const result = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanSuccess(span); + assert.strictEqual(span.parentSpanId, undefined); + }); + + it('should support nested handlers', async () => { + initializeHandler('lambda-test/async.deeply.nested.handler'); + + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -215,9 +228,10 @@ describe('lambda handler', () => { it('should record error', async () => { initializeHandler('lambda-test/async.error'); + const handler = await lambdaLoadHandler(); let err: Error; try { - await lambdaRequire('lambda-test/async').error('arg', ctx); + await handler('arg', ctx); } catch (e: any) { err = e; } @@ -232,9 +246,10 @@ describe('lambda handler', () => { it('should record string error', async () => { initializeHandler('lambda-test/async.stringerror'); + const handler = await lambdaLoadHandler(); let err: string; try { - await lambdaRequire('lambda-test/async').stringerror('arg', ctx); + await handler('arg', ctx); } catch (e: any) { err = e; } @@ -248,10 +263,8 @@ describe('lambda handler', () => { it('context should have parent trace', async () => { initializeHandler('lambda-test/async.context'); - const result = await lambdaRequire('lambda-test/async').context( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; assert.strictEqual(span.spanContext().traceId, result); @@ -260,10 +273,8 @@ describe('lambda handler', () => { it('context should have parent trace', async () => { initializeHandler('lambda-test/async.context'); - const result = await lambdaRequire('lambda-test/async').context( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; assert.strictEqual(span.spanContext().traceId, result); @@ -274,18 +285,36 @@ describe('lambda handler', () => { it('should export a valid span', async () => { initializeHandler('lambda-test/sync.handler'); + const handler = await lambdaLoadHandler(); const result = await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); + }); + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanSuccess(span); + assert.strictEqual(span.parentSpanId, undefined); + }); + + it('should support nested handlers', async () => { + initializeHandler('lambda-test/sync.deeply.nested.handler'); + + const handler = await lambdaLoadHandler(); + const result = await new Promise((resolve, reject) => { + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); }); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); @@ -298,13 +327,10 @@ describe('lambda handler', () => { it('should record error', async () => { initializeHandler('lambda-test/sync.error'); + const handler = await lambdaLoadHandler(); let err: Error; try { - lambdaRequire('lambda-test/sync').error( - 'arg', - ctx, - (err: Error, res: any) => {} - ); + handler('arg', ctx, (err: Error, res: any) => {}); } catch (e: any) { err = e; } @@ -319,20 +345,17 @@ describe('lambda handler', () => { it('should record error in callback', async () => { initializeHandler('lambda-test/sync.callbackerror'); + const handler = await lambdaLoadHandler(); let err: Error; try { await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').callbackerror( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); }); } catch (e: any) { err = e; @@ -348,13 +371,10 @@ describe('lambda handler', () => { it('should record string error', async () => { initializeHandler('lambda-test/sync.stringerror'); + const handler = await lambdaLoadHandler(); let err: string; try { - lambdaRequire('lambda-test/sync').stringerror( - 'arg', - ctx, - (err: Error, res: any) => {} - ); + handler('arg', ctx, (err: Error, res: any) => {}); } catch (e: any) { err = e; } @@ -369,18 +389,15 @@ describe('lambda handler', () => { it('context should have parent trace', async () => { initializeHandler('lambda-test/sync.context'); + const handler = await lambdaLoadHandler(); const result = await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').context( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); }); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -390,18 +407,15 @@ describe('lambda handler', () => { it('context should have parent trace', async () => { initializeHandler('lambda-test/sync.context'); + const handler = await lambdaLoadHandler(); const result = await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').context( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); }); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -412,20 +426,17 @@ describe('lambda handler', () => { it('should record string error in callback', async () => { initializeHandler('lambda-test/sync.callbackstringerror'); + const handler = await lambdaLoadHandler(); let err: string; try { await new Promise((resolve, reject) => { - lambdaRequire('lambda-test/sync').callbackstringerror( - 'arg', - ctx, - (err: Error, res: any) => { - if (err) { - reject(err); - } else { - resolve(res); - } + handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); } - ); + }); }); } catch (e: any) { err = e; @@ -443,10 +454,8 @@ describe('lambda handler', () => { process.env[traceContextEnvironmentKey] = sampledAwsHeader; initializeHandler('lambda-test/async.handler'); - const result = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -463,10 +472,8 @@ describe('lambda handler', () => { process.env[traceContextEnvironmentKey] = unsampledAwsHeader; initializeHandler('lambda-test/async.handler'); - const result = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); // Parent unsampled so no exported spans. @@ -483,10 +490,8 @@ describe('lambda handler', () => { }, }; - const result = await lambdaRequire('lambda-test/async').handler( - proxyEvent, - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler(proxyEvent, ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -509,10 +514,8 @@ describe('lambda handler', () => { }, }; - const result = await lambdaRequire('lambda-test/async').handler( - proxyEvent, - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler(proxyEvent, ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -535,10 +538,8 @@ describe('lambda handler', () => { }, }; - const result = await lambdaRequire('lambda-test/async').handler( - proxyEvent, - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler(proxyEvent, ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); // Parent unsampled so no spans exported. @@ -550,10 +551,8 @@ describe('lambda handler', () => { process.env[traceContextEnvironmentKey] = sampledAwsHeader; initializeHandler('lambda-test/async.handler', {}); - const result = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -571,10 +570,8 @@ describe('lambda handler', () => { process.env[traceContextEnvironmentKey] = sampledAwsHeader; initializeHandler('lambda-test/async.handler', {}); - const result = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -592,10 +589,8 @@ describe('lambda handler', () => { process.env[traceContextEnvironmentKey] = sampledAwsHeader; initializeHandler('lambda-test/async.handler', {}); - const result = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -615,10 +610,8 @@ describe('lambda handler', () => { disableAwsContextPropagation: false, }); - const result = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -637,10 +630,8 @@ describe('lambda handler', () => { disableAwsContextPropagation: true, }); - const result = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; @@ -665,10 +656,8 @@ describe('lambda handler', () => { }, }; - const result = await lambdaRequire('lambda-test/async').handler( - proxyEvent, - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler(proxyEvent, ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); @@ -699,10 +688,8 @@ describe('lambda handler', () => { }, }; - const result = await lambdaRequire('lambda-test/async').handler( - otherEvent, - ctx - ); + const handler = await lambdaLoadHandler(); + const result = await handler(otherEvent, ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); @@ -738,11 +725,8 @@ describe('lambda handler', () => { }, }; - const lambdaTestAsync = lambdaRequire('lambda-test/async'); - const actual = await lambdaTestAsync.handler_return_baggage( - customRemoteEvent, - ctx - ); + const handler = await lambdaLoadHandler(); + const actual = await handler(customRemoteEvent, ctx); assert.strictEqual(actual, baggage); }); @@ -762,14 +746,12 @@ describe('lambda handler', () => { eventContextExtractor: customExtractor, }); + const handler = await lambdaLoadHandler(); const testSpan = provider.getTracer('test').startSpan('random_span'); await context.with( trace.setSpan(context.active(), testSpan), async () => { - await lambdaRequire('lambda-test/async').handler( - { message: 'event with no context' }, - ctx - ); + await handler({ message: 'event with no context' }, ctx); } ); @@ -819,10 +801,8 @@ describe('lambda handler', () => { }, } as Context; - const result = await lambdaRequire('lambda-test/async').handler( - otherEvent, - ctxWithCustomData - ); + const handler = await lambdaLoadHandler(); + const result = await handler(otherEvent, ctxWithCustomData); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); @@ -846,7 +826,8 @@ describe('lambda handler', () => { }, }); - await lambdaRequire('lambda-test/async').handler('arg', ctx); + const handler = await lambdaLoadHandler(); + await handler('arg', ctx); const spans = memoryExporter.getFinishedSpans(); const [span] = spans; assert.strictEqual(spans.length, 1); @@ -879,10 +860,8 @@ describe('lambda handler', () => { it('async - success', async () => { initializeHandler('lambda-test/async.handler', config); - const res = await lambdaRequire('lambda-test/async').handler( - 'arg', - ctx - ); + const handler = await lambdaLoadHandler(); + const res = await handler('arg', ctx); const [span] = memoryExporter.getFinishedSpans(); assert.strictEqual(span.attributes[RES_ATTR], res); }); @@ -890,9 +869,10 @@ describe('lambda handler', () => { it('async - error', async () => { initializeHandler('lambda-test/async.error', config); + const handler = await lambdaLoadHandler(); let err: Error; try { - await lambdaRequire('lambda-test/async').error('arg', ctx); + await handler('arg', ctx); } catch (e: any) { err = e; } @@ -903,12 +883,9 @@ describe('lambda handler', () => { it('sync - success', async () => { initializeHandler('lambda-test/sync.handler', config); + const handler = await lambdaLoadHandler(); const result = await new Promise((resolve, _reject) => { - lambdaRequire('lambda-test/sync').handler( - 'arg', - ctx, - (_err: Error, res: any) => resolve(res) - ); + handler('arg', ctx, (_err: Error, res: any) => resolve(res)); }); const [span] = memoryExporter.getFinishedSpans(); assert.strictEqual(span.attributes[RES_ATTR], result); @@ -917,9 +894,10 @@ describe('lambda handler', () => { it('sync - error', async () => { initializeHandler('lambda-test/sync.error', config); + const handler = await lambdaLoadHandler(); let err: Error; try { - lambdaRequire('lambda-test/sync').error('arg', ctx, () => {}); + handler('arg', ctx, () => {}); } catch (e: any) { err = e; } @@ -930,35 +908,79 @@ describe('lambda handler', () => { it('sync - error with callback', async () => { initializeHandler('lambda-test/sync.callbackerror', config); + const handler = await lambdaLoadHandler(); let error: Error; await new Promise((resolve, _reject) => { - lambdaRequire('lambda-test/sync').callbackerror( - 'arg', - ctx, - (err: Error, _res: any) => { - error = err; - resolve({}); - } - ); + handler('arg', ctx, (err: Error, _res: any) => { + error = err; + resolve({}); + }); }); const [span] = memoryExporter.getFinishedSpans(); assert.strictEqual(span.attributes[ERR_ATTR], error!.message); }); }); + }); - describe('.cjs lambda bundle', () => { - it('should export a valid span', async () => { - initializeHandler('lambda-test/commonjs.handler'); - const result = await lambdaRequire('lambda-test/commonjs.cjs').handler( - 'arg', - ctx - ); - assert.strictEqual(result, 'ok'); - const spans = memoryExporter.getFinishedSpans(); - const [span] = spans; - assert.strictEqual(spans.length, 1); - assertSpanSuccess(span); - assert.strictEqual(span.parentSpanId, undefined); + describe('extensionless lambda bundle', () => { + it('should export a valid span', async () => { + initializeHandler('lambda-test/extensionless.handler'); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanSuccess(span); + assert.strictEqual(span.parentSpanId, undefined); + }); + }); + + describe('.cjs lambda bundle', () => { + it('should export a valid span', async () => { + initializeHandler('lambda-test/commonjs.handler'); + const handler = await lambdaLoadHandler(); + const result = await handler('arg', ctx); + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanSuccess(span); + assert.strictEqual(span.parentSpanId, undefined); + }); + }); + + describe('.mjs lambda bundle', () => { + it('should export a valid span', async () => { + await runTestFixture({ + cwd: path.dirname(__dirname), + argv: ['lambda-test/use-lambda.mjs'], + env: { + NODE_OPTIONS: + '--experimental-loader=@opentelemetry/instrumentation/hook.mjs', + NODE_NO_WARNINGS: '1', + }, + checkResult: err => { + assert.ifError(err); + }, + checkCollector: collector => { + const spans = collector.sortedSpans; + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.kind, 2); + assert.strictEqual(span.name, 'my_function'); + assert.strictEqual( + span.attributes.find(a => a.key === SEMATTRS_FAAS_EXECUTION)?.value + .stringValue, + 'aws_request_id' + ); + assert.strictEqual( + span.attributes.find(a => a.key === 'faas.id')?.value.stringValue, + 'my_arn' + ); + assert.strictEqual(span.status.code, 0); + assert.strictEqual(span.status.message, undefined); + }, }); }); }); @@ -970,10 +992,8 @@ describe('lambda handler', () => { }); const otherEvent = {}; - const result = await lambdaRequire('lambda-test/async').handler( - otherEvent, - ctx - ); + const handler = await lambdaLoadHandler('lambda-test/async.handler'); + const result = await handler(otherEvent, ctx); assert.strictEqual(result, 'ok'); const spans = memoryExporter.getFinishedSpans(); @@ -983,4 +1003,67 @@ describe('lambda handler', () => { assert.strictEqual(span.parentSpanId, undefined); }); }); + + describe('missing handler', () => { + it('should skip instrumentation', async () => { + const handler = await lambdaLoadHandler('lambda-test/async.handler'); + const result = await handler('arg', ctx); + + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + }); + + describe('invalid handler string', () => { + it('should skip instrumentation', async () => { + initializeHandler('lambda-test/async..handler'); + + const handler = await lambdaLoadHandler('lambda-test/async.handler'); + const result = await handler('arg', ctx); + + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + }); + + describe('missing handler function name', () => { + it('should skip instrumentation', async () => { + initializeHandler('lambda-test/async'); + + const handler = await lambdaLoadHandler('lambda-test/async.handler'); + const result = await handler('arg', ctx); + + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + }); + + describe('non-existent handler function', () => { + it('should skip instrumentation', async () => { + initializeHandler('lambda-test/async.handle'); + + const handler = await lambdaLoadHandler('lambda-test/async.handler'); + const result = await handler('arg', ctx); + + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + }); + + describe('non-existent nested handler function', () => { + it('should skip instrumentation', async () => { + initializeHandler('lambda-test/async.nested.handle'); + + const handler = await lambdaLoadHandler('lambda-test/async.handler'); + const result = await handler('arg', ctx); + + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + }); }); diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/async.js b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/async.js index c6c2e529cf..d7cdb1eca3 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/async.js +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/async.js @@ -19,19 +19,29 @@ exports.handler = async function (event, context) { return 'ok'; }; +exports.deeply = { + nested: { + handler: async function (event, context) { + return 'ok'; + }, + }, +}; + exports.error = async function (event, context) { throw new Error('handler error'); -} +}; exports.stringerror = async function (event, context) { throw 'handler error'; -} +}; exports.context = async function (event, context) { return api.trace.getSpan(api.context.active()).spanContext().traceId; }; exports.handler_return_baggage = async function (event, context) { - const [baggageEntryKey, baggageEntryValue] = api.propagation.getBaggage(api.context.active()).getAllEntries()[0]; + const [baggageEntryKey, baggageEntryValue] = api.propagation + .getBaggage(api.context.active()) + .getAllEntries()[0]; return `${baggageEntryKey}=${baggageEntryValue.value}`; -} +}; diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/commonjs.cjs b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/commonjs.cjs index 3fc61adccf..a14326637a 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/commonjs.cjs +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/commonjs.cjs @@ -1,3 +1,3 @@ exports.handler = async function (event, context) { - return "ok"; -}; \ No newline at end of file + return 'ok'; +}; diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/extensionless b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/extensionless new file mode 100644 index 0000000000..06c24dedcb --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/extensionless @@ -0,0 +1,4 @@ +#!/usr/bin/env node +exports.handler = async function (event, context) { + return 'ok'; +}; diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/module.mjs b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/module.mjs new file mode 100644 index 0000000000..e223a09a86 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/module.mjs @@ -0,0 +1,3 @@ +export async function handler(event, context) { + return 'ok'; +} diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/sync.js b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/sync.js index e9f38d8ea7..4dd043274e 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/sync.js +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/sync.js @@ -19,21 +19,29 @@ exports.handler = function (event, context, callback) { callback(null, 'ok'); }; +exports.deeply = { + nested: { + handler: function (event, context, callback) { + callback(null, 'ok'); + }, + }, +}; + exports.error = function (event, context, callback) { throw new Error('handler error'); -} +}; exports.callbackerror = function (event, context, callback) { callback(new Error('handler error')); -} +}; exports.stringerror = function (event, context, callback) { throw 'handler error'; -} +}; exports.callbackstringerror = function (event, context, callback) { callback('handler error'); -} +}; exports.context = function (event, context, callback) { callback(null, api.trace.getSpan(api.context.active()).spanContext().traceId); diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/use-lambda.mjs b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/use-lambda.mjs new file mode 100644 index 0000000000..04e4b80685 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/lambda-test/use-lambda.mjs @@ -0,0 +1,26 @@ +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +import { trace } from '@opentelemetry/api'; +import { createTestNodeSdk } from '@opentelemetry/contrib-test-utils'; + +import { AwsLambdaInstrumentation } from '../../build/src/index.js'; +import { load } from '../vendor/UserFunction.js'; + +process.env.LAMBDA_TASK_ROOT = path.dirname(fileURLToPath(import.meta.url)); +process.env._HANDLER = 'module.handler'; + +const instrumentation = new AwsLambdaInstrumentation(); +const sdk = createTestNodeSdk({ + serviceName: 'use-lambda', + instrumentations: [instrumentation], +}); +sdk.start(); +instrumentation.setTracerProvider(trace.getTracerProvider()); + +const handler = await load(process.env.LAMBDA_TASK_ROOT, process.env._HANDLER); +await handler('arg', { + functionName: 'my_function', + invokedFunctionArn: 'my_arn', + awsRequestId: 'aws_request_id', +}); diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/Errors.js b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/Errors.js new file mode 100644 index 0000000000..d34c2f5bac --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/Errors.js @@ -0,0 +1,123 @@ +// https://raw.githubusercontent.com/aws/aws-lambda-nodejs-runtime-interface-client/v3.1.0/src/Errors.js + +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Defines custom error types throwable by the runtime. + */ + +'use strict'; + +const util = require('util'); + +function _isError(obj) { + return ( + obj && + obj.name && + obj.message && + obj.stack && + typeof obj.name === 'string' && + typeof obj.message === 'string' && + typeof obj.stack === 'string' + ); +} + +function intoError(err) { + if (err instanceof Error) { + return err; + } else { + return new Error(err); + } +} + +module.exports.intoError = intoError; + +/** + * Attempt to convert an object into a response object. + * This method accounts for failures when serializing the error object. + */ +function toRapidResponse(error) { + try { + if (util.types.isNativeError(error) || _isError(error)) { + return { + errorType: error.name, + errorMessage: error.message, + trace: error.stack.split('\n'), + }; + } else { + return { + errorType: typeof error, + errorMessage: error.toString(), + trace: [], + }; + } + } catch (_err) { + return { + errorType: 'handled', + errorMessage: + 'callback called with Error argument, but there was a problem while retrieving one or more of its message, name, and stack', + }; + } +} + +module.exports.toRapidResponse = toRapidResponse; + +/** + * Format an error with the expected properties. + * For compatability, the error string always starts with a tab. + */ +module.exports.toFormatted = error => { + try { + return ( + '\t' + JSON.stringify(error, (_k, v) => _withEnumerableProperties(v)) + ); + } catch (err) { + return '\t' + JSON.stringify(toRapidResponse(error)); + } +}; + +/** + * Error name, message, code, and stack are all members of the superclass, which + * means they aren't enumerable and don't normally show up in JSON.stringify. + * This method ensures those interesting properties are available along with any + * user-provided enumerable properties. + */ +function _withEnumerableProperties(error) { + if (error instanceof Error) { + let ret = Object.assign( + { + errorType: error.name, + errorMessage: error.message, + code: error.code, + }, + error + ); + if (typeof error.stack == 'string') { + ret.stack = error.stack.split('\n'); + } + return ret; + } else { + return error; + } +} + +const errorClasses = [ + class ImportModuleError extends Error {}, + class HandlerNotFound extends Error {}, + class MalformedHandlerName extends Error {}, + class UserCodeSyntaxError extends Error {}, + class MalformedStreamingHandler extends Error {}, + class InvalidStreamingOperation extends Error {}, + class UnhandledPromiseRejection extends Error { + constructor(reason, promise) { + super(reason); + this.reason = reason; + this.promise = promise; + } + }, +]; + +errorClasses.forEach(e => { + module.exports[e.name] = e; + e.prototype.name = `Runtime.${e.name}`; +}); diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/HttpResponseStream.js b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/HttpResponseStream.js new file mode 100644 index 0000000000..4e1386a03b --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/HttpResponseStream.js @@ -0,0 +1,35 @@ +// https://raw.githubusercontent.com/aws/aws-lambda-nodejs-runtime-interface-client/v3.1.0/src/HttpResponseStream.js + +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * HttpResponseStream is NOT used by the runtime. + * It is only exposed in the `awslambda` variable for customers to use. + */ + +'use strict'; + +const METADATA_PRELUDE_CONTENT_TYPE = + 'application/vnd.awslambda.http-integration-response'; +const DELIMITER_LEN = 8; + +// Implements the application/vnd.awslambda.http-integration-response content type. +class HttpResponseStream { + static from(underlyingStream, prelude) { + underlyingStream.setContentType(METADATA_PRELUDE_CONTENT_TYPE); + + // JSON.stringify is required. NULL byte is not allowed in metadataPrelude. + const metadataPrelude = JSON.stringify(prelude); + + underlyingStream._onBeforeFirstWrite = write => { + write(metadataPrelude); + + // Write 8 null bytes after the JSON prelude. + write(new Uint8Array(DELIMITER_LEN)); + }; + + return underlyingStream; + } +} + +module.exports.HttpResponseStream = HttpResponseStream; diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/UserFunction.d.ts b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/UserFunction.d.ts new file mode 100644 index 0000000000..89b7991ebb --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/UserFunction.d.ts @@ -0,0 +1,4 @@ +export declare function load( + appRoot: string, + fullHandlerString: string +): Promise; diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/UserFunction.js b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/UserFunction.js new file mode 100644 index 0000000000..1fb58c00c0 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/test/vendor/UserFunction.js @@ -0,0 +1,301 @@ +// https://raw.githubusercontent.com/aws/aws-lambda-nodejs-runtime-interface-client/v3.1.0/src/UserFunction.js + +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * This module defines the functions for loading the user's code as specified + * in a handler string. + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const { + HandlerNotFound, + MalformedHandlerName, + ImportModuleError, + UserCodeSyntaxError, +} = require('./Errors'); +const { HttpResponseStream } = require('./HttpResponseStream'); + +const FUNCTION_EXPR = /^([^.]*)\.(.*)$/; +const RELATIVE_PATH_SUBSTRING = '..'; +const HANDLER_STREAMING = Symbol.for('aws.lambda.runtime.handler.streaming'); +const HANDLER_HIGHWATERMARK = Symbol.for( + 'aws.lambda.runtime.handler.streaming.highWaterMark' +); +const STREAM_RESPONSE = 'response'; + +// `awslambda.streamifyResponse` function is provided by default. +const NoGlobalAwsLambda = + process.env['AWS_LAMBDA_NODEJS_NO_GLOBAL_AWSLAMBDA'] === '1' || + process.env['AWS_LAMBDA_NODEJS_NO_GLOBAL_AWSLAMBDA'] === 'true'; + +/** + * Break the full handler string into two pieces, the module root and the actual + * handler string. + * Given './somepath/something/module.nestedobj.handler' this returns + * ['./somepath/something', 'module.nestedobj.handler'] + */ +function _moduleRootAndHandler(fullHandlerString) { + let handlerString = path.basename(fullHandlerString); + let moduleRoot = fullHandlerString.substring( + 0, + fullHandlerString.indexOf(handlerString) + ); + return [moduleRoot, handlerString]; +} + +/** + * Split the handler string into two pieces: the module name and the path to + * the handler function. + */ +function _splitHandlerString(handler) { + let match = handler.match(FUNCTION_EXPR); + if (!match || match.length != 3) { + throw new MalformedHandlerName('Bad handler'); + } + return [match[1], match[2]]; // [module, function-path] +} + +/** + * Resolve the user's handler function from the module. + */ +function _resolveHandler(object, nestedProperty) { + return nestedProperty.split('.').reduce((nested, key) => { + return nested && nested[key]; + }, object); +} + +function _tryRequireFile(file, extension) { + const path = file + (extension || ''); + return fs.existsSync(path) ? require(path) : undefined; +} + +async function _tryAwaitImport(file, extension) { + const path = file + (extension || ''); + + if (fs.existsSync(path)) { + return await import(path); + } + + return undefined; +} + +function _hasFolderPackageJsonTypeModule(folder) { + // Check if package.json exists, return true if type === "module" in package json. + // If there is no package.json, and there is a node_modules, return false. + // Check parent folder otherwise, if there is one. + if (folder.endsWith('/node_modules')) { + return false; + } + + const pj = path.join(folder, '/package.json'); + if (fs.existsSync(pj)) { + try { + const pkg = JSON.parse(fs.readFileSync(pj)); + if (pkg) { + if (pkg.type === 'module') { + return true; + } else { + return false; + } + } + } catch (e) { + console.warn( + `${pj} cannot be read, it will be ignored for ES module detection purposes.`, + e + ); + return false; + } + } + + if (folder === '/') { + // We have reached root without finding either a package.json or a node_modules. + return false; + } + + return _hasFolderPackageJsonTypeModule(path.resolve(folder, '..')); +} + +function _hasPackageJsonTypeModule(file) { + // File must have a .js extension + const jsPath = file + '.js'; + return fs.existsSync(jsPath) + ? _hasFolderPackageJsonTypeModule(path.resolve(path.dirname(jsPath))) + : false; +} + +/** + * Attempt to load the user's module. + * Attempts to directly resolve the module relative to the application root, + * then falls back to the more general require(). + */ +async function _tryRequire(appRoot, moduleRoot, module) { + const lambdaStylePath = path.resolve(appRoot, moduleRoot, module); + + // Extensionless files are loaded via require. + const extensionless = _tryRequireFile(lambdaStylePath); + if (extensionless) { + return extensionless; + } + + // If package.json type != module, .js files are loaded via require. + const pjHasModule = _hasPackageJsonTypeModule(lambdaStylePath); + if (!pjHasModule) { + const loaded = _tryRequireFile(lambdaStylePath, '.js'); + if (loaded) { + return loaded; + } + } + + // If still not loaded, try .js, .mjs, and .cjs in that order. + // Files ending with .js are loaded as ES modules when the nearest parent package.json + // file contains a top-level field "type" with a value of "module". + // https://nodejs.org/api/packages.html#packages_type + const loaded = + (pjHasModule && (await _tryAwaitImport(lambdaStylePath, '.js'))) || + (await _tryAwaitImport(lambdaStylePath, '.mjs')) || + _tryRequireFile(lambdaStylePath, '.cjs'); + if (loaded) { + return loaded; + } + + // Why not just require(module)? + // Because require() is relative to __dirname, not process.cwd(). And the + // runtime implementation is not located in /var/task + // This won't work (yet) for esModules as import.meta.resolve is still experimental + // See: https://nodejs.org/api/esm.html#esm_import_meta_resolve_specifier_parent + const nodeStylePath = require.resolve(module, { + paths: [appRoot, moduleRoot], + }); + + return require(nodeStylePath); +} + +/** + * Load the user's application or throw a descriptive error. + * @throws Runtime errors in two cases + * 1 - UserCodeSyntaxError if there's a syntax error while loading the module + * 2 - ImportModuleError if the module cannot be found + */ +async function _loadUserApp(appRoot, moduleRoot, module) { + if (!NoGlobalAwsLambda) { + globalThis.awslambda = { + streamifyResponse: (handler, options) => { + handler[HANDLER_STREAMING] = STREAM_RESPONSE; + if (typeof options?.highWaterMark === 'number') { + handler[HANDLER_HIGHWATERMARK] = parseInt(options.highWaterMark); + } + return handler; + }, + HttpResponseStream: HttpResponseStream, + }; + } + + try { + return await _tryRequire(appRoot, moduleRoot, module); + } catch (e) { + if (e instanceof SyntaxError) { + throw new UserCodeSyntaxError(e); + } else if (e.code !== undefined && e.code === 'MODULE_NOT_FOUND') { + throw new ImportModuleError(e); + } else { + throw e; + } + } +} + +function _throwIfInvalidHandler(fullHandlerString) { + if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) { + throw new MalformedHandlerName( + `'${fullHandlerString}' is not a valid handler name. Use absolute paths when specifying root directories in handler names.` + ); + } +} + +function _isHandlerStreaming(handler) { + if ( + typeof handler[HANDLER_STREAMING] === 'undefined' || + handler[HANDLER_STREAMING] === null || + handler[HANDLER_STREAMING] === false + ) { + return false; + } + + if (handler[HANDLER_STREAMING] === STREAM_RESPONSE) { + return STREAM_RESPONSE; + } else { + throw new MalformedStreamingHandler( + 'Only response streaming is supported.' + ); + } +} + +function _highWaterMark(handler) { + if ( + typeof handler[HANDLER_HIGHWATERMARK] === 'undefined' || + handler[HANDLER_HIGHWATERMARK] === null || + handler[HANDLER_HIGHWATERMARK] === false + ) { + return undefined; + } + + const hwm = parseInt(handler[HANDLER_HIGHWATERMARK]); + return isNaN(hwm) ? undefined : hwm; +} + +/** + * Load the user's function with the approot and the handler string. + * @param appRoot {string} + * The path to the application root. + * @param handlerString {string} + * The user-provided handler function in the form 'module.function'. + * @return userFuction {function} + * The user's handler function. This function will be passed the event body, + * the context object, and the callback function. + * @throws In five cases:- + * 1 - if the handler string is incorrectly formatted an error is thrown + * 2 - if the module referenced by the handler cannot be loaded + * 3 - if the function in the handler does not exist in the module + * 4 - if a property with the same name, but isn't a function, exists on the + * module + * 5 - the handler includes illegal character sequences (like relative paths + * for traversing up the filesystem '..') + * Errors for scenarios known by the runtime, will be wrapped by Runtime.* errors. + */ +module.exports.load = async function (appRoot, fullHandlerString) { + _throwIfInvalidHandler(fullHandlerString); + + let [moduleRoot, moduleAndHandler] = _moduleRootAndHandler(fullHandlerString); + let [module, handlerPath] = _splitHandlerString(moduleAndHandler); + + let userApp = await _loadUserApp(appRoot, moduleRoot, module); + let handlerFunc = _resolveHandler(userApp, handlerPath); + + if (!handlerFunc) { + throw new HandlerNotFound( + `${fullHandlerString} is undefined or not exported` + ); + } + + if (typeof handlerFunc !== 'function') { + throw new HandlerNotFound(`${fullHandlerString} is not a function`); + } + + return handlerFunc; +}; + +module.exports.isHandlerFunction = function (value) { + return typeof value === 'function'; +}; + +module.exports.getHandlerMetadata = function (handlerFunc) { + return { + streaming: _isHandlerStreaming(handlerFunc), + highWaterMark: _highWaterMark(handlerFunc), + }; +}; + +module.exports.STREAM_RESPONSE = STREAM_RESPONSE; diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/tsconfig.json b/plugins/node/opentelemetry-instrumentation-aws-lambda/tsconfig.json index 28be80d266..47c24b6178 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/tsconfig.json +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/tsconfig.json @@ -4,8 +4,5 @@ "rootDir": ".", "outDir": "build" }, - "include": [ - "src/**/*.ts", - "test/**/*.ts" - ] + "include": ["src/**/*.ts", "test/**/*.ts", "test/vendor/**/*"] }