Skip to content

Commit f6f39bc

Browse files
author
Andrew Barkley
committed
feat: add comprehensive CloudWatch metric analysis with anomaly detection alarm recommendations
1 parent ea2ca6a commit f6f39bc

28 files changed

+2223
-1770
lines changed

src/cloudwatch-mcp-server/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@ target/
5656
.env.*.local
5757

5858
# PyPI
59-
.pypirc
59+
.pypirc

src/cloudwatch-mcp-server/=0.14.0

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/cloudwatch-mcp-server/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
9+
10+
## [0.0.5] - 2025-10-06
11+
12+
### Added
13+
- Added tool to analyze CloudWatch Metric data
14+
15+
### Changed
16+
17+
- Updated Alarm recommendation tool to support CloudWatch Anomaly Detection Alarms
18+
919
## [0.0.4] - 2025-07-11
1020

1121
### Changed

src/cloudwatch-mcp-server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ Alarm Recommendations - Suggests recommended alarm configurations for CloudWatch
2828
### Tools for CloudWatch Metrics
2929
* `get_metric_data` - Retrieves detailed CloudWatch metric data for any CloudWatch metric. Use this for general CloudWatch metrics that aren't specific to Application Signals. Provides ability to query any metric namespace, dimension, and statistic
3030
* `get_metric_metadata` - Retrieves comprehensive metadata about a specific CloudWatch metric
31-
* `analyze_metric` - Analyzes CloudWatch metric data for patterns, seasonality, and statistical properties
3231
* `get_recommended_metric_alarms` - Gets recommended alarms for a CloudWatch metric
32+
* `analyze_metric` - Analyzes CloudWatch metric data to determine trend, seasonality, and statistical properties
3333

3434
### Tools for CloudWatch Alarms
3535
* `get_active_alarms` - Identifies currently active CloudWatch alarms across the account
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import logging
17+
import os
18+
from awslabs.cloudwatch_mcp_server.cloudwatch_metrics.constants import COMPARISON_OPERATOR_ANOMALY
19+
from jinja2 import Environment, FileSystemLoader, select_autoescape
20+
from typing import Any, Dict
21+
22+
23+
logger = logging.getLogger(__name__)
24+
25+
26+
class CloudFormationTemplateGenerator:
27+
"""Generate CloudFormation JSON for CloudWatch Anomaly Detection Alarms using templates."""
28+
29+
ANOMALY_DETECTION_ALARM_TEMPLATE = 'anomaly_detection_alarm.json'
30+
31+
def __init__(self):
32+
"""Initialize the CloudFormation template generator."""
33+
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
34+
self.env = Environment(
35+
loader=FileSystemLoader(template_dir),
36+
autoescape=select_autoescape(['html', 'xml', 'json']),
37+
)
38+
39+
def generate_template(self, alarm_data: Dict[str, Any]) -> Dict[str, Any]:
40+
"""Generate CFN template for a single CloudWatch Alarm."""
41+
if not self._is_anomaly_detection_alarm(alarm_data):
42+
return {}
43+
44+
# Process alarm data and add computed fields
45+
formatted_data = self._format_anomaly_detection_alarm_data(alarm_data)
46+
alarm_json = self._generate_anomaly_detection_alarm_resource(formatted_data)
47+
resources = json.loads(alarm_json)
48+
49+
final_template = {
50+
'AWSTemplateFormatVersion': '2010-09-09',
51+
'Description': 'CloudWatch Alarms and Anomaly Detectors',
52+
'Resources': resources,
53+
}
54+
55+
return final_template
56+
57+
def _format_anomaly_detection_alarm_data(self, alarm_data: Dict[str, Any]) -> Dict[str, Any]:
58+
"""Sanitise alarm data and add computed fields."""
59+
formatted_data = alarm_data.copy()
60+
61+
# Generate alarm name if not provided
62+
if 'alarmName' not in formatted_data:
63+
metric_name = alarm_data.get('metricName', 'Alarm')
64+
namespace = alarm_data.get('namespace', '')
65+
formatted_data['alarmName'] = self._generate_alarm_name(metric_name, namespace)
66+
67+
# Generate resource key (sanitized alarm name for CloudFormation resource)
68+
formatted_data['resourceKey'] = self._sanitize_resource_name(formatted_data['alarmName'])
69+
70+
# Process threshold value
71+
threshold = alarm_data.get('threshold', {})
72+
formatted_data['sensitivity'] = threshold.get('sensitivity', 2)
73+
74+
# Set defaults
75+
formatted_data.setdefault(
76+
'alarmDescription', 'CloudWatch alarm generated by CloudWatch MCP server.'
77+
)
78+
formatted_data.setdefault('statistic', 'Average')
79+
formatted_data.setdefault('period', 300)
80+
formatted_data.setdefault('evaluationPeriods', 2)
81+
formatted_data.setdefault('datapointsToAlarm', 2)
82+
formatted_data.setdefault('comparisonOperator', COMPARISON_OPERATOR_ANOMALY)
83+
formatted_data.setdefault('treatMissingData', 'missing')
84+
formatted_data.setdefault('dimensions', [])
85+
86+
return formatted_data
87+
88+
def _generate_anomaly_detection_alarm_resource(self, alarm_data: Dict[str, Any]) -> str:
89+
"""Generate CloudWatch anomaly detection alarm template using Jinja2.
90+
91+
Args:
92+
alarm_data: Alarm configuration data
93+
94+
Returns:
95+
str: Generated CloudFormation template
96+
"""
97+
template = self.env.get_template(self.ANOMALY_DETECTION_ALARM_TEMPLATE)
98+
alarm_resource = template.render(**alarm_data)
99+
100+
return alarm_resource
101+
102+
def _is_anomaly_detection_alarm(self, alarm_data: Dict[str, Any]) -> bool:
103+
return alarm_data.get('comparisonOperator') == COMPARISON_OPERATOR_ANOMALY
104+
105+
def _generate_alarm_name(self, metric_name: str, namespace: str) -> str:
106+
"""Generate alarm name from metric name and namespace."""
107+
return f'{metric_name.capitalize()}{namespace.replace("/", "").replace("AWS", "")}Alarm'
108+
109+
def _sanitize_resource_name(self, name: str) -> str:
110+
"""Sanitize name for CloudFormation resource key."""
111+
sanitized = name.replace('-', '').replace('_', '').replace('/', '').replace(' ', '')
112+
# Validate CloudFormation naming requirements
113+
if not sanitized or not sanitized[0].isalpha():
114+
logger.warning(f'Invalid resource name: {sanitized} (must start with letter)')
115+
sanitized = 'Resource' + sanitized
116+
if len(sanitized) > 255:
117+
logger.warning(f'Resource name too long ({len(sanitized)} chars), truncating')
118+
sanitized = sanitized[:255]
119+
return sanitized

src/cloudwatch-mcp-server/awslabs/cloudwatch_mcp_server/cloudwatch_metrics/constants.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,28 @@
1414

1515
# CloudWatch MCP Server Constants
1616

17-
# Analysis period constants
18-
DEFAULT_ANALYSIS_PERIOD_HOURS = 2 * 7 * 24 # 2 weeks in hours
17+
# Time constants
18+
SECONDS_PER_MINUTE = 60
1919
MINUTES_PER_HOUR = 60
20+
HOURS_PER_DAY = 24
21+
DAYS_PER_WEEK = 7
22+
23+
# Analysis period constants
24+
DEFAULT_ANALYSIS_WEEKS = 2
25+
DEFAULT_ANALYSIS_PERIOD = (
26+
MINUTES_PER_HOUR * HOURS_PER_DAY * DEFAULT_ANALYSIS_WEEKS * DAYS_PER_WEEK
27+
) # 2 weeks in minutes
2028

2129
# Threshold constants
2230
DEFAULT_SENSITIVITY = 2.0
23-
ANOMALY_DETECTION_TYPE = "anomaly_detection"
24-
STATIC_TYPE = "static"
31+
ANOMALY_DETECTION_TYPE = 'anomaly_detection'
32+
STATIC_TYPE = 'static'
33+
COMPARISON_OPERATOR_ANOMALY = 'LessThanLowerOrGreaterThanUpperThreshold'
34+
TREAT_MISSING_DATA_BREACHING = 'breaching'
2535

2636
# Seasonality constants
27-
SEASONALITY_STRENGTH_THRESHOLD = 0.6 # See https://robjhyndman.com/hyndsight/tsoutliers/
37+
SEASONALITY_STRENGTH_THRESHOLD = 0.6 # See https://robjhyndman.com/hyndsight/tsoutliers/
38+
ROUNDING_THRESHOLD = 0.1
2839

2940
# Numerical stability
3041
NUMERICAL_STABILITY_THRESHOLD = 1e-10

0 commit comments

Comments
 (0)