Skip to content

Commit dddbab0

Browse files
ran-isenbergRan Isenberg
andauthored
feature: add cloudwatch dashboards and alarms (ran-isenberg#747)
Co-authored-by: Ran Isenberg <ran.isenberg@ranthebuilder.cloud>
1 parent 6c7bfb5 commit dddbab0

File tree

9 files changed

+166
-41
lines changed

9 files changed

+166
-41
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ This project aims to reduce cognitive load and answer these questions for you by
8484
- AWS Lambda handler 3 layer architecture: handler layer, logic layer and data access layer
8585
- Features flags and configuration based on AWS AppConfig
8686
- Idempotent API
87+
- CloudWatch dashboards - High level and low level including CloudWatch alarms
8788
- Unit, infrastructure, security, integration and end to end tests.
8889

8990

cdk/service/api_construct.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import cdk.service.constants as constants
1010
from cdk.service.api_db_construct import ApiDbConstruct
11+
from cdk.service.monitoring import CrudMonitoring
1112

1213

1314
class ApiConstruct(Construct):
@@ -20,7 +21,9 @@ def __init__(self, scope: Construct, id_: str, appconfig_app_name: str) -> None:
2021
self.common_layer = self._build_common_layer()
2122
self.rest_api = self._build_api_gw()
2223
api_resource: aws_apigateway.Resource = self.rest_api.root.add_resource('api').add_resource(constants.GW_RESOURCE)
23-
self._add_post_lambda_integration(api_resource, self.lambda_role, self.api_db.db, appconfig_app_name, self.api_db.idempotency_db)
24+
self.create_order_func = self._add_post_lambda_integration(api_resource, self.lambda_role, self.api_db.db, appconfig_app_name,
25+
self.api_db.idempotency_db)
26+
self.monitoring = CrudMonitoring(self, id_, self.rest_api, self.api_db.db, self.api_db.idempotency_db, [self.create_order_func])
2427

2528
def _build_api_gw(self) -> aws_apigateway.RestApi:
2629
rest_api: aws_apigateway.RestApi = aws_apigateway.RestApi(
@@ -81,7 +84,7 @@ def _build_common_layer(self) -> PythonLayerVersion:
8184
)
8285

8386
def _add_post_lambda_integration(self, api_name: aws_apigateway.Resource, role: iam.Role, db: dynamodb.Table, appconfig_app_name: str,
84-
idempotency_table: dynamodb.Table):
87+
idempotency_table: dynamodb.Table) -> _lambda.Function:
8588
lambda_function = _lambda.Function(
8689
self,
8790
constants.CREATE_LAMBDA,
@@ -111,3 +114,4 @@ def _add_post_lambda_integration(self, api_name: aws_apigateway.Resource, role:
111114

112115
# POST /api/orders/
113116
api_name.add_method(http_method='POST', integration=aws_apigateway.LambdaIntegration(handler=lambda_function))
117+
return lambda_function

cdk/service/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
API_HANDLER_LAMBDA_TIMEOUT = 10 # seconds
1414
POWERTOOLS_SERVICE_NAME = 'POWERTOOLS_SERVICE_NAME'
1515
SERVICE_NAME = 'Orders'
16-
METRICS_NAMESPACE = 'my_product_kpi'
16+
METRICS_NAMESPACE = 'orders_kpi'
17+
METRICS_DIMENSION_KEY = 'service'
1718
POWERTOOLS_TRACE_DISABLED = 'POWERTOOLS_TRACE_DISABLED'
1819
POWER_TOOLS_LOG_LEVEL = 'LOG_LEVEL'
1920
BUILD_FOLDER = '.build/lambdas/'

cdk/service/monitoring.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from aws_cdk import Duration, aws_apigateway
2+
from aws_cdk import aws_dynamodb as dynamodb
3+
from aws_cdk import aws_lambda as _lambda
4+
from cdk_monitoring_constructs import CustomMetricGroup, ErrorRateThreshold, LatencyThreshold, MetricStatistic, MonitoringFacade
5+
from constructs import Construct
6+
7+
from cdk.service import constants
8+
9+
10+
class CrudMonitoring(Construct):
11+
12+
def __init__(
13+
self,
14+
scope: Construct,
15+
id_: str,
16+
crud_api: aws_apigateway.RestApi,
17+
db: dynamodb.Table,
18+
idempotency_table: dynamodb.Table,
19+
functions: list[_lambda.Function],
20+
) -> None:
21+
super().__init__(scope, id_)
22+
self.id_ = id_
23+
self._build_high_level_dashboard(crud_api)
24+
self._build_low_level_dashboard(db, idempotency_table, functions)
25+
26+
def _build_high_level_dashboard(self, crud_api: aws_apigateway.RestApi):
27+
high_level_facade = MonitoringFacade(self, f'{self.id_}HighFacade')
28+
high_level_facade.add_large_header('Order REST API High Level Dashboard')
29+
high_level_facade.monitor_api_gateway(
30+
api=crud_api,
31+
add5_xx_fault_rate_alarm={'internal_error': ErrorRateThreshold(max_error_rate=1)},
32+
)
33+
metric_factory = high_level_facade.create_metric_factory()
34+
create_metric = metric_factory.create_metric(
35+
metric_name='ValidCreateOrderEvents',
36+
namespace=constants.METRICS_NAMESPACE,
37+
statistic=MetricStatistic.N,
38+
dimensions_map={constants.METRICS_DIMENSION_KEY: constants.SERVICE_NAME},
39+
label='create order events',
40+
period=Duration.days(1),
41+
)
42+
43+
group = CustomMetricGroup(metrics=[create_metric], title='Daily Order Requests')
44+
high_level_facade.monitor_custom(metric_groups=[group], human_readable_name='Daily KPIs', alarm_friendly_name='KPIs')
45+
46+
def _build_low_level_dashboard(self, db: dynamodb.Table, idempotency_table: dynamodb.Table, functions: list[_lambda.Function]):
47+
low_level_facade = MonitoringFacade(self, f'{self.id_}LowFacade')
48+
low_level_facade.add_large_header('Orders REST API Low Level Dashboard')
49+
for func in functions:
50+
low_level_facade.monitor_lambda_function(
51+
lambda_function=func,
52+
add_latency_p90_alarm={'p90': LatencyThreshold(max_latency=Duration.seconds(3))},
53+
)
54+
low_level_facade.monitor_log(
55+
log_group_name=func.log_group.log_group_name,
56+
human_readable_name='Error logs',
57+
pattern='ERROR',
58+
alarm_friendly_name='error logs',
59+
)
60+
61+
low_level_facade.monitor_dynamo_table(table=db, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST)
62+
low_level_facade.monitor_dynamo_table(table=idempotency_table, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST)

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This project aims to reduce cognitive load and answer these questions for you by
3737
- AWS Lambda handler uses [AWS Lambda Powertools](https://docs.powertools.aws.dev/lambda-python/){:target="_blank" rel="noopener"}.
3838
- AWS Lambda handler 3 layer architecture: handler layer, logic layer and data access layer
3939
- Features flags and configuration based on AWS AppConfig
40+
- CloudWatch dashboards - High level and low level including CloudWatch alarms
4041
- Idempotent API
4142
- Unit, infrastructure, security, integration and E2E tests.
4243

poetry.lock

Lines changed: 91 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ constructs = ">=10.0.0"
3333
cdk-nag = ">2.0.0"
3434
"aws-cdk.aws-lambda-python-alpha" = "^2.99.0-alpha.0"
3535
"aws-cdk.aws-appconfig-alpha" = "^2.99.0-alpha.0"
36+
cdk-monitoring-constructs = "*"
3637
# DEV
3738
pytest = "*"
3839
pytest-mock = "*"

service/handlers/handle_create_order.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from http import HTTPMethod
21
from typing import Any
32

43
from aws_lambda_env_modeler import get_environment_variables, init_environment_variables
@@ -18,7 +17,7 @@
1817
from service.schemas.output import CreateOrderOutput
1918

2019

21-
@app.route(ORDERS_PATH, method=HTTPMethod.POST)
20+
@app.post(ORDERS_PATH)
2221
def handle_create_order() -> dict[str, Any]:
2322
env_vars: MyHandlerEnvVars = get_environment_variables(model=MyHandlerEnvVars)
2423
logger.debug('environment variables', env_vars=env_vars.model_dump())

service/handlers/utils/observability.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from aws_lambda_powertools.metrics.metrics import Metrics
33
from aws_lambda_powertools.tracing.tracer import Tracer
44

5-
METRICS_NAMESPACE = 'my_product_kpi'
5+
METRICS_NAMESPACE = 'orders_kpi'
66

77
# JSON output format, service name can be set by environment variable "POWERTOOLS_SERVICE_NAME"
88
logger: Logger = Logger()

0 commit comments

Comments
 (0)