Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lambda): add isIterator prop #569

Merged
merged 1 commit into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 36 additions & 5 deletions lib/monitoring/aws-lambda/LambdaFunctionMonitoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ErrorCountThreshold,
ErrorRateThreshold,
ErrorType,
HalfWidth,
HighTpsThreshold,
LatencyAlarmFactory,
LatencyThreshold,
Expand Down Expand Up @@ -53,6 +54,14 @@ import {
} from "../../dashboard";

export interface LambdaFunctionMonitoringOptions extends BaseMonitoringProps {
/**
* Indicates that the Lambda function handles an event source (e.g. DynamoDB event stream).
* This impacts what widgets are shown, as well as validates the ability to use addMaxIteratorAgeAlarm.
*
* @default - true
*/
readonly isIterator?: boolean;

readonly addLatencyP50Alarm?: Record<string, LatencyThreshold>;
readonly addLatencyP90Alarm?: Record<string, LatencyThreshold>;
readonly addLatencyP99Alarm?: Record<string, LatencyThreshold>;
Expand Down Expand Up @@ -153,6 +162,8 @@ export class LambdaFunctionMonitoring extends Monitoring {
readonly concurrentExecutionsCountMetric: MetricWithAlarmSupport;
readonly provisionedConcurrencySpilloverInvocationsCountMetric: MetricWithAlarmSupport;
readonly provisionedConcurrencySpilloverInvocationsRateMetric: MetricWithAlarmSupport;

readonly isIterator: boolean;
readonly maxIteratorAgeMetric: MetricWithAlarmSupport;

readonly lambdaInsightsEnabled: boolean;
Expand Down Expand Up @@ -227,6 +238,8 @@ export class LambdaFunctionMonitoring extends Monitoring {
this.metricFactory.metricProvisionedConcurrencySpilloverInvocations();
this.provisionedConcurrencySpilloverInvocationsRateMetric =
this.metricFactory.metricProvisionedConcurrencySpilloverRate();

this.isIterator = props.isIterator ?? true;
this.maxIteratorAgeMetric =
this.metricFactory.metricMaxIteratorAgeInMillis();

Expand Down Expand Up @@ -493,6 +506,12 @@ export class LambdaFunctionMonitoring extends Monitoring {
this.addAlarm(createdAlarm);
}
for (const disambiguator in props.addMaxIteratorAgeAlarm) {
if (!this.isIterator) {
throw new Error(
"addMaxIteratorAgeAlarm is not applicable if isIterator is not true",
);
Comment on lines +510 to +512
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel I've asked this before, so sorry in advance if i have

Can we use Annotations here? Having the scope included (via Annotations.error(...)) really helps folks find where in their app this is being thrown

}

const alarmProps = props.addMaxIteratorAgeAlarm[disambiguator];
const createdAlarm = this.ageAlarmFactory.addIteratorMaxAgeAlarm(
this.maxIteratorAgeMetric,
Expand Down Expand Up @@ -524,13 +543,25 @@ export class LambdaFunctionMonitoring extends Monitoring {
this.createErrorRateWidget(QuarterWidth, DefaultGraphWidgetHeight),
this.createRateWidget(QuarterWidth, DefaultGraphWidgetHeight),
),
new Row(
this.createInvocationWidget(ThirdWidth, DefaultGraphWidgetHeight),
this.createIteratorAgeWidget(ThirdWidth, DefaultGraphWidgetHeight),
this.createErrorCountWidget(ThirdWidth, DefaultGraphWidgetHeight),
),
];

if (this.isIterator) {
widgets.push(
new Row(
this.createInvocationWidget(ThirdWidth, DefaultGraphWidgetHeight),
this.createIteratorAgeWidget(ThirdWidth, DefaultGraphWidgetHeight),
this.createErrorCountWidget(ThirdWidth, DefaultGraphWidgetHeight),
),
);
} else {
widgets.push(
new Row(
this.createInvocationWidget(HalfWidth, DefaultGraphWidgetHeight),
this.createErrorCountWidget(HalfWidth, DefaultGraphWidgetHeight),
),
);
}

if (this.lambdaInsightsEnabled) {
widgets.push(
new Row(
Expand Down
59 changes: 56 additions & 3 deletions test/monitoring/aws-lambda/LambdaFunctionMonitoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { AlarmWithAnnotation, LambdaFunctionMonitoring } from "../../../lib";
import { addMonitoringDashboardsToStack } from "../../utils/SnapshotUtil";
import { TestMonitoringScope } from "../TestMonitoringScope";

test("snapshot test: no alarms", () => {
test("snapshot test: default iterator and no alarms", () => {
const stack = new Stack();

const scope = new TestMonitoringScope(stack, "Scope");
Expand All @@ -24,14 +24,14 @@ test("snapshot test: no alarms", () => {
handler: "Dummy::handler",
});

new LambdaFunctionMonitoring(scope, {
const monitoring = new LambdaFunctionMonitoring(scope, {
lambdaFunction,
humanReadableName: "Dummy Lambda for testing",
alarmFriendlyName: "DummyLambda",
});
addMonitoringDashboardsToStack(stack, monitoring);

// alternative: use reference

new LambdaFunctionMonitoring(scope, {
lambdaFunction: Function.fromFunctionAttributes(stack, "DummyFunctionRef", {
functionArn:
Expand All @@ -42,6 +42,29 @@ test("snapshot test: no alarms", () => {
expect(Template.fromStack(stack)).toMatchSnapshot();
});

test("snapshot test: non-iterator and no alarms", () => {
const stack = new Stack();

const scope = new TestMonitoringScope(stack, "Scope");

const lambdaFunction = new Function(stack, "Function", {
functionName: "DummyLambda",
runtime: Runtime.NODEJS_18_X,
code: InlineCode.fromInline("{}"),
handler: "Dummy::handler",
});

const monitoring = new LambdaFunctionMonitoring(scope, {
lambdaFunction,
humanReadableName: "Dummy Lambda for testing",
alarmFriendlyName: "DummyLambda",
isIterator: false,
});

addMonitoringDashboardsToStack(stack, monitoring);
expect(Template.fromStack(stack)).toMatchSnapshot();
});

test("snapshot test: all alarms", () => {
const stack = new Stack();

Expand Down Expand Up @@ -483,6 +506,36 @@ test("snapshot test: all alarms, alarmPrefix on latency dedupeString", () => {
expect(Template.fromStack(stack)).toMatchSnapshot();
});

test("throws error if attempting to create iterator age alarm if not an iterator", () => {
const stack = new Stack();

const scope = new TestMonitoringScope(stack, "Scope");

const lambdaFunction = new Function(stack, "Function", {
functionName: "DummyLambda",
runtime: Runtime.NODEJS_18_X,
code: InlineCode.fromInline("{}"),
handler: "Dummy::handler",
});

expect(
() =>
new LambdaFunctionMonitoring(scope, {
lambdaFunction,
humanReadableName: "Dummy Lambda for testing",
alarmFriendlyName: "DummyLambda",
isIterator: false,
addMaxIteratorAgeAlarm: {
Warning: {
maxAgeInMillis: 1_000_000,
},
},
}),
).toThrow(
"addMaxIteratorAgeAlarm is not applicable if isIterator is not true",
);
});

test("doesn't create alarms for enhanced Lambda Insights metrics if not enabled", () => {
const stack = new Stack();

Expand Down
Loading