Skip to content

Commit bc69fff

Browse files
serkan-ozaldyladan
andauthored
feat: Record AWS Lambda coldstarts (#2403)
Co-authored-by: Daniel Dyla <[email protected]>
1 parent ec3b9c8 commit bc69fff

File tree

3 files changed

+145
-4
lines changed

3 files changed

+145
-4
lines changed

plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
SEMRESATTRS_CLOUD_ACCOUNT_ID,
4949
SEMRESATTRS_FAAS_ID,
5050
} from '@opentelemetry/semantic-conventions';
51+
import { ATTR_FAAS_COLDSTART } from '@opentelemetry/semantic-conventions/incubating';
5152

5253
import {
5354
APIGatewayProxyEventHeaders,
@@ -72,6 +73,7 @@ const headerGetter: TextMapGetter<APIGatewayProxyEventHeaders> = {
7273
};
7374

7475
export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID';
76+
export const lambdaMaxInitInMilliseconds = 10_000;
7577

7678
export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstrumentationConfig> {
7779
private _traceForceFlusher?: () => Promise<void>;
@@ -135,6 +137,10 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
135137
functionName,
136138
});
137139

140+
const lambdaStartTime =
141+
this.getConfig().lambdaStartTime ||
142+
Date.now() - Math.floor(1000 * process.uptime());
143+
138144
return [
139145
new InstrumentationNodeModuleDefinition(
140146
// NB: The patching infrastructure seems to match names backwards, this must be the filename, while
@@ -151,7 +157,11 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
151157
if (isWrapped(moduleExports[functionName])) {
152158
this._unwrap(moduleExports, functionName);
153159
}
154-
this._wrap(moduleExports, functionName, this._getHandler());
160+
this._wrap(
161+
moduleExports,
162+
functionName,
163+
this._getHandler(lambdaStartTime)
164+
);
155165
return moduleExports;
156166
},
157167
(moduleExports?: LambdaModule) => {
@@ -164,16 +174,47 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
164174
];
165175
}
166176

167-
private _getHandler() {
177+
private _getHandler(handlerLoadStartTime: number) {
168178
return (original: Handler) => {
169-
return this._getPatchHandler(original);
179+
return this._getPatchHandler(original, handlerLoadStartTime);
170180
};
171181
}
172182

173-
private _getPatchHandler(original: Handler) {
183+
private _getPatchHandler(original: Handler, lambdaStartTime: number) {
174184
diag.debug('patch handler function');
175185
const plugin = this;
176186

187+
let requestHandledBefore = false;
188+
let requestIsColdStart = true;
189+
190+
function _onRequest(): void {
191+
if (requestHandledBefore) {
192+
// Non-first requests cannot be coldstart.
193+
requestIsColdStart = false;
194+
} else {
195+
if (
196+
process.env.AWS_LAMBDA_INITIALIZATION_TYPE ===
197+
'provisioned-concurrency'
198+
) {
199+
// If sandbox environment is initialized with provisioned concurrency,
200+
// even the first requests should not be considered as coldstart.
201+
requestIsColdStart = false;
202+
} else {
203+
// Check whether it is proactive initialization or not:
204+
// https://aaronstuyvenberg.com/posts/understanding-proactive-initialization
205+
const passedTimeSinceHandlerLoad: number =
206+
Date.now() - lambdaStartTime;
207+
const proactiveInitialization: boolean =
208+
passedTimeSinceHandlerLoad > lambdaMaxInitInMilliseconds;
209+
210+
// If sandbox has been initialized proactively before the actual request,
211+
// even the first requests should not be considered as coldstart.
212+
requestIsColdStart = !proactiveInitialization;
213+
}
214+
requestHandledBefore = true;
215+
}
216+
}
217+
177218
return function patchedHandler(
178219
this: never,
179220
// The event can be a user type, it truly is any.
@@ -182,6 +223,8 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
182223
context: Context,
183224
callback: Callback
184225
) {
226+
_onRequest();
227+
185228
const config = plugin.getConfig();
186229
const parent = AwsLambdaInstrumentation._determineParent(
187230
event,
@@ -203,6 +246,7 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
203246
AwsLambdaInstrumentation._extractAccountId(
204247
context.invokedFunctionArn
205248
),
249+
[ATTR_FAAS_COLDSTART]: requestIsColdStart,
206250
},
207251
},
208252
parent

plugins/node/opentelemetry-instrumentation-aws-lambda/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ export interface AwsLambdaInstrumentationConfig extends InstrumentationConfig {
4141
disableAwsContextPropagation?: boolean;
4242
eventContextExtractor?: EventContextExtractor;
4343
lambdaHandler?: string;
44+
lambdaStartTime?: number;
4445
}

plugins/node/opentelemetry-instrumentation-aws-lambda/test/integrations/lambda-handler.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
AwsLambdaInstrumentation,
2424
AwsLambdaInstrumentationConfig,
2525
traceContextEnvironmentKey,
26+
lambdaMaxInitInMilliseconds,
2627
} from '../../src';
2728
import {
2829
BatchSpanProcessor,
@@ -34,6 +35,7 @@ import { Context } from 'aws-lambda';
3435
import * as assert from 'assert';
3536
import {
3637
SEMATTRS_EXCEPTION_MESSAGE,
38+
SEMATTRS_FAAS_COLDSTART,
3739
SEMATTRS_FAAS_EXECUTION,
3840
SEMRESATTRS_FAAS_NAME,
3941
} from '@opentelemetry/semantic-conventions';
@@ -295,6 +297,100 @@ describe('lambda handler', () => {
295297
assert.strictEqual(span.parentSpanId, undefined);
296298
});
297299

300+
it('should record coldstart', async () => {
301+
initializeHandler('lambda-test/sync.handler');
302+
303+
const handlerModule = lambdaRequire('lambda-test/sync');
304+
305+
const result1 = await new Promise((resolve, reject) => {
306+
handlerModule.handler('arg', ctx, (err: Error, res: any) => {
307+
if (err) {
308+
reject(err);
309+
} else {
310+
resolve(res);
311+
}
312+
});
313+
});
314+
315+
const result2 = await new Promise((resolve, reject) => {
316+
handlerModule.handler('arg', ctx, (err: Error, res: any) => {
317+
if (err) {
318+
reject(err);
319+
} else {
320+
resolve(res);
321+
}
322+
});
323+
});
324+
325+
const spans = memoryExporter.getFinishedSpans();
326+
assert.strictEqual(spans.length, 2);
327+
const [span1, span2] = spans;
328+
329+
assert.strictEqual(result1, 'ok');
330+
assertSpanSuccess(span1);
331+
assert.strictEqual(span1.parentSpanId, undefined);
332+
assert.strictEqual(span1.attributes[SEMATTRS_FAAS_COLDSTART], true);
333+
334+
assert.strictEqual(result2, 'ok');
335+
assertSpanSuccess(span2);
336+
assert.strictEqual(span2.parentSpanId, undefined);
337+
assert.strictEqual(span2.attributes[SEMATTRS_FAAS_COLDSTART], false);
338+
});
339+
340+
it('should record coldstart with provisioned concurrency', async () => {
341+
process.env.AWS_LAMBDA_INITIALIZATION_TYPE = 'provisioned-concurrency';
342+
343+
initializeHandler('lambda-test/sync.handler');
344+
345+
const result = await new Promise((resolve, reject) => {
346+
lambdaRequire('lambda-test/sync').handler(
347+
'arg',
348+
ctx,
349+
(err: Error, res: any) => {
350+
if (err) {
351+
reject(err);
352+
} else {
353+
resolve(res);
354+
}
355+
}
356+
);
357+
});
358+
assert.strictEqual(result, 'ok');
359+
const spans = memoryExporter.getFinishedSpans();
360+
const [span] = spans;
361+
assert.strictEqual(spans.length, 1);
362+
assertSpanSuccess(span);
363+
assert.strictEqual(span.parentSpanId, undefined);
364+
assert.strictEqual(span.attributes[SEMATTRS_FAAS_COLDSTART], false);
365+
});
366+
367+
it('should record coldstart with proactive initialization', async () => {
368+
initializeHandler('lambda-test/sync.handler', {
369+
lambdaStartTime: Date.now() - 2 * lambdaMaxInitInMilliseconds,
370+
});
371+
372+
const result = await new Promise((resolve, reject) => {
373+
lambdaRequire('lambda-test/sync').handler(
374+
'arg',
375+
ctx,
376+
(err: Error, res: any) => {
377+
if (err) {
378+
reject(err);
379+
} else {
380+
resolve(res);
381+
}
382+
}
383+
);
384+
});
385+
assert.strictEqual(result, 'ok');
386+
const spans = memoryExporter.getFinishedSpans();
387+
const [span] = spans;
388+
assert.strictEqual(spans.length, 1);
389+
assertSpanSuccess(span);
390+
assert.strictEqual(span.parentSpanId, undefined);
391+
assert.strictEqual(span.attributes[SEMATTRS_FAAS_COLDSTART], false);
392+
});
393+
298394
it('should record error', async () => {
299395
initializeHandler('lambda-test/sync.error');
300396

0 commit comments

Comments
 (0)