Setting the file. One moment. Generate Dashboards · Amazon Elasticache · aws/agent-toolkit-for-aws · Skills Docs10
Setup DevOps Agent
33
AWS Deployment
61.5
Recipe Gallery
Script Find Tunnel Host
scripts/generate_dashboards.py
Python·526 lines·27 KB
17
18 # Alarms only (no dashboard)
19 python generate_dashboards.py --serverless my-cache --no-dashboard
20
21 # Write to file
22 python generate_dashboards.py --serverless my-cache --output elasticache-observability.json
23
24 # Set SNS topic for alarm notifications
25 python generate_dashboards.py --serverless my-cache --sns-topic arn:aws:sns:us-east-1:123456789012:alerts
26"""
27
28import argparse
29import json
30import re
31import sys
32
33# ---------------------------------------------------------------------------
34# Dependency check
35# This script uses only the Python standard library (no pip install needed).
36# It generates CloudFormation JSON locally and does not call AWS APIs.
37# ---------------------------------------------------------------------------
38
39
40def serverless_dashboard_body(cache_name, region):
41 """CloudWatch dashboard body for a serverless cache.
42
43 Note: CacheHitRate and some command-family metrics are only available for
44 Valkey/Redis OSS serverless caches, not Memcached serverless caches.
45 """
46 return {
47 "widgets": [
48 _metric_widget("Cache Hit Rate", region, "ServerlessCacheName", cache_name,
49 [["AWS/ElastiCache", "CacheHitRate", "ServerlessCacheName", cache_name]],
50 period=300),
51 _metric_widget("ECPU Consumption", region, "ServerlessCacheName", cache_name,
52 [["AWS/ElastiCache", "ElastiCacheProcessingUnits", "ServerlessCacheName", cache_name]],
53 period=60),
54 _metric_widget("Throttled Commands", region, "ServerlessCacheName", cache_name,
55 [["AWS/ElastiCache", "ThrottledCmds", "ServerlessCacheName", cache_name]],
56 period=60, stat="Sum"),
57 _metric_widget("Read / Write Latency", region, "ServerlessCacheName", cache_name,
58 [["AWS/ElastiCache", "SuccessfulReadRequestLatency", "ServerlessCacheName", cache_name],
59 ["AWS/ElastiCache", "SuccessfulWriteRequestLatency", "ServerlessCacheName", cache_name]],
60 period=60),
61 _metric_widget("Current Connections", region, "ServerlessCacheName", cache_name,
62 [["AWS/ElastiCache", "CurrConnections", "ServerlessCacheName", cache_name]],
63 period=300),
64 _metric_widget("New Connections", region, "ServerlessCacheName", cache_name,
65 [["AWS/ElastiCache", "NewConnections", "ServerlessCacheName", cache_name]],
66 period=300, stat="Sum"),
67 _metric_widget("Bytes Used For Cache", region, "ServerlessCacheName", cache_name,
68 [["AWS/ElastiCache", "BytesUsedForCache", "ServerlessCacheName", cache_name]],
69 period=300),
70 _metric_widget("Command-Family Breakdown", region, "ServerlessCacheName", cache_name,
71 [["AWS/ElastiCache", "StringBasedCmds", "ServerlessCacheName", cache_name],
72 ["AWS/ElastiCache", "HashBasedCmds", "ServerlessCacheName", cache_name],
73 ["AWS/ElastiCache", "SortedSetBasedCmds", "ServerlessCacheName", cache_name],
74 ["AWS/ElastiCache", "StreamBasedCmds", "ServerlessCacheName", cache_name],
75 ["AWS/ElastiCache", "PubSubBasedCmds", "ServerlessCacheName", cache_name]],
76 period=300, stat="Sum"),
77 _metric_widget("Total Commands Count", region, "ServerlessCacheName", cache_name,
78 [["AWS/ElastiCache", "TotalCmdsCount", "ServerlessCacheName", cache_name]],
79 period=300, stat="Sum"),
80 _metric_widget("Item Counts", region, "ServerlessCacheName", cache_name,
81 [["AWS/ElastiCache", "CurrVolatileItems", "ServerlessCacheName", cache_name],
82 ["AWS/ElastiCache", "CurrItems", "ServerlessCacheName", cache_name]],
83 period=300),
84 ]
85 }
86
87
88def node_based_dashboard_body(rg_id, region, node_ids=None):
89 """CloudWatch dashboard body for a node-based replication group.
90
91 Args:
92 node_ids: List of CacheClusterId values (e.g., ["my-cluster-001", "my-cluster-002"]).
93 If provided, per-node metrics are shown. If None, falls back to ReplicationGroupId
94 which only works for a subset of metrics.
95 """
96 if node_ids:
97 # Per-node metrics using CacheClusterId dimension
98 def _node_metrics(metric_name):
99 return [["AWS/ElastiCache", metric_name, "CacheClusterId", nid] for nid in node_ids]
100
101 return {
102 "widgets": [
103 _metric_widget("Cache Hit Rate", region, "CacheClusterId", node_ids[0],
104 _node_metrics("CacheHitRate"), period=300),
105 _metric_widget("Engine CPU Utilization", region, "CacheClusterId", node_ids[0],
106 _node_metrics("EngineCPUUtilization") + _node_metrics("CPUUtilization"),
107 period=60),
108 _metric_widget("Database Memory Usage", region, "CacheClusterId", node_ids[0],
109 _node_metrics("DatabaseMemoryUsagePercentage"), period=300),
110 _metric_widget("Read / Write Latency", region, "CacheClusterId", node_ids[0],
111 _node_metrics("SuccessfulReadRequestLatency") + _node_metrics("SuccessfulWriteRequestLatency"),
112 period=60),
113 _metric_widget("Current Connections", region, "CacheClusterId", node_ids[0],
114 _node_metrics("CurrConnections"), period=300),
115 _metric_widget("Replication Lag", region, "CacheClusterId", node_ids[0],
116 _node_metrics("ReplicationLag"), period=60),
117 _metric_widget("Network Bytes In/Out", region, "CacheClusterId", node_ids[0],
118 _node_metrics("NetworkBytesIn") + _node_metrics("NetworkBytesOut"),
119 period=300, stat="Sum"),
120 _metric_widget("Evictions", region, "CacheClusterId", node_ids[0],
121 _node_metrics("Evictions"), period=300, stat="Sum"),
122 _metric_widget("Command-Family Breakdown", region, "CacheClusterId", node_ids[0],
123 _node_metrics("StringBasedCmds") + _node_metrics("HashBasedCmds") +
124 _node_metrics("SortedSetBasedCmds") + _node_metrics("StreamBasedCmds") +
125 _node_metrics("SearchBasedCmds"),
126 period=300, stat="Sum"),
127 ]
128 }
129 else:
130 # Fallback: ReplicationGroupId (limited metrics available)
131 dim_name = "ReplicationGroupId"
132 return {
133 "widgets": [
134 _metric_widget("Cache Hit Rate", region, dim_name, rg_id,
135 [["AWS/ElastiCache", "CacheHitRate", dim_name, rg_id]], period=300),
136 _metric_widget("Engine CPU Utilization", region, dim_name, rg_id,
137 [["AWS/ElastiCache", "EngineCPUUtilization", dim_name, rg_id],
138 ["AWS/ElastiCache", "CPUUtilization", dim_name, rg_id]], period=60),
139 _metric_widget("Database Memory Usage", region, dim_name, rg_id,
140 [["AWS/ElastiCache", "DatabaseMemoryUsagePercentage", dim_name, rg_id]], period=300),
141 _metric_widget("Read / Write Latency", region, dim_name, rg_id,
142 [["AWS/ElastiCache", "SuccessfulReadRequestLatency", dim_name, rg_id],
143 ["AWS/ElastiCache", "SuccessfulWriteRequestLatency", dim_name, rg_id]], period=60),
144 _metric_widget("Current Connections", region, dim_name, rg_id,
145 [["AWS/ElastiCache", "CurrConnections", dim_name, rg_id]], period=300),
146 _metric_widget("Replication Lag", region, dim_name, rg_id,
147 [["AWS/ElastiCache", "ReplicationLag", dim_name, rg_id]], period=60),
148 _metric_widget("Evictions", region, dim_name, rg_id,
149 [["AWS/ElastiCache", "Evictions", dim_name, rg_id]], period=300, stat="Sum"),
150 ]
151 }
152
153
154def serverless_alarms(cache_name, sns_topic=None, max_storage_gb=None,
155 storage_alarm_pct=80,
156 hit_rate_threshold=80, throttle_threshold=None,
157 read_latency_threshold=None, write_latency_threshold=None,
158 ecpu_threshold=None, evictions_threshold=None):
159 """CloudFormation alarm resources for a serverless cache.
160
161 Alarms are controlled via threshold parameters. Pass None to disable
162 an alarm. CacheHitRate is enabled by default at 80%.
163 """
164 alarms = {}
165 dim = [{"Name": "ServerlessCacheName", "Value": cache_name}]
166
167 if hit_rate_threshold is not None:
168 alarms["CacheHitRateAlarm"] = _alarm(
169 f"{cache_name}-low-hit-rate",
170 f"Cache hit rate below {hit_rate_threshold}% for {cache_name}",
171 "AWS/ElastiCache", "CacheHitRate", dim,
172 threshold=hit_rate_threshold, comparison="LessThanThreshold",
173 period=300, eval_periods=6, datapoints_to_alarm=4, stat="Average",
174 sns_topic=sns_topic
175 )
176 if throttle_threshold is not None:
177 alarms["ThrottledCmdsAlarm"] = _alarm(
178 f"{cache_name}-throttled-cmds",
179 f"Throttled commands above {throttle_threshold} on {cache_name}",
180 "AWS/ElastiCache", "ThrottledCmds", dim,
181 threshold=throttle_threshold, comparison="GreaterThanThreshold",
182 period=60, eval_periods=3, datapoints_to_alarm=2, stat="Sum",
183 sns_topic=sns_topic
184 )
185 if max_storage_gb is not None:
186 storage_threshold_bytes = int(max_storage_gb * (storage_alarm_pct / 100) * 1073741824)
187 alarms["StorageLimitAlarm"] = _alarm(
188 f"{cache_name}-storage-approaching-limit",
189 f"Storage approaching {storage_alarm_pct}% of configured limit ({max_storage_gb} GB) on {cache_name}",
190 "AWS/ElastiCache", "BytesUsedForCache", dim,
191 threshold=storage_threshold_bytes, comparison="GreaterThanThreshold",
192 period=300, eval_periods=3, datapoints_to_alarm=2, stat="Maximum",
193 sns_topic=sns_topic
194 )
195 if read_latency_threshold is not None:
196 alarms["ReadLatencyAlarm"] = _alarm(
197 f"{cache_name}-high-read-latency",
198 f"Read latency above {read_latency_threshold}us on {cache_name}",
199 "AWS/ElastiCache", "SuccessfulReadRequestLatency", dim,
200 threshold=read_latency_threshold, comparison="GreaterThanThreshold",
201 period=60, eval_periods=5, datapoints_to_alarm=3, stat="Average",
202 sns_topic=sns_topic
203 )
204 if write_latency_threshold is not None:
205 alarms["WriteLatencyAlarm"] = _alarm(
206 f"{cache_name}-high-write-latency",
207 f"Write latency above {write_latency_threshold}us on {cache_name}",
208 "AWS/ElastiCache", "SuccessfulWriteRequestLatency", dim,
209 threshold=write_latency_threshold, comparison="GreaterThanThreshold",
210 period=60, eval_periods=5, datapoints_to_alarm=3, stat="Average",
211 sns_topic=sns_topic
212 )
213 if ecpu_threshold is not None:
214 alarms["ECPUSpikeAlarm"] = _alarm(
215 f"{cache_name}-ecpu-spike",
216 f"ECPU consumption above {ecpu_threshold} on {cache_name}",
217 "AWS/ElastiCache", "ElastiCacheProcessingUnits", dim,
218 threshold=ecpu_threshold, comparison="GreaterThanThreshold",
219 period=300, eval_periods=3, datapoints_to_alarm=2, stat="Sum",
220 sns_topic=sns_topic
221 )
222 if evictions_threshold is not None:
223 alarms["EvictionsAlarm"] = _alarm(
224 f"{cache_name}-evictions",
225 f"Evictions above {evictions_threshold} on {cache_name}",
226 "AWS/ElastiCache", "Evictions", dim,
227 threshold=evictions_threshold, comparison="GreaterThanThreshold",
228 period=300, eval_periods=3, datapoints_to_alarm=2, stat="Sum",
229 sns_topic=sns_topic
230 )
231 return alarms
232
233
234def node_based_alarms(rg_id, sns_topic=None, engine_cpu_threshold=90,
235 memory_threshold=80, hit_rate_threshold=80,
236 replication_lag_threshold=None,
237 read_latency_threshold=None, write_latency_threshold=None,
238 evictions_threshold=None, new_connections_threshold=None,
239 node_ids=None):
240 """CloudFormation alarm resources for a node-based replication group.
241
242 Alarms are controlled via threshold parameters. Pass None to disable
243 an alarm. EngineCPU (90%), memory (80%), and hit rate (80%) are enabled
244 by default. If node_ids are provided, alarms are created for each node
245 using CacheClusterId. Otherwise falls back to ReplicationGroupId
246 (limited metric availability).
247 """
248 alarms = {}
249 if node_ids:
250 targets = [("CacheClusterId", nid) for nid in node_ids]
251 else:
252 targets = [("ReplicationGroupId", rg_id)]
253
254 for dim_name, dim_value in targets:
255 dim = [{"Name": dim_name, "Value": dim_value}]
256 prefix = dim_value
257 safe = re.sub(r'[^a-zA-Z0-9]', '', dim_value)
258
259 if hit_rate_threshold is not None:
260 alarms[f"CacheHitRateAlarm{safe}"] = _alarm(
261 f"{prefix}-low-hit-rate",
262 f"Cache hit rate below {hit_rate_threshold}% on {dim_value}",
263 "AWS/ElastiCache", "CacheHitRate", dim,
264 threshold=hit_rate_threshold, comparison="LessThanThreshold",
265 period=300, eval_periods=6, datapoints_to_alarm=4, stat="Average",
266 sns_topic=sns_topic
267 )
268 if engine_cpu_threshold is not None:
269 alarms[f"EngineCPUAlarm{safe}"] = _alarm(
270 f"{prefix}-high-engine-cpu",
271 f"Engine CPU above {engine_cpu_threshold}% on {dim_value}",
272 "AWS/ElastiCache", "EngineCPUUtilization", dim,
273 threshold=engine_cpu_threshold, comparison="GreaterThanThreshold",
274 period=60, eval_periods=5, datapoints_to_alarm=3, stat="Maximum",
275 sns_topic=sns_topic
276 )
277 if memory_threshold is not None:
278 alarms[f"MemoryAlarm{safe}"] = _alarm(
279 f"{prefix}-high-memory",
280 f"Memory usage above {memory_threshold}% on {dim_value}",
281 "AWS/ElastiCache", "DatabaseMemoryUsagePercentage", dim,
282 threshold=memory_threshold, comparison="GreaterThanThreshold",
283 period=60, eval_periods=5, datapoints_to_alarm=3, stat="Maximum",
284 sns_topic=sns_topic
285 )
286 if replication_lag_threshold is not None:
287 alarms[f"ReplicationLagAlarm{safe}"] = _alarm(
288 f"{prefix}-high-replication-lag",
289 f"Replication lag above {replication_lag_threshold}s on {dim_value}",
290 "AWS/ElastiCache", "ReplicationLag", dim,
291 threshold=replication_lag_threshold, comparison="GreaterThanThreshold",
292 period=60, eval_periods=5, datapoints_to_alarm=3, stat="Maximum",
293 sns_topic=sns_topic
294 )
295 if read_latency_threshold is not None:
296 alarms[f"ReadLatencyAlarm{safe}"] = _alarm(
297 f"{prefix}-high-read-latency",
298 f"Read latency above {read_latency_threshold}us on {dim_value}",
299 "AWS/ElastiCache", "SuccessfulReadRequestLatency", dim,
300 threshold=read_latency_threshold, comparison="GreaterThanThreshold",
301 period=60, eval_periods=5, datapoints_to_alarm=3, stat="Average",
302 sns_topic=sns_topic
303 )
304 if write_latency_threshold is not None:
305 alarms[f"WriteLatencyAlarm{safe}"] = _alarm(
306 f"{prefix}-high-write-latency",
307 f"Write latency above {write_latency_threshold}us on {dim_value}",
308 "AWS/ElastiCache", "SuccessfulWriteRequestLatency", dim,
309 threshold=write_latency_threshold, comparison="GreaterThanThreshold",
310 period=60, eval_periods=5, datapoints_to_alarm=3, stat="Average",
311 sns_topic=sns_topic
312 )
313 if evictions_threshold is not None:
314 alarms[f"EvictionsAlarm{safe}"] = _alarm(
315 f"{prefix}-evictions",
316 f"Evictions above {evictions_threshold} on {dim_value}",
317 "AWS/ElastiCache", "Evictions", dim,
318 threshold=evictions_threshold, comparison="GreaterThanThreshold",
319 period=300, eval_periods=3, datapoints_to_alarm=2, stat="Sum",
320 sns_topic=sns_topic
321 )
322 if new_connections_threshold is not None:
323 alarms[f"NewConnectionsAlarm{safe}"] = _alarm(
324 f"{prefix}-new-connections",
325 f"New connections above {new_connections_threshold}/min on {dim_value}",
326 "AWS/ElastiCache", "NewConnections", dim,
327 threshold=new_connections_threshold, comparison="GreaterThanThreshold",
328 period=60, eval_periods=3, datapoints_to_alarm=2, stat="Sum",
329 sns_topic=sns_topic
330 )
331 return alarms
332
333
334def _metric_widget(title, region, dim_name, dim_value, metrics, period=300, stat="Average"):
335 """Build a CloudWatch dashboard metric widget."""
336 return {
337 "type": "metric",
338 "properties": {
339 "title": title,
340 "region": region,
341 "metrics": metrics,
342 "period": period,
343 "stat": stat,
344 "view": "timeSeries",
345 },
346 "width": 12,
347 "height": 6,
348 }
349
350
351def _alarm(name, description, namespace, metric, dimensions, threshold,
352 comparison, period, eval_periods, stat, sns_topic=None,
353 datapoints_to_alarm=None):
354 """Build a CloudFormation alarm resource."""
355 alarm = {
356 "Type": "AWS::CloudWatch::Alarm",
357 "Properties": {
358 "AlarmName": name,
359 "AlarmDescription": description,
360 "Namespace": namespace,
361 "MetricName": metric,
362 "Dimensions": dimensions,
363 "Threshold": threshold,
364 "ComparisonOperator": comparison,
365 "Period": period,
366 "EvaluationPeriods": eval_periods,
367 "DatapointsToAlarm": datapoints_to_alarm if datapoints_to_alarm else eval_periods,
368 "Statistic": stat,
369 "TreatMissingData": "notBreaching",
370 }
371 }
372 if sns_topic:
373 props = alarm["Properties"]
374 assert isinstance(props, dict)
375 props["AlarmActions"] = [{"Ref": "SNSTopicARN"}]
376 props["OKActions"] = [{"Ref": "SNSTopicARN"}]
377 return alarm
378
379
380def generate_template(cache_type, identifier, region, sns_topic=None,
381 include_dashboard=True, include_alarms=True,
382 max_storage_gb=None, storage_alarm_pct=80, memory_threshold=80,
383 engine_cpu_threshold=90,
384 replication_lag_threshold=None, hit_rate_threshold=80,
385 throttle_threshold=None,
386 read_latency_threshold=None, write_latency_threshold=None,
387 ecpu_threshold=None, evictions_threshold=None,
388 new_connections_threshold=None, node_ids=None):
389 """Generate a complete CloudFormation template."""
390 resources = {}
391
392 if include_dashboard:
393 if cache_type == "serverless":
394 body = serverless_dashboard_body(identifier, region)
395 else:
396 body = node_based_dashboard_body(identifier, region, node_ids=node_ids)
397
398 safe_name = re.sub(r'[^a-zA-Z0-9]', '', identifier)
399 resources[f"{safe_name}Dashboard"] = {
400 "Type": "AWS::CloudWatch::Dashboard",
401 "Properties": {
402 "DashboardName": f"ElastiCache-{identifier}",
403 "DashboardBody": json.dumps(body),
404 }
405 }
406
407 if include_alarms:
408 if cache_type == "serverless":
409 alarms = serverless_alarms(identifier, sns_topic,
410 max_storage_gb=max_storage_gb,
411 storage_alarm_pct=storage_alarm_pct,
412 hit_rate_threshold=hit_rate_threshold,
413 throttle_threshold=throttle_threshold,
414 read_latency_threshold=read_latency_threshold,
415 write_latency_threshold=write_latency_threshold,
416 ecpu_threshold=ecpu_threshold,
417 evictions_threshold=evictions_threshold)
418 else:
419 alarms = node_based_alarms(identifier, sns_topic,
420 memory_threshold=memory_threshold,
421 engine_cpu_threshold=engine_cpu_threshold,
422 replication_lag_threshold=replication_lag_threshold,
423 hit_rate_threshold=hit_rate_threshold,
424 read_latency_threshold=read_latency_threshold,
425 write_latency_threshold=write_latency_threshold,
426 evictions_threshold=evictions_threshold,
427 new_connections_threshold=new_connections_threshold,
428 node_ids=node_ids)
429 resources.update(alarms)
430
431 template = {
432 "AWSTemplateFormatVersion": "2010-09-09",
433 "Description": f"ElastiCache observability stack for {identifier}",
434 "Resources": resources,
435 }
436
437 if sns_topic:
438 template["Parameters"] = {
439 "SNSTopicARN": {
440 "Type": "String",
441 "Default": sns_topic,
442 "Description": "SNS topic ARN for alarm notifications"
443 }
444 }
445
446 return template
447
448
449if __name__ == "__main__":
450 parser = argparse.ArgumentParser(description="ElastiCache Dashboard & Alarm Generator")
451 group = parser.add_mutually_exclusive_group(required=True)
452 group.add_argument("--serverless", metavar="CACHE_NAME", help="Generate for a serverless cache")
453 group.add_argument("--replication-group", metavar="RG_ID", help="Generate for a node-based replication group")
454 parser.add_argument("--node-ids", default=None,
455 help="Comma-separated CacheClusterIds for per-node metrics (e.g., my-cluster-001,my-cluster-002). "
456 "Required for node-based dashboards to show data.")
457 parser.add_argument("--region", default="us-east-1", help="AWS region for dashboard widgets")
458 parser.add_argument("--sns-topic", default=None, help="SNS topic ARN for alarm notifications")
459 parser.add_argument("--no-dashboard", action="store_true", help="Skip dashboard generation")
460 parser.add_argument("--no-alarms", action="store_true", help="Skip alarm generation")
461 parser.add_argument("--max-storage-gb", type=float, default=None,
462 help="Serverless MaxDataStorageGB for storage alarm. If omitted, alarm is skipped.")
463 parser.add_argument("--storage-alarm-pct", type=float, default=80,
464 help="Percentage of max-storage-gb to alarm at (default: 80)")
465 parser.add_argument("--memory-threshold", type=float, default=80,
466 help="Node-based memory usage alarm threshold in percent (default: 80)")
467 parser.add_argument("--engine-cpu-threshold", type=float, default=90,
468 help="Node-based engine CPU alarm threshold in percent (default: 90)")
469 parser.add_argument("--replication-lag-threshold", type=float, default=None,
470 help="Node-based replication lag alarm threshold in seconds. If omitted, alarm is skipped.")
471 parser.add_argument("--hit-rate-threshold", type=float, default=80,
472 help="Cache hit rate alarm threshold in percent (default: 80)")
473 parser.add_argument("--throttle-threshold", type=float, default=None,
474 help="Serverless throttled commands alarm threshold (off by default)")
475 parser.add_argument("--read-latency-threshold", type=float, default=None,
476 help="Read latency alarm threshold in microseconds (off by default)")
477 parser.add_argument("--write-latency-threshold", type=float, default=None,
478 help="Write latency alarm threshold in microseconds (off by default)")
479 parser.add_argument("--ecpu-threshold", type=float, default=None,
480 help="Serverless ECPU consumption alarm threshold (off by default)")
481 parser.add_argument("--evictions-threshold", type=float, default=None,
482 help="Evictions alarm threshold per 5-min period (off by default)")
483 parser.add_argument("--new-connections-threshold", type=float, default=None,
484 help="Node-based new connections alarm threshold per minute (off by default)")
485 parser.add_argument("--output", "-o", default=None, help="Write to file instead of stdout")
486 args = parser.parse_args()
487
488 if args.serverless:
489 cache_type = "serverless"
490 identifier = args.serverless
491 else:
492 cache_type = "node-based"
493 identifier = args.replication_group
494
495 node_ids = [nid.strip() for nid in args.node_ids.split(",") if nid.strip()] if args.node_ids else None
496
497 template = generate_template(
498 cache_type, identifier, args.region,
499 sns_topic=args.sns_topic,
500 include_dashboard=not args.no_dashboard,
501 include_alarms=not args.no_alarms,
502 max_storage_gb=args.max_storage_gb,
503 storage_alarm_pct=args.storage_alarm_pct,
504 memory_threshold=args.memory_threshold,
505 engine_cpu_threshold=args.engine_cpu_threshold,
506 replication_lag_threshold=args.replication_lag_threshold,
507 hit_rate_threshold=args.hit_rate_threshold,
508 throttle_threshold=args.throttle_threshold,
509 read_latency_threshold=args.read_latency_threshold,
510 write_latency_threshold=args.write_latency_threshold,
511 ecpu_threshold=args.ecpu_threshold,
512 evictions_threshold=args.evictions_threshold,
513 new_connections_threshold=args.new_connections_threshold,
514 node_ids=node_ids,
515 )
516
517 output = json.dumps(template, indent=2)
518
519 if args.output:
520 with open(args.output, "w") as f:
521 f.write(output)
522 print(f"Written to {args.output}")
523 print(f"\nDeploy with:")
524 print(f" aws cloudformation deploy --template-file {args.output} --stack-name {identifier}-observability")
525 else:
526 print(output)