|
| 1 | +"""CloudWatch API Client |
| 2 | +
|
| 3 | +Client module for communicating with AWS CloudWatch API to retrieve EBS metric data. |
| 4 | +
|
| 5 | +Features: |
| 6 | +- Load AWS credentials from environment variables or AWS profile |
| 7 | +- Query metric data by metric name, volume ID, and time range |
| 8 | +- Support for EBS metric list |
| 9 | +- Support for statistic types (Average, Sum, Minimum, Maximum, SampleCount) |
| 10 | +""" |
| 11 | + |
| 12 | +import asyncio |
| 13 | +from datetime import datetime |
| 14 | +from typing import Optional |
| 15 | + |
| 16 | +import boto3 |
| 17 | +from botocore.exceptions import ClientError |
| 18 | + |
| 19 | +from .models import MetricDataPoint, MetricResult |
| 20 | + |
| 21 | + |
| 22 | +# Supported EBS CloudWatch metrics |
| 23 | +SUPPORTED_EBS_METRICS = [ |
| 24 | + "VolumeReadOps", |
| 25 | + "VolumeWriteOps", |
| 26 | + "VolumeReadBytes", |
| 27 | + "VolumeWriteBytes", |
| 28 | + "VolumeTotalReadTime", |
| 29 | + "VolumeTotalWriteTime", |
| 30 | + "VolumeIdleTime", |
| 31 | + "VolumeQueueLength", |
| 32 | + "VolumeThroughputPercentage", |
| 33 | + "VolumeConsumedReadWriteOps", |
| 34 | + "BurstBalance", |
| 35 | +] |
| 36 | + |
| 37 | +# Supported statistic types |
| 38 | +SUPPORTED_STATISTICS = [ |
| 39 | + "Average", |
| 40 | + "Sum", |
| 41 | + "Minimum", |
| 42 | + "Maximum", |
| 43 | + "SampleCount", |
| 44 | +] |
| 45 | + |
| 46 | +# CloudWatch namespace |
| 47 | +EBS_NAMESPACE = "AWS/EBS" |
| 48 | + |
| 49 | + |
| 50 | +class CloudWatchClient: |
| 51 | + """AWS CloudWatch API Client |
| 52 | + |
| 53 | + Client for querying CloudWatch metrics for EBS volumes. |
| 54 | + |
| 55 | + Attributes: |
| 56 | + region: AWS region (uses default region if None) |
| 57 | + _client: boto3 CloudWatch client |
| 58 | + |
| 59 | + Example: |
| 60 | + >>> client = CloudWatchClient(region="us-east-1") |
| 61 | + >>> result = await client.get_metric_statistics( |
| 62 | + ... volume_id="vol-1234567890abcdef0", |
| 63 | + ... metric_name="VolumeReadOps", |
| 64 | + ... start_time=datetime(2024, 1, 1), |
| 65 | + ... end_time=datetime(2024, 1, 2), |
| 66 | + ... period=300, |
| 67 | + ... statistics=["Average", "Maximum"] |
| 68 | + ... ) |
| 69 | + """ |
| 70 | + |
| 71 | + def __init__(self, region: Optional[str] = None): |
| 72 | + """Initialize boto3 CloudWatch client |
| 73 | + |
| 74 | + Args: |
| 75 | + region: AWS region. Loads from environment variables or AWS profile if None |
| 76 | + """ |
| 77 | + self.region = region |
| 78 | + if region: |
| 79 | + self._client = boto3.client("cloudwatch", region_name=region) |
| 80 | + else: |
| 81 | + self._client = boto3.client("cloudwatch") |
| 82 | + |
| 83 | + async def get_metric_statistics( |
| 84 | + self, |
| 85 | + volume_id: str, |
| 86 | + metric_name: str, |
| 87 | + start_time: datetime, |
| 88 | + end_time: datetime, |
| 89 | + period: int = 300, |
| 90 | + statistics: Optional[list[str]] = None |
| 91 | + ) -> MetricResult: |
| 92 | + """Query CloudWatch metric statistics |
| 93 | + |
| 94 | + Retrieves CloudWatch metric statistics for the specified EBS volume. |
| 95 | + |
| 96 | + Args: |
| 97 | + volume_id: EBS volume ID (e.g., vol-1234567890abcdef0) |
| 98 | + metric_name: CloudWatch metric name (e.g., VolumeReadOps) |
| 99 | + start_time: Query start time |
| 100 | + end_time: Query end time |
| 101 | + period: Metric collection interval in seconds (default: 300) |
| 102 | + statistics: List of statistic types to query (default: ["Average"]) |
| 103 | + |
| 104 | + Returns: |
| 105 | + MetricResult: Metric query result |
| 106 | + |
| 107 | + Raises: |
| 108 | + ValueError: If unsupported metric name or statistic type |
| 109 | + ClientError: If AWS API call fails |
| 110 | + """ |
| 111 | + if statistics is None: |
| 112 | + statistics = ["Average"] |
| 113 | + |
| 114 | + # Validate metric name |
| 115 | + if metric_name not in SUPPORTED_EBS_METRICS: |
| 116 | + raise ValueError( |
| 117 | + f"Unsupported metric name: {metric_name}. " |
| 118 | + f"Supported metrics: {', '.join(SUPPORTED_EBS_METRICS)}" |
| 119 | + ) |
| 120 | + |
| 121 | + # Validate statistic types |
| 122 | + for stat in statistics: |
| 123 | + if stat not in SUPPORTED_STATISTICS: |
| 124 | + raise ValueError( |
| 125 | + f"Unsupported statistic type: {stat}. " |
| 126 | + f"Supported statistics: {', '.join(SUPPORTED_STATISTICS)}" |
| 127 | + ) |
| 128 | + |
| 129 | + # CloudWatch API call (wrap synchronous call as async) |
| 130 | + loop = asyncio.get_event_loop() |
| 131 | + response = await loop.run_in_executor( |
| 132 | + None, |
| 133 | + lambda: self._client.get_metric_statistics( |
| 134 | + Namespace=EBS_NAMESPACE, |
| 135 | + MetricName=metric_name, |
| 136 | + Dimensions=[ |
| 137 | + { |
| 138 | + "Name": "VolumeId", |
| 139 | + "Value": volume_id |
| 140 | + } |
| 141 | + ], |
| 142 | + StartTime=start_time, |
| 143 | + EndTime=end_time, |
| 144 | + Period=period, |
| 145 | + Statistics=statistics |
| 146 | + ) |
| 147 | + ) |
| 148 | + |
| 149 | + # Convert data points |
| 150 | + datapoints = [] |
| 151 | + for dp in response.get("Datapoints", []): |
| 152 | + # Use first statistic value |
| 153 | + value = None |
| 154 | + for stat in statistics: |
| 155 | + if stat in dp: |
| 156 | + value = dp[stat] |
| 157 | + break |
| 158 | + |
| 159 | + if value is not None: |
| 160 | + datapoints.append( |
| 161 | + MetricDataPoint( |
| 162 | + timestamp=dp["Timestamp"], |
| 163 | + value=value, |
| 164 | + unit=dp.get("Unit", "None") |
| 165 | + ) |
| 166 | + ) |
| 167 | + |
| 168 | + # Sort by timestamp |
| 169 | + datapoints.sort(key=lambda x: x.timestamp) |
| 170 | + |
| 171 | + # Calculate statistics |
| 172 | + average = None |
| 173 | + maximum = None |
| 174 | + minimum = None |
| 175 | + total_sum = None |
| 176 | + |
| 177 | + if datapoints: |
| 178 | + values = [dp.value for dp in datapoints] |
| 179 | + if "Average" in statistics: |
| 180 | + average = sum(values) / len(values) |
| 181 | + if "Maximum" in statistics: |
| 182 | + maximum = max(values) |
| 183 | + if "Minimum" in statistics: |
| 184 | + minimum = min(values) |
| 185 | + if "Sum" in statistics: |
| 186 | + total_sum = sum(values) |
| 187 | + |
| 188 | + return MetricResult( |
| 189 | + metric_name=metric_name, |
| 190 | + volume_id=volume_id, |
| 191 | + datapoints=datapoints, |
| 192 | + average=average, |
| 193 | + maximum=maximum, |
| 194 | + minimum=minimum, |
| 195 | + sum=total_sum |
| 196 | + ) |
| 197 | + |
| 198 | + def list_available_metrics(self) -> list[str]: |
| 199 | + """Return list of available EBS metrics |
| 200 | + |
| 201 | + Returns a list of all EBS metric names supported by CloudWatch. |
| 202 | + |
| 203 | + Returns: |
| 204 | + list[str]: List of supported EBS metric names |
| 205 | + """ |
| 206 | + return SUPPORTED_EBS_METRICS.copy() |
0 commit comments