Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/filters/metrics.py: 30%
123 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3"""
4CloudWatch Metrics suppport for resources
5"""
6import re
8from collections import namedtuple
9from concurrent.futures import as_completed
10from datetime import datetime, timedelta
12from c7n.exceptions import PolicyValidationError
13from c7n.filters.core import Filter, OPERATORS
14from c7n.utils import local_session, type_schema, chunks
17class MetricsFilter(Filter):
18 """Supports cloud watch metrics filters on resources.
20 All resources that have cloud watch metrics are supported.
22 Docs on cloud watch metrics
24 - GetMetricStatistics
25 https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_GetMetricStatistics.html
27 - Supported Metrics
28 https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html
30 .. code-block:: yaml
32 - name: ec2-underutilized
33 resource: ec2
34 filters:
35 - type: metrics
36 name: CPUUtilization
37 days: 4
38 period: 86400
39 value: 30
40 op: less-than
42 Note periods when a resource is not sending metrics are not part
43 of calculated statistics as in the case of a stopped ec2 instance,
44 nor for resources to new to have existed the entire
45 period. ie. being stopped for an ec2 instance wouldn't lower the
46 average cpu utilization.
48 The "missing-value" key allows a policy to specify a default
49 value when CloudWatch has no data to report:
51 .. code-block:: yaml
53 - name: elb-low-request-count
54 resource: elb
55 filters:
56 - type: metrics
57 name: RequestCount
58 statistics: Sum
59 days: 7
60 value: 7
61 missing-value: 0
62 op: less-than
64 This policy matches any ELB with fewer than 7 requests for the past week.
65 ELBs with no requests during that time will have an empty set of metrics.
66 Rather than skipping those resources, "missing-value: 0" causes the
67 policy to treat their request counts as 0.
69 Note the default statistic for metrics is Average.
70 """
72 schema = type_schema(
73 'metrics',
74 **{'namespace': {'type': 'string'},
75 'name': {'type': 'string'},
76 'dimensions': {
77 'type': 'object',
78 'patternProperties': {
79 '^.*$': {'type': 'string'}}},
80 # Type choices
81 'statistics': {'type': 'string'},
82 'days': {'type': 'number'},
83 'op': {'type': 'string', 'enum': list(OPERATORS.keys())},
84 'value': {'type': 'number'},
85 'period': {'type': 'number'},
86 'attr-multiplier': {'type': 'number'},
87 'percent-attr': {'type': 'string'},
88 'missing-value': {'type': 'number'},
89 'required': ('value', 'name')})
90 schema_alias = True
91 permissions = ("cloudwatch:GetMetricStatistics",)
93 MAX_QUERY_POINTS = 50850
94 MAX_RESULT_POINTS = 1440
96 # Default per service, for overloaded services like ec2
97 # we do type specific default namespace annotation
98 # specifically AWS/EBS and AWS/EC2Spot
100 # ditto for spot fleet
101 DEFAULT_NAMESPACE = {
102 'apigateway': 'AWS/ApiGateway',
103 'cloudfront': 'AWS/CloudFront',
104 'cloudsearch': 'AWS/CloudSearch',
105 'dynamodb': 'AWS/DynamoDB',
106 'ecs': 'AWS/ECS',
107 'ecr': 'AWS/ECR',
108 'efs': 'AWS/EFS',
109 'elasticache': 'AWS/ElastiCache',
110 'ec2': 'AWS/EC2',
111 'elb': 'AWS/ELB',
112 'elbv2': 'AWS/ApplicationELB',
113 'emr': 'AWS/ElasticMapReduce',
114 'es': 'AWS/ES',
115 'events': 'AWS/Events',
116 'firehose': 'AWS/Firehose',
117 'kinesis': 'AWS/Kinesis',
118 'lambda': 'AWS/Lambda',
119 'logs': 'AWS/Logs',
120 'redshift': 'AWS/Redshift',
121 'rds': 'AWS/RDS',
122 'route53': 'AWS/Route53',
123 's3': 'AWS/S3',
124 'sns': 'AWS/SNS',
125 'sqs': 'AWS/SQS',
126 'workspaces': 'AWS/WorkSpaces',
127 }
129 standard_stats = {'Average', 'Sum', 'Maximum', 'Minimum', 'SampleCount'}
130 extended_stats_re = re.compile(r'^p\d{1,3}\.{0,1}\d{0,1}$')
132 def __init__(self, data, manager=None):
133 super(MetricsFilter, self).__init__(data, manager)
134 self.days = self.data.get('days', 14)
136 def validate(self):
137 stats = self.data.get('statistics', 'Average')
138 if stats not in self.standard_stats and not self.extended_stats_re.match(stats):
139 raise PolicyValidationError(
140 "metrics filter statistics method %s not supported" % stats)
142 if self.days > 455:
143 raise PolicyValidationError(
144 "metrics filter days value (%s) cannot exceed 455" % self.days)
146 def get_metric_window(self):
147 """Determine start and end times for the CloudWatch metric window
149 Ensure that the window aligns with time segments based on CloudWatch's retention
150 schedule defined here:
152 https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Metric
153 """ # noqa: E501
155 duration = timedelta(self.days)
156 now = datetime.utcnow()
157 MetricWindow = namedtuple('MetricWindow', 'start end')
159 if duration <= timedelta(days=(1 / 8.0)):
160 # Align period with the start of the next second
161 # CloudWatch retention: 3 hours
162 end = now.replace(microsecond=0) + timedelta(seconds=1)
163 elif duration <= timedelta(days=15):
164 # Align period with the start of the next minute
165 # CloudWatch retention: 15 days
166 end = now.replace(second=0, microsecond=0) + timedelta(minutes=1)
167 elif duration <= timedelta(days=63):
168 # Align period with the start of the next five-minute block
169 # CloudWatch retention: 63 days
170 end = (now.replace(minute=(now.minute // 5) * 5, second=0, microsecond=0)
171 + timedelta(minutes=5))
172 else:
173 # Align period with the start of the next hour
174 # CloudWatch retention: 455 days
175 end = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
177 return MetricWindow((end - duration), end)
179 def process(self, resources, event=None):
180 self.start, self.end = self.get_metric_window()
181 self.metric = self.data['name']
182 self.period = int(self.data.get('period', (self.end - self.start).total_seconds()))
183 self.statistics = self.data.get('statistics', 'Average')
184 self.model = self.manager.get_model()
185 self.op = OPERATORS[self.data.get('op', 'less-than')]
186 self.value = self.data['value']
188 ns = self.data.get('namespace')
189 if not ns:
190 ns = getattr(self.model, 'metrics_namespace', None)
191 if not ns:
192 ns = self.DEFAULT_NAMESPACE[self.model.service]
193 self.namespace = ns
195 self.log.debug("Querying metrics for %d", len(resources))
196 matched = []
197 with self.executor_factory(max_workers=3) as w:
198 futures = []
199 for resource_set in chunks(resources, 50):
200 futures.append(
201 w.submit(self.process_resource_set, resource_set))
203 for f in as_completed(futures):
204 if f.exception():
205 self.log.warning(
206 "CW Retrieval error: %s" % f.exception())
207 continue
208 matched.extend(f.result())
209 return matched
211 def get_dimensions(self, resource):
212 return [{'Name': self.model.dimension,
213 'Value': resource[self.model.dimension]}]
215 def get_user_dimensions(self):
216 dims = []
217 if 'dimensions' not in self.data:
218 return dims
219 for k, v in self.data['dimensions'].items():
220 dims.append({'Name': k, 'Value': v})
221 return dims
223 def process_resource_set(self, resource_set):
224 client = local_session(
225 self.manager.session_factory).client('cloudwatch')
227 matched = []
228 for r in resource_set:
229 # if we overload dimensions with multiple resources we get
230 # the statistics/average over those resources.
231 dimensions = self.get_dimensions(r)
232 # Merge in any filter specified metrics, get_dimensions is
233 # commonly overridden so we can't do it there.
234 dimensions.extend(self.get_user_dimensions())
236 collected_metrics = r.setdefault('c7n.metrics', {})
237 # Note this annotation cache is policy scoped, not across
238 # policies, still the lack of full qualification on the key
239 # means multiple filters within a policy using the same metric
240 # across different periods or dimensions would be problematic.
241 key = "%s.%s.%s.%s" % (self.namespace, self.metric, self.statistics, str(self.days))
243 params = dict(
244 Namespace=self.namespace,
245 MetricName=self.metric,
246 StartTime=self.start,
247 EndTime=self.end,
248 Period=self.period,
249 Dimensions=dimensions
250 )
252 stats_key = (self.statistics in self.standard_stats
253 and 'Statistics' or 'ExtendedStatistics')
254 params[stats_key] = [self.statistics]
256 if key not in collected_metrics:
257 collected_metrics[key] = client.get_metric_statistics(
258 **params)['Datapoints']
260 # In certain cases CloudWatch reports no data for a metric.
261 # If the policy specifies a fill value for missing data, add
262 # that here before testing for matches. Otherwise, skip
263 # matching entirely.
264 if len(collected_metrics[key]) == 0:
265 if 'missing-value' not in self.data:
266 continue
267 collected_metrics[key].append({
268 'Timestamp': self.start,
269 self.statistics: self.data['missing-value'],
270 'c7n:detail': 'Fill value for missing data'
271 })
273 if self.data.get('percent-attr'):
274 rvalue = r[self.data.get('percent-attr')]
275 if self.data.get('attr-multiplier'):
276 rvalue = rvalue * self.data['attr-multiplier']
277 all_meet_condition = True
278 for data_point in collected_metrics[key]:
279 percent = (data_point[self.statistics] / rvalue * 100)
280 if not self.op(percent, self.value):
281 all_meet_condition = False
282 break
283 if all_meet_condition:
284 matched.append(r)
285 else:
286 all_meet_condition = True
287 for data_point in collected_metrics[key]:
288 if not self.op(data_point[self.statistics], self.value):
289 all_meet_condition = False
290 break
291 if all_meet_condition:
292 matched.append(r)
293 return matched
296class ShieldMetrics(MetricsFilter):
297 """Specialized metrics filter for shield
298 """
299 schema = type_schema('shield-metrics', rinherit=MetricsFilter.schema)
301 namespace = "AWS/DDoSProtection"
302 metrics = (
303 'DDoSAttackBitsPerSecond',
304 'DDoSAttackRequestsPerSecond',
305 'DDoSDetected')
307 attack_vectors = (
308 'ACKFlood',
309 'ChargenReflection',
310 'DNSReflection',
311 'GenericUDPReflection',
312 'MSSQLReflection',
313 'NetBIOSReflection',
314 'NTPReflection',
315 'PortMapper',
316 'RequestFlood',
317 'RIPReflection',
318 'SNMPReflection',
319 'SYNFlood',
320 'SSDPReflection',
321 'UDPTraffic',
322 'UDPFragment')
324 def validate(self):
325 if self.data.get('name') not in self.metrics:
326 raise PolicyValidationError(
327 "invalid shield metric %s valid:%s on %s" % (
328 self.data['name'],
329 ", ".join(self.metrics),
330 self.manager.data))
332 def get_dimensions(self, resource):
333 return [{
334 'Name': 'ResourceArn',
335 'Value': self.manager.get_arns([resource])[0]}]
337 def process(self, resources, event=None):
338 self.data['namespace'] = self.namespace
339 return super(ShieldMetrics, self).process(resources, event)