Skip to content

Commit 9216125

Browse files
sthuber90rubenfonsecaleandrodamascena
committed
feat(event_sources): add CloudWatch dashboard custom widget event (#1474)
Co-authored-by: Ruben Fonseca <[email protected]> Co-authored-by: Leandro Damascena <[email protected]>
1 parent e7e91d4 commit 9216125

File tree

5 files changed

+272
-0
lines changed

5 files changed

+272
-0
lines changed

Diff for: aws_lambda_powertools/utilities/data_classes/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .alb_event import ALBEvent
66
from .api_gateway_proxy_event import APIGatewayProxyEvent, APIGatewayProxyEventV2
77
from .appsync_resolver_event import AppSyncResolverEvent
8+
from .cloud_watch_custom_widget_event import CloudWatchDashboardCustomWidgetEvent
89
from .cloud_watch_logs_event import CloudWatchLogsEvent
910
from .code_pipeline_job_event import CodePipelineJobEvent
1011
from .connect_contact_flow_event import ConnectContactFlowEvent
@@ -23,6 +24,7 @@
2324
"APIGatewayProxyEventV2",
2425
"AppSyncResolverEvent",
2526
"ALBEvent",
27+
"CloudWatchDashboardCustomWidgetEvent",
2628
"CloudWatchLogsEvent",
2729
"CodePipelineJobEvent",
2830
"ConnectContactFlowEvent",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from typing import Any, Dict, Optional
2+
3+
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
4+
5+
6+
class TimeZone(DictWrapper):
7+
@property
8+
def label(self) -> str:
9+
"""The time range label. Either 'UTC' or 'Local'"""
10+
return self["label"]
11+
12+
@property
13+
def offset_iso(self) -> str:
14+
"""The time range offset in the format +/-00:00"""
15+
return self["offsetISO"]
16+
17+
@property
18+
def offset_in_minutes(self) -> int:
19+
"""The time range offset in minutes"""
20+
return int(self["offsetInMinutes"])
21+
22+
23+
class TimeRange(DictWrapper):
24+
@property
25+
def mode(self) -> str:
26+
"""The time range mode, i.e. 'relative' or 'absolute'"""
27+
return self["mode"]
28+
29+
@property
30+
def start(self) -> int:
31+
"""The start time within the time range"""
32+
return self["start"]
33+
34+
@property
35+
def end(self) -> int:
36+
"""The end time within the time range"""
37+
return self["end"]
38+
39+
@property
40+
def relative_start(self) -> Optional[int]:
41+
"""The relative start time within the time range"""
42+
return self.get("relativeStart")
43+
44+
@property
45+
def zoom_start(self) -> Optional[int]:
46+
"""The start time within the zoomed time range"""
47+
return (self.get("zoom") or {}).get("start")
48+
49+
@property
50+
def zoom_end(self) -> Optional[int]:
51+
"""The end time within the zoomed time range"""
52+
return (self.get("zoom") or {}).get("end")
53+
54+
55+
class CloudWatchWidgetContext(DictWrapper):
56+
@property
57+
def dashboard_name(self) -> str:
58+
"""Get dashboard name, in which the widget is used"""
59+
return self["dashboardName"]
60+
61+
@property
62+
def widget_id(self) -> str:
63+
"""Get widget ID"""
64+
return self["widgetId"]
65+
66+
@property
67+
def domain(self) -> str:
68+
"""AWS domain name"""
69+
return self["domain"]
70+
71+
@property
72+
def account_id(self) -> str:
73+
"""Get AWS Account ID"""
74+
return self["accountId"]
75+
76+
@property
77+
def locale(self) -> str:
78+
"""Get locale language"""
79+
return self["locale"]
80+
81+
@property
82+
def timezone(self) -> TimeZone:
83+
"""Timezone information of the dashboard"""
84+
return TimeZone(self["timezone"])
85+
86+
@property
87+
def period(self) -> int:
88+
"""The period shown on the dashboard"""
89+
return int(self["period"])
90+
91+
@property
92+
def is_auto_period(self) -> bool:
93+
"""Whether auto period is enabled"""
94+
return bool(self["isAutoPeriod"])
95+
96+
@property
97+
def time_range(self) -> TimeRange:
98+
"""The widget time range"""
99+
return TimeRange(self["timeRange"])
100+
101+
@property
102+
def theme(self) -> str:
103+
"""The dashboard theme, i.e. 'light' or 'dark'"""
104+
return self["theme"]
105+
106+
@property
107+
def link_charts(self) -> bool:
108+
"""The widget is linked to other charts"""
109+
return bool(self["linkCharts"])
110+
111+
@property
112+
def title(self) -> str:
113+
"""Get widget title"""
114+
return self["title"]
115+
116+
@property
117+
def params(self) -> Dict[str, Any]:
118+
"""Get widget parameters"""
119+
return self["params"]
120+
121+
@property
122+
def forms(self) -> Dict[str, Any]:
123+
"""Get widget form data"""
124+
return self["forms"]["all"]
125+
126+
@property
127+
def height(self) -> int:
128+
"""Get widget height"""
129+
return int(self["height"])
130+
131+
@property
132+
def width(self) -> int:
133+
"""Get widget width"""
134+
return int(self["width"])
135+
136+
137+
class CloudWatchDashboardCustomWidgetEvent(DictWrapper):
138+
"""CloudWatch dashboard custom widget event
139+
140+
You can use a Lambda function to create a custom widget on a CloudWatch dashboard.
141+
142+
Documentation:
143+
-------------
144+
- https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/add_custom_widget_dashboard_about.html
145+
"""
146+
147+
@property
148+
def describe(self) -> bool:
149+
"""Display widget documentation"""
150+
return bool(self.get("describe", False))
151+
152+
@property
153+
def widget_context(self) -> Optional[CloudWatchWidgetContext]:
154+
"""The widget context"""
155+
if self.get("widgetContext"):
156+
return CloudWatchWidgetContext(self["widgetContext"])
157+
158+
return None

Diff for: docs/utilities/data_classes.md

+35
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Event Source | Data_class
6868
[Application Load Balancer](#application-load-balancer) | `ALBEvent`
6969
[AppSync Authorizer](#appsync-authorizer) | `AppSyncAuthorizerEvent`
7070
[AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent`
71+
[CloudWatch Dashboard Custom Widget](#cloudwatch-dashboard-custom-widget) | `CloudWatchDashboardCustomWidgetEvent`
7172
[CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent`
7273
[CodePipeline Job Event](#codepipeline-job) | `CodePipelineJobEvent`
7374
[Cognito User Pool](#cognito-user-pool) | Multiple available under `cognito_user_pool_event`
@@ -441,6 +442,40 @@ In this example, we also use the new Logger `correlation_id` and built-in `corre
441442
}
442443
```
443444

445+
### CloudWatch Dashboard Custom Widget
446+
447+
=== "app.py"
448+
449+
```python
450+
from aws_lambda_powertools.utilities.data_classes import event_source, CloudWatchDashboardCustomWidgetEvent
451+
452+
const DOCS = `
453+
## Echo
454+
A simple echo script. Anything passed in \`\`\`echo\`\`\` parameter is returned as the content of custom widget.
455+
456+
### Widget parameters
457+
Param | Description
458+
---|---
459+
**echo** | The content to echo back
460+
461+
### Example parameters
462+
\`\`\` yaml
463+
echo: <h1>Hello world</h1>
464+
\`\`\`
465+
`
466+
467+
@event_source(data_class=CloudWatchDashboardCustomWidgetEvent)
468+
def lambda_handler(event: CloudWatchDashboardCustomWidgetEvent, context):
469+
470+
if event.describe:
471+
return DOCS
472+
473+
# You can directly return HTML or JSON content
474+
# Alternatively, you can return markdown that will be rendered by CloudWatch
475+
echo = event.widget_context.params["echo"]
476+
return { "markdown": f"# {echo}" }
477+
```
478+
444479
### CloudWatch Logs
445480

446481
CloudWatch Logs events by default are compressed and base64 encoded. You can use the helper function provided to decode,

Diff for: tests/events/cloudWatchDashboardEvent.json

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"original": "param-to-widget",
3+
"widgetContext": {
4+
"dashboardName": "Name-of-current-dashboard",
5+
"widgetId": "widget-16",
6+
"domain": "https://us-east-1.console.aws.amazon.com",
7+
"accountId": "123456789123",
8+
"locale": "en",
9+
"timezone": {
10+
"label": "UTC",
11+
"offsetISO": "+00:00",
12+
"offsetInMinutes": 0
13+
},
14+
"period": 300,
15+
"isAutoPeriod": true,
16+
"timeRange": {
17+
"mode": "relative",
18+
"start": 1627236199729,
19+
"end": 1627322599729,
20+
"relativeStart": 86400012,
21+
"zoom": {
22+
"start": 1627276030434,
23+
"end": 1627282956521
24+
}
25+
},
26+
"theme": "light",
27+
"linkCharts": true,
28+
"title": "Tweets for Amazon website problem",
29+
"forms": {
30+
"all": {}
31+
},
32+
"params": {
33+
"original": "param-to-widget"
34+
},
35+
"width": 588,
36+
"height": 369
37+
}
38+
}

Diff for: tests/functional/test_data_classes.py

+39
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
APIGatewayProxyEvent,
1414
APIGatewayProxyEventV2,
1515
AppSyncResolverEvent,
16+
CloudWatchDashboardCustomWidgetEvent,
1617
CloudWatchLogsEvent,
1718
CodePipelineJobEvent,
1819
EventBridgeEvent,
@@ -99,6 +100,44 @@ def message(self) -> str:
99100
assert DataClassSample(data1).raw_event is data1
100101

101102

103+
def test_cloud_watch_dashboard_event():
104+
event = CloudWatchDashboardCustomWidgetEvent(load_event("cloudWatchDashboardEvent.json"))
105+
assert event.describe is False
106+
assert event.widget_context.account_id == "123456789123"
107+
assert event.widget_context.domain == "https://us-east-1.console.aws.amazon.com"
108+
assert event.widget_context.dashboard_name == "Name-of-current-dashboard"
109+
assert event.widget_context.widget_id == "widget-16"
110+
assert event.widget_context.locale == "en"
111+
assert event.widget_context.timezone.label == "UTC"
112+
assert event.widget_context.timezone.offset_iso == "+00:00"
113+
assert event.widget_context.timezone.offset_in_minutes == 0
114+
assert event.widget_context.period == 300
115+
assert event.widget_context.is_auto_period is True
116+
assert event.widget_context.time_range.mode == "relative"
117+
assert event.widget_context.time_range.start == 1627236199729
118+
assert event.widget_context.time_range.end == 1627322599729
119+
assert event.widget_context.time_range.relative_start == 86400012
120+
assert event.widget_context.time_range.zoom_start == 1627276030434
121+
assert event.widget_context.time_range.zoom_end == 1627282956521
122+
assert event.widget_context.theme == "light"
123+
assert event.widget_context.link_charts is True
124+
assert event.widget_context.title == "Tweets for Amazon website problem"
125+
assert event.widget_context.forms == {}
126+
assert event.widget_context.params == {"original": "param-to-widget"}
127+
assert event.widget_context.width == 588
128+
assert event.widget_context.height == 369
129+
assert event.widget_context.params["original"] == "param-to-widget"
130+
assert event["original"] == "param-to-widget"
131+
assert event.raw_event["original"] == "param-to-widget"
132+
133+
134+
def test_cloud_watch_dashboard_describe_event():
135+
event = CloudWatchDashboardCustomWidgetEvent({"describe": True})
136+
assert event.describe is True
137+
assert event.widget_context is None
138+
assert event.raw_event == {"describe": True}
139+
140+
102141
def test_cloud_watch_trigger_event():
103142
event = CloudWatchLogsEvent(load_event("cloudWatchLogEvent.json"))
104143

0 commit comments

Comments
 (0)