Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions src/cloudwatch-mcp-server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

## [0.0.5] - 2025-10-06

### Added
- Added tool to analyze CloudWatch Metric data

### Changed

- Updated Alarm recommendation tool to support CloudWatch Anomaly Detection Alarms

## [0.0.4] - 2025-07-11

### Changed
Expand Down
1 change: 1 addition & 0 deletions src/cloudwatch-mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Alarm Recommendations - Suggests recommended alarm configurations for CloudWatch
* `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
* `get_metric_metadata` - Retrieves comprehensive metadata about a specific CloudWatch metric
* `get_recommended_metric_alarms` - Gets recommended alarms for a CloudWatch metric
* `analyze_metric` - Analyzes CloudWatch metric data to determine trend, seasonality, and statistical properties

### Tools for CloudWatch Alarms
* `get_active_alarms` - Identifies currently active CloudWatch alarms across the account
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import logging
import os
from awslabs.cloudwatch_mcp_server.cloudwatch_metrics.constants import COMPARISON_OPERATOR_ANOMALY
from jinja2 import Environment, FileSystemLoader, select_autoescape
from typing import Any, Dict


logger = logging.getLogger(__name__)


class CloudFormationTemplateGenerator:
"""Generate CloudFormation JSON for CloudWatch Anomaly Detection Alarms using templates."""

ANOMALY_DETECTION_ALARM_TEMPLATE = 'anomaly_detection_alarm.json'

def __init__(self):
"""Initialize the CloudFormation template generator."""
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
self.env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(['html', 'xml', 'json']),
)

def generate_template(self, alarm_data: Dict[str, Any]) -> Dict[str, Any]:
"""Generate CFN template for a single CloudWatch Alarm."""
if not self._is_anomaly_detection_alarm(alarm_data):
return {}

# Process alarm data and add computed fields
formatted_data = self._format_anomaly_detection_alarm_data(alarm_data)
alarm_json = self._generate_anomaly_detection_alarm_resource(formatted_data)
resources = json.loads(alarm_json)

final_template = {
'AWSTemplateFormatVersion': '2010-09-09',
'Description': 'CloudWatch Alarms and Anomaly Detectors',
'Resources': resources,
}

return final_template

def _format_anomaly_detection_alarm_data(self, alarm_data: Dict[str, Any]) -> Dict[str, Any]:
"""Sanitise alarm data and add computed fields."""
formatted_data = alarm_data.copy()

# Generate alarm name if not provided
if 'alarmName' not in formatted_data:
metric_name = alarm_data.get('metricName', 'Alarm')
namespace = alarm_data.get('namespace', '')
formatted_data['alarmName'] = self._generate_alarm_name(metric_name, namespace)

# Generate resource key (sanitized alarm name for CloudFormation resource)
formatted_data['resourceKey'] = self._sanitize_resource_name(formatted_data['alarmName'])

# Process threshold value
threshold = alarm_data.get('threshold', {})
formatted_data['sensitivity'] = threshold.get('sensitivity', 2)

# Set defaults
formatted_data.setdefault(
'alarmDescription', 'CloudWatch alarm generated by CloudWatch MCP server.'
)
formatted_data.setdefault('statistic', 'Average')
formatted_data.setdefault('period', 300)
formatted_data.setdefault('evaluationPeriods', 2)
formatted_data.setdefault('datapointsToAlarm', 2)
formatted_data.setdefault('comparisonOperator', COMPARISON_OPERATOR_ANOMALY)
formatted_data.setdefault('treatMissingData', 'missing')
formatted_data.setdefault('dimensions', [])

return formatted_data

def _generate_anomaly_detection_alarm_resource(self, alarm_data: Dict[str, Any]) -> str:
"""Generate CloudWatch anomaly detection alarm template using Jinja2.
Args:
alarm_data: Alarm configuration data
Returns:
str: Generated CloudFormation template
"""
template = self.env.get_template(self.ANOMALY_DETECTION_ALARM_TEMPLATE)
alarm_resource = template.render(**alarm_data)

return alarm_resource

def _is_anomaly_detection_alarm(self, alarm_data: Dict[str, Any]) -> bool:
return alarm_data.get('comparisonOperator') == COMPARISON_OPERATOR_ANOMALY

def _generate_alarm_name(self, metric_name: str, namespace: str) -> str:
"""Generate alarm name from metric name and namespace."""
return f'{metric_name.capitalize()}{namespace.replace("/", "").replace("AWS", "")}Alarm'

def _sanitize_resource_name(self, name: str) -> str:
"""Sanitize name for CloudFormation resource key."""
sanitized = name.replace('-', '').replace('_', '').replace('/', '').replace(' ', '')
# Validate CloudFormation naming requirements
if not sanitized or not sanitized[0].isalpha():
logger.warning(f'Invalid resource name: {sanitized} (must start with letter)')
sanitized = 'Resource' + sanitized
if len(sanitized) > 255:
logger.warning(f'Resource name too long ({len(sanitized)} chars), truncating')
sanitized = sanitized[:255]
return sanitized
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# CloudWatch MCP Server Constants

# Time constants
SECONDS_PER_MINUTE = 60
MINUTES_PER_HOUR = 60
HOURS_PER_DAY = 24
DAYS_PER_WEEK = 7

# Analysis period constants
DEFAULT_ANALYSIS_WEEKS = 2
DEFAULT_ANALYSIS_PERIOD = (
MINUTES_PER_HOUR * HOURS_PER_DAY * DEFAULT_ANALYSIS_WEEKS * DAYS_PER_WEEK
) # 2 weeks in minutes

# Threshold constants
DEFAULT_SENSITIVITY = 2.0
ANOMALY_DETECTION_TYPE = 'anomaly_detection'
STATIC_TYPE = 'static'
COMPARISON_OPERATOR_ANOMALY = 'LessThanLowerOrGreaterThanUpperThreshold'
TREAT_MISSING_DATA_BREACHING = 'breaching'

# Seasonality constants
SEASONALITY_STRENGTH_THRESHOLD = 0.6 # See https://robjhyndman.com/hyndsight/tsoutliers/
ROUNDING_THRESHOLD = 0.1

# Numerical stability
NUMERICAL_STABILITY_THRESHOLD = 1e-10
STATISTICAL_SIGNIFICANCE_THRESHOLD = 0.05
Loading