Skip to content

Commit 6c0447d

Browse files
authored
[SVLS-4699] Detect exceptions raised outside of the handler (#533)
1 parent 1621e81 commit 6c0447d

File tree

6 files changed

+140
-13
lines changed

6 files changed

+140
-13
lines changed

src/handler.cjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const {
33
datadogHandlerEnvVar,
44
lambdaTaskRootEnvVar,
55
traceExtractorEnvVar,
6+
emitTelemetryOnErrorOutsideHandler,
67
getEnvValue,
78
} = require("./index.js");
89
const { logDebug, logError } = require("./utils/index.js");
@@ -32,4 +33,11 @@ if (extractorEnv) {
3233
}
3334
}
3435

35-
exports.handler = datadog(loadSync(taskRootEnv, handlerEnv), { traceExtractor });
36+
try {
37+
exports.handler = datadog(loadSync(taskRootEnv, handlerEnv), { traceExtractor });
38+
} catch (error) {
39+
emitTelemetryOnErrorOutsideHandler(error, handlerEnv, Date.now()).catch(
40+
logDebug("failed to error telemetry on error outside handler"),
41+
);
42+
throw error;
43+
}

src/handler.mjs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { datadog, datadogHandlerEnvVar, lambdaTaskRootEnvVar, traceExtractorEnvVar, getEnvValue } from "./index.js";
1+
import {
2+
datadog,
3+
datadogHandlerEnvVar,
4+
lambdaTaskRootEnvVar,
5+
traceExtractorEnvVar,
6+
getEnvValue,
7+
emitTelemetryOnErrorOutsideHandler,
8+
} from "./index.js";
29
import { logDebug, logError } from "./utils/index.js";
310
import { load } from "./runtime/index.js";
411
import { initTracer } from "./runtime/module_importer.js";
@@ -26,4 +33,12 @@ if (extractorEnv) {
2633
}
2734
}
2835

29-
export const handler = datadog(await load(taskRootEnv, handlerEnv), { traceExtractor });
36+
let wrappedHandler;
37+
try {
38+
wrappedHandler = datadog(await load(taskRootEnv, handlerEnv), { traceExtractor });
39+
} catch (error) {
40+
await emitTelemetryOnErrorOutsideHandler(error, handlerEnv, Date.now());
41+
throw error;
42+
}
43+
44+
export const handler = wrappedHandler;

src/index.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
sendDistributionMetric,
99
sendDistributionMetricWithDate,
1010
_metricsQueue,
11+
emitTelemetryOnErrorOutsideHandler,
1112
} from "./index";
1213
import {
1314
incrementErrorsMetric,
@@ -21,6 +22,8 @@ import { DatadogTraceHeaders } from "./trace/context/extractor";
2122
import { SpanContextWrapper } from "./trace/span-context-wrapper";
2223
import { TraceSource } from "./trace/trace-context-service";
2324
import { inflateSync } from "zlib";
25+
import { MetricsListener } from "./metrics/listener";
26+
import { SpanOptions, TracerWrapper } from "./trace/tracer-wrapper";
2427

2528
jest.mock("./metrics/enhanced-metrics");
2629

@@ -536,3 +539,58 @@ describe("sendDistributionMetricWithDate", () => {
536539
expect(_metricsQueue.length).toBe(1);
537540
});
538541
});
542+
543+
describe("emitTelemetryOnErrorOutsideHandler", () => {
544+
let mockedStartSpan = jest.spyOn(TracerWrapper.prototype, "startSpan");
545+
beforeEach(() => {
546+
jest.spyOn(MetricsListener.prototype, "onStartInvocation").mockImplementation();
547+
jest.spyOn(TracerWrapper.prototype, "isTracerAvailable", "get").mockImplementation(() => true);
548+
});
549+
afterEach(() => {
550+
mockedIncrementErrors.mockClear();
551+
mockedStartSpan.mockClear();
552+
});
553+
it("emits a metric when enhanced metrics are enabled", async () => {
554+
process.env.DD_ENHANCED_METRICS = "true";
555+
await emitTelemetryOnErrorOutsideHandler(new ReferenceError("some error"), "myFunction", Date.now());
556+
expect(mockedIncrementErrors).toBeCalledTimes(1);
557+
});
558+
559+
it("does not emit a metric when enhanced metrics are disabled", async () => {
560+
process.env.DD_ENHANCED_METRICS = "false";
561+
await emitTelemetryOnErrorOutsideHandler(new ReferenceError("some error"), "myFunction", Date.now());
562+
expect(mockedIncrementErrors).toBeCalledTimes(0);
563+
});
564+
565+
it("creates a span when tracing is enabled", async () => {
566+
process.env.DD_TRACE_ENABLED = "true";
567+
const functionName = "myFunction";
568+
const startTime = Date.now();
569+
const fakeError = new ReferenceError("some error");
570+
const spanName = "aws.lambda";
571+
572+
await emitTelemetryOnErrorOutsideHandler(fakeError, functionName, startTime);
573+
574+
const options: SpanOptions = {
575+
tags: {
576+
service: spanName,
577+
operation_name: spanName,
578+
resource_names: functionName,
579+
"resource.name": functionName,
580+
"span.type": "serverless",
581+
"error.status": 500,
582+
"error.type": fakeError.name,
583+
"error.message": fakeError.message,
584+
"error.stack": fakeError.stack,
585+
},
586+
startTime,
587+
};
588+
expect(mockedStartSpan).toBeCalledWith(spanName, options);
589+
});
590+
591+
it("does not create a span when tracing is disabled", async () => {
592+
process.env.DD_TRACE_ENABLED = "false";
593+
await emitTelemetryOnErrorOutsideHandler(new ReferenceError("some error"), "myFunction", Date.now());
594+
expect(mockedStartSpan).toBeCalledTimes(0);
595+
});
596+
});

src/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
} from "./utils";
2525
import { getEnhancedMetricTags } from "./metrics/enhanced-metrics";
2626
import { DatadogTraceHeaders } from "./trace/context/extractor";
27+
import { SpanWrapper } from "./trace/span-wrapper";
28+
import { SpanOptions, TracerWrapper } from "./trace/tracer-wrapper";
2729

2830
// Backwards-compatible export, TODO deprecate in next major
2931
export { DatadogTraceHeaders as TraceHeaders } from "./trace/context/extractor";
@@ -416,3 +418,37 @@ function getRuntimeTag(): string {
416418
const version = process.version;
417419
return `dd_lambda_layer:datadog-node${version}`;
418420
}
421+
422+
export async function emitTelemetryOnErrorOutsideHandler(
423+
error: Error,
424+
functionName: string,
425+
startTime: number,
426+
): Promise<void> {
427+
if (getEnvValue("DD_TRACE_ENABLED", "true").toLowerCase() === "true") {
428+
const options: SpanOptions = {
429+
tags: {
430+
service: "aws.lambda",
431+
operation_name: "aws.lambda",
432+
resource_names: functionName,
433+
"resource.name": functionName,
434+
"span.type": "serverless",
435+
"error.status": 500,
436+
"error.type": error.name,
437+
"error.message": error.message,
438+
"error.stack": error.stack,
439+
},
440+
startTime,
441+
};
442+
const tracerWrapper = new TracerWrapper();
443+
const span = new SpanWrapper(tracerWrapper.startSpan("aws.lambda", options), {});
444+
span.finish();
445+
}
446+
447+
const config = getConfig();
448+
if (config.enhancedMetrics) {
449+
const metricsListener = new MetricsListener(new KMSService(), config);
450+
await metricsListener.onStartInvocation(undefined);
451+
incrementErrorsMetric(metricsListener);
452+
await metricsListener.onCompleteInvocation();
453+
}
454+
}

src/metrics/enhanced-metrics.spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ describe("getEnhancedMetricTags", () => {
5555
"account_id:123497598159",
5656
"functionname:my-test-lambda",
5757
"resource:my-test-lambda",
58-
"cold_start:true",
5958
"memorysize:128",
59+
"cold_start:true",
6060
"datadog_lambda:vX.X.X",
6161
"runtime:nodejs20.x",
6262
]);
@@ -66,8 +66,8 @@ describe("getEnhancedMetricTags", () => {
6666
mockedGetProcessVersion.mockReturnValue("v20.9.0");
6767
expect(getEnhancedMetricTags(mockContextLocal)).toStrictEqual([
6868
"functionname:my-test-lambda",
69-
"cold_start:true",
7069
"memorysize:128",
70+
"cold_start:true",
7171
"datadog_lambda:vX.X.X",
7272
"runtime:nodejs20.x",
7373
]);
@@ -80,9 +80,14 @@ describe("getEnhancedMetricTags", () => {
8080
"account_id:123497598159",
8181
"functionname:my-test-lambda",
8282
"resource:my-test-lambda",
83-
"cold_start:true",
8483
"memorysize:128",
84+
"cold_start:true",
8585
"datadog_lambda:vX.X.X",
8686
]);
8787
});
88+
89+
it("doesn't add context-based tags when context not provided", () => {
90+
mockedGetProcessVersion.mockReturnValue("v20.9.0");
91+
expect(getEnhancedMetricTags()).toStrictEqual(["cold_start:true", "datadog_lambda:vX.X.X", "runtime:nodejs20.x"]);
92+
});
8893
});

src/metrics/enhanced-metrics.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,17 @@ export function getRuntimeTag(): string | null {
4949
return `runtime:${processVersionTagString}`;
5050
}
5151

52-
export function getEnhancedMetricTags(context: Context): string[] {
53-
let arnTags = [`functionname:${context.functionName}`];
54-
if (context.invokedFunctionArn) {
55-
arnTags = parseTagsFromARN(context.invokedFunctionArn, context.functionVersion);
52+
export function getEnhancedMetricTags(context?: Context): string[] {
53+
const tags: string[] = [];
54+
if (context) {
55+
let arnTags = [`functionname:${context.functionName}`];
56+
if (context.invokedFunctionArn) {
57+
arnTags = parseTagsFromARN(context.invokedFunctionArn, context.functionVersion);
58+
}
59+
tags.push(...arnTags, `memorysize:${context.memoryLimitInMB}`);
5660
}
57-
const tags = [...arnTags, ...getSandboxInitTags(), `memorysize:${context.memoryLimitInMB}`, getVersionTag()];
61+
62+
tags.push(...getSandboxInitTags(), getVersionTag());
5863

5964
const runtimeTag = getRuntimeTag();
6065
if (runtimeTag) {
@@ -69,7 +74,7 @@ export function getEnhancedMetricTags(context: Context): string[] {
6974
* @param context object passed to invocation by AWS
7075
* @param metricName name of the enhanced metric without namespace prefix, i.e. "invocations" or "errors"
7176
*/
72-
function incrementEnhancedMetric(listener: MetricsListener, metricName: string, context: Context) {
77+
function incrementEnhancedMetric(listener: MetricsListener, metricName: string, context?: Context) {
7378
// Always write enhanced metrics to standard out
7479
listener.sendDistributionMetric(`aws.lambda.enhanced.${metricName}`, 1, true, ...getEnhancedMetricTags(context));
7580
}
@@ -78,7 +83,7 @@ export function incrementInvocationsMetric(listener: MetricsListener, context: C
7883
incrementEnhancedMetric(listener, "invocations", context);
7984
}
8085

81-
export function incrementErrorsMetric(listener: MetricsListener, context: Context): void {
86+
export function incrementErrorsMetric(listener: MetricsListener, context?: Context): void {
8287
incrementEnhancedMetric(listener, "errors", context);
8388
}
8489

0 commit comments

Comments
 (0)