diff --git a/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts index 4aef1509ce..a705ca8b25 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts @@ -44,6 +44,7 @@ import { AWSXRayPropagator, } from '@opentelemetry/propagator-aws-xray'; import { + SEMATTRS_FAAS_COLDSTART, SEMATTRS_FAAS_EXECUTION, SEMRESATTRS_CLOUD_ACCOUNT_ID, SEMRESATTRS_FAAS_ID, @@ -72,6 +73,7 @@ const headerGetter: TextMapGetter = { }; export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID'; +export const lambdaMaxInitInMilliseconds = 10_000; export class AwsLambdaInstrumentation extends InstrumentationBase { private _traceForceFlusher?: () => Promise; @@ -135,6 +137,10 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { @@ -164,16 +174,47 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { - return this._getPatchHandler(original); + return this._getPatchHandler(original, handlerLoadStartTime); }; } - private _getPatchHandler(original: Handler) { + private _getPatchHandler(original: Handler, lambdaStartTime: number) { diag.debug('patch handler function'); const plugin = this; + let requestHandledBefore = false; + let requestIsColdStart = true; + + function _onRequest(): void { + if (requestHandledBefore) { + // Non-first requests cannot be coldstart. + requestIsColdStart = false; + } else { + if ( + process.env.AWS_LAMBDA_INITIALIZATION_TYPE === + 'provisioned-concurrency' + ) { + // If sandbox environment is initialized with provisioned concurrency, + // even the first requests should not be considered as coldstart. + requestIsColdStart = false; + } else { + // Check whether it is proactive initialization or not: + // https://aaronstuyvenberg.com/posts/understanding-proactive-initialization + const passedTimeSinceHandlerLoad: number = + Date.now() - lambdaStartTime; + const proactiveInitialization: boolean = + passedTimeSinceHandlerLoad > lambdaMaxInitInMilliseconds; + + // If sandbox has been initialized proactively before the actual request, + // even the first requests should not be considered as coldstart. + requestIsColdStart = !proactiveInitialization; + } + requestHandledBefore = true; + } + } + return function patchedHandler( this: never, // The event can be a user type, it truly is any. @@ -182,6 +223,8 @@ export class AwsLambdaInstrumentation extends InstrumentationBase { assert.strictEqual(span.parentSpanId, undefined); }); + it('should record coldstart', async () => { + initializeHandler('lambda-test/sync.handler'); + + const handlerModule = lambdaRequire('lambda-test/sync'); + + const result1 = await new Promise((resolve, reject) => { + handlerModule.handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); + + const result2 = await new Promise((resolve, reject) => { + handlerModule.handler('arg', ctx, (err: Error, res: any) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + const [span1, span2] = spans; + + assert.strictEqual(result1, 'ok'); + assertSpanSuccess(span1); + assert.strictEqual(span1.parentSpanId, undefined); + assert.strictEqual(span1.attributes[SEMATTRS_FAAS_COLDSTART], true); + + assert.strictEqual(result2, 'ok'); + assertSpanSuccess(span2); + assert.strictEqual(span2.parentSpanId, undefined); + assert.strictEqual(span2.attributes[SEMATTRS_FAAS_COLDSTART], false); + }); + + it('should record coldstart with provisioned concurrency', async () => { + process.env.AWS_LAMBDA_INITIALIZATION_TYPE = 'provisioned-concurrency'; + + initializeHandler('lambda-test/sync.handler'); + + 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); + } + } + ); + }); + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanSuccess(span); + assert.strictEqual(span.parentSpanId, undefined); + assert.strictEqual(span.attributes[SEMATTRS_FAAS_COLDSTART], false); + }); + + it('should record coldstart with proactive initialization', async () => { + initializeHandler('lambda-test/sync.handler', { + lambdaStartTime: Date.now() - 2 * lambdaMaxInitInMilliseconds, + }); + + 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); + } + } + ); + }); + assert.strictEqual(result, 'ok'); + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assertSpanSuccess(span); + assert.strictEqual(span.parentSpanId, undefined); + assert.strictEqual(span.attributes[SEMATTRS_FAAS_COLDSTART], false); + }); + it('should record error', async () => { initializeHandler('lambda-test/sync.error');