Cloudwatch - Return build-in S3 metrics - take 2 (#3839)
This commit is contained in:
parent
7664cab828
commit
52aeac1cee
@ -2,11 +2,15 @@ import json
|
|||||||
|
|
||||||
from boto3 import Session
|
from boto3 import Session
|
||||||
|
|
||||||
|
from moto.core import (
|
||||||
|
BaseBackend,
|
||||||
|
BaseModel,
|
||||||
|
CloudWatchMetricProvider,
|
||||||
|
)
|
||||||
from moto.core.utils import (
|
from moto.core.utils import (
|
||||||
iso_8601_datetime_without_milliseconds,
|
iso_8601_datetime_without_milliseconds,
|
||||||
iso_8601_datetime_with_nanoseconds,
|
iso_8601_datetime_with_nanoseconds,
|
||||||
)
|
)
|
||||||
from moto.core import BaseBackend, BaseModel
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dateutil.tz import tzutc
|
from dateutil.tz import tzutc
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@ -258,6 +262,7 @@ class Statistics:
|
|||||||
self.timestamp = iso_8601_datetime_without_milliseconds(dt)
|
self.timestamp = iso_8601_datetime_without_milliseconds(dt)
|
||||||
self.values = []
|
self.values = []
|
||||||
self.stats = stats
|
self.stats = stats
|
||||||
|
self.unit = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sample_count(self):
|
def sample_count(self):
|
||||||
@ -266,10 +271,6 @@ class Statistics:
|
|||||||
|
|
||||||
return len(self.values)
|
return len(self.values)
|
||||||
|
|
||||||
@property
|
|
||||||
def unit(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sum(self):
|
def sum(self):
|
||||||
if "Sum" not in self.stats:
|
if "Sum" not in self.stats:
|
||||||
@ -325,9 +326,10 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
# Retrieve a list of all OOTB metrics that are provided by metrics providers
|
# Retrieve a list of all OOTB metrics that are provided by metrics providers
|
||||||
# Computed on the fly
|
# Computed on the fly
|
||||||
def aws_metric_data(self):
|
def aws_metric_data(self):
|
||||||
|
providers = CloudWatchMetricProvider.__subclasses__()
|
||||||
md = []
|
md = []
|
||||||
for name, service in metric_providers.items():
|
for provider in providers:
|
||||||
md.extend(service.get_cloudwatch_metrics())
|
md.extend(provider.get_cloudwatch_metrics())
|
||||||
return md
|
return md
|
||||||
|
|
||||||
def put_metric_alarm(
|
def put_metric_alarm(
|
||||||
@ -530,13 +532,14 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
end_time,
|
end_time,
|
||||||
period,
|
period,
|
||||||
stats,
|
stats,
|
||||||
|
dimensions,
|
||||||
unit=None,
|
unit=None,
|
||||||
dimensions=None,
|
|
||||||
):
|
):
|
||||||
period_delta = timedelta(seconds=period)
|
period_delta = timedelta(seconds=period)
|
||||||
|
# TODO: Also filter by unit and dimensions
|
||||||
filtered_data = [
|
filtered_data = [
|
||||||
md
|
md
|
||||||
for md in self.metric_data
|
for md in self.get_all_metrics()
|
||||||
if md.namespace == namespace
|
if md.namespace == namespace
|
||||||
and md.name == metric_name
|
and md.name == metric_name
|
||||||
and start_time <= md.timestamp <= end_time
|
and start_time <= md.timestamp <= end_time
|
||||||
@ -566,6 +569,7 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
dt + period_delta
|
dt + period_delta
|
||||||
):
|
):
|
||||||
s.values.append(filtered_data[idx].value)
|
s.values.append(filtered_data[idx].value)
|
||||||
|
s.unit = filtered_data[idx].unit
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
if not s.values:
|
if not s.values:
|
||||||
@ -685,8 +689,3 @@ for region in Session().get_available_regions(
|
|||||||
cloudwatch_backends[region] = CloudWatchBackend(region)
|
cloudwatch_backends[region] = CloudWatchBackend(region)
|
||||||
for region in Session().get_available_regions("cloudwatch", partition_name="aws-cn"):
|
for region in Session().get_available_regions("cloudwatch", partition_name="aws-cn"):
|
||||||
cloudwatch_backends[region] = CloudWatchBackend(region)
|
cloudwatch_backends[region] = CloudWatchBackend(region)
|
||||||
|
|
||||||
# List of services that provide OOTB CW metrics
|
|
||||||
# See the S3Backend constructor for an example
|
|
||||||
# TODO: We might have to separate this out per region for non-global services
|
|
||||||
metric_providers = {}
|
|
||||||
|
@ -186,11 +186,9 @@ class CloudWatchResponse(BaseResponse):
|
|||||||
# Unsupported Parameters (To Be Implemented)
|
# Unsupported Parameters (To Be Implemented)
|
||||||
unit = self._get_param("Unit")
|
unit = self._get_param("Unit")
|
||||||
extended_statistics = self._get_param("ExtendedStatistics")
|
extended_statistics = self._get_param("ExtendedStatistics")
|
||||||
if extended_statistics:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
# TODO: this should instead throw InvalidParameterCombination
|
# TODO: this should instead throw InvalidParameterCombination
|
||||||
if not statistics:
|
if not statistics and not extended_statistics:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"Must specify either Statistics or ExtendedStatistics"
|
"Must specify either Statistics or ExtendedStatistics"
|
||||||
)
|
)
|
||||||
@ -202,7 +200,7 @@ class CloudWatchResponse(BaseResponse):
|
|||||||
end_time,
|
end_time,
|
||||||
period,
|
period,
|
||||||
statistics,
|
statistics,
|
||||||
unit,
|
unit=unit,
|
||||||
dimensions=dimensions,
|
dimensions=dimensions,
|
||||||
)
|
)
|
||||||
template = self.response_template(GET_METRIC_STATISTICS_TEMPLATE)
|
template = self.response_template(GET_METRIC_STATISTICS_TEMPLATE)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from .models import BaseModel, BaseBackend, moto_api_backend, ACCOUNT_ID # noqa
|
from .models import BaseModel, BaseBackend, moto_api_backend, ACCOUNT_ID # noqa
|
||||||
from .models import CloudFormationModel # noqa
|
from .models import CloudFormationModel, CloudWatchMetricProvider # noqa
|
||||||
from .models import patch_client, patch_resource # noqa
|
from .models import patch_client, patch_resource # noqa
|
||||||
from .responses import ActionAuthenticatorMixin
|
from .responses import ActionAuthenticatorMixin
|
||||||
|
|
||||||
|
@ -958,4 +958,11 @@ class MotoAPIBackend(BaseBackend):
|
|||||||
self.__init__()
|
self.__init__()
|
||||||
|
|
||||||
|
|
||||||
|
class CloudWatchMetricProvider(object):
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_cloudwatch_metrics():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
moto_api_backend = MotoAPIBackend()
|
moto_api_backend = MotoAPIBackend()
|
||||||
|
@ -10,14 +10,20 @@ import random
|
|||||||
import string
|
import string
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
import pytz
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from bisect import insort
|
from bisect import insort
|
||||||
import pytz
|
from moto.core import (
|
||||||
|
ACCOUNT_ID,
|
||||||
|
BaseBackend,
|
||||||
|
BaseModel,
|
||||||
|
CloudFormationModel,
|
||||||
|
CloudWatchMetricProvider,
|
||||||
|
)
|
||||||
|
|
||||||
from moto.core import ACCOUNT_ID, BaseBackend, BaseModel, CloudFormationModel
|
|
||||||
from moto.core.utils import (
|
from moto.core.utils import (
|
||||||
iso_8601_datetime_without_milliseconds_s3,
|
iso_8601_datetime_without_milliseconds_s3,
|
||||||
rfc_1123_datetime,
|
rfc_1123_datetime,
|
||||||
@ -1314,7 +1320,7 @@ class FakeBucket(CloudFormationModel):
|
|||||||
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
class S3Backend(BaseBackend):
|
class S3Backend(BaseBackend, CloudWatchMetricProvider):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.buckets = {}
|
self.buckets = {}
|
||||||
self.account_public_access_block = None
|
self.account_public_access_block = None
|
||||||
@ -1358,9 +1364,10 @@ class S3Backend(BaseBackend):
|
|||||||
# Must provide a method 'get_cloudwatch_metrics' that will return a list of metrics, based on the data available
|
# Must provide a method 'get_cloudwatch_metrics' that will return a list of metrics, based on the data available
|
||||||
# metric_providers["S3"] = self
|
# metric_providers["S3"] = self
|
||||||
|
|
||||||
def get_cloudwatch_metrics(self):
|
@classmethod
|
||||||
|
def get_cloudwatch_metrics(cls):
|
||||||
metrics = []
|
metrics = []
|
||||||
for name, bucket in self.buckets.items():
|
for name, bucket in s3_backend.buckets.items():
|
||||||
metrics.append(
|
metrics.append(
|
||||||
MetricDatum(
|
MetricDatum(
|
||||||
namespace="AWS/S3",
|
namespace="AWS/S3",
|
||||||
@ -1370,7 +1377,10 @@ class S3Backend(BaseBackend):
|
|||||||
{"Name": "StorageType", "Value": "StandardStorage"},
|
{"Name": "StorageType", "Value": "StandardStorage"},
|
||||||
{"Name": "BucketName", "Value": name},
|
{"Name": "BucketName", "Value": name},
|
||||||
],
|
],
|
||||||
timestamp=datetime.datetime.now(),
|
timestamp=datetime.datetime.now(tz=pytz.utc).replace(
|
||||||
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
|
),
|
||||||
|
unit="Bytes",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
metrics.append(
|
metrics.append(
|
||||||
@ -1382,7 +1392,10 @@ class S3Backend(BaseBackend):
|
|||||||
{"Name": "StorageType", "Value": "AllStorageTypes"},
|
{"Name": "StorageType", "Value": "AllStorageTypes"},
|
||||||
{"Name": "BucketName", "Value": name},
|
{"Name": "BucketName", "Value": name},
|
||||||
],
|
],
|
||||||
timestamp=datetime.datetime.now(),
|
timestamp=datetime.datetime.now(tz=pytz.utc).replace(
|
||||||
|
hour=0, minute=0, second=0, microsecond=0
|
||||||
|
),
|
||||||
|
unit="Count",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return metrics
|
return metrics
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import boto
|
import boto
|
||||||
from boto.ec2.cloudwatch.alarm import MetricAlarm
|
from boto.ec2.cloudwatch.alarm import MetricAlarm
|
||||||
|
from boto.s3.key import Key
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import sure # noqa # pylint: disable=unused-import
|
import sure # noqa # pylint: disable=unused-import
|
||||||
|
|
||||||
from moto import mock_cloudwatch_deprecated
|
from moto import mock_cloudwatch_deprecated, mock_s3_deprecated
|
||||||
|
|
||||||
|
|
||||||
def alarm_fixture(name="tester", action=None):
|
def alarm_fixture(name="tester", action=None):
|
||||||
@ -194,34 +195,31 @@ def test_get_metric_statistics():
|
|||||||
datapoints.should.have.length_of(0)
|
datapoints.should.have.length_of(0)
|
||||||
|
|
||||||
|
|
||||||
# TODO: THIS IS CURRENTLY BROKEN!
|
@mock_s3_deprecated
|
||||||
# @mock_s3_deprecated
|
@mock_cloudwatch_deprecated
|
||||||
# @mock_cloudwatch_deprecated
|
def test_cloudwatch_return_s3_metrics():
|
||||||
# def test_cloudwatch_return_s3_metrics():
|
|
||||||
#
|
region = "us-east-1"
|
||||||
# region = "us-east-1"
|
|
||||||
#
|
cw = boto.ec2.cloudwatch.connect_to_region(region)
|
||||||
# cw = boto.ec2.cloudwatch.connect_to_region(region)
|
s3 = boto.s3.connect_to_region(region)
|
||||||
# s3 = boto.s3.connect_to_region(region)
|
bucket_name_1 = "test-bucket-1"
|
||||||
#
|
bucket_name_2 = "test-bucket-2"
|
||||||
# bucket_name_1 = "test-bucket-1"
|
|
||||||
# bucket_name_2 = "test-bucket-2"
|
bucket1 = s3.create_bucket(bucket_name=bucket_name_1)
|
||||||
#
|
key = Key(bucket1)
|
||||||
# bucket1 = s3.create_bucket(bucket_name=bucket_name_1)
|
key.key = "the-key"
|
||||||
# key = Key(bucket1)
|
key.set_contents_from_string("foobar" * 4)
|
||||||
# key.key = "the-key"
|
s3.create_bucket(bucket_name=bucket_name_2)
|
||||||
# key.set_contents_from_string("foobar" * 4)
|
|
||||||
# s3.create_bucket(bucket_name=bucket_name_2)
|
metrics_s3_bucket_1 = cw.list_metrics(dimensions={"BucketName": bucket_name_1})
|
||||||
#
|
|
||||||
# metrics_s3_bucket_1 = cw.list_metrics(dimensions={"BucketName": bucket_name_1})
|
# Verify that the OOTB S3 metrics are available for the created buckets
|
||||||
# # Verify that the OOTB S3 metrics are available for the created buckets
|
len(metrics_s3_bucket_1).should.be(2)
|
||||||
# len(metrics_s3_bucket_1).should.be(2)
|
metric_names = [m.name for m in metrics_s3_bucket_1]
|
||||||
# metric_names = [m.name for m in metrics_s3_bucket_1]
|
sorted(metric_names).should.equal(["BucketSizeBytes", "NumberOfObjects"])
|
||||||
# sorted(metric_names).should.equal(
|
|
||||||
# ["Metric:BucketSizeBytes", "Metric:NumberOfObjects"]
|
# Delete everything, to make sure it's not picked up in later tests
|
||||||
# )
|
bucket1.delete_key("the-key")
|
||||||
#
|
s3.delete_bucket("test-bucket-1")
|
||||||
# # Explicit clean up - the metrics for these buckets are messing with subsequent tests
|
s3.delete_bucket("test-bucket-2")
|
||||||
# key.delete()
|
|
||||||
# s3.delete_bucket(bucket_name_1)
|
|
||||||
# s3.delete_bucket(bucket_name_2)
|
|
||||||
|
@ -11,7 +11,7 @@ from freezegun import freeze_time
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from moto import mock_cloudwatch
|
from moto import mock_cloudwatch, mock_s3
|
||||||
from moto.core import ACCOUNT_ID
|
from moto.core import ACCOUNT_ID
|
||||||
|
|
||||||
|
|
||||||
@ -170,6 +170,7 @@ def test_get_metric_statistics_dimensions():
|
|||||||
Statistics=["Average", "Sum"],
|
Statistics=["Average", "Sum"],
|
||||||
**params[0],
|
**params[0],
|
||||||
)
|
)
|
||||||
|
print(stats)
|
||||||
stats["Datapoints"].should.have.length_of(1)
|
stats["Datapoints"].should.have.length_of(1)
|
||||||
datapoint = stats["Datapoints"][0]
|
datapoint = stats["Datapoints"][0]
|
||||||
datapoint["Sum"].should.equal(params[1])
|
datapoint["Sum"].should.equal(params[1])
|
||||||
@ -342,7 +343,7 @@ def test_list_metrics():
|
|||||||
def test_list_metrics_paginated():
|
def test_list_metrics_paginated():
|
||||||
cloudwatch = boto3.client("cloudwatch", "eu-west-1")
|
cloudwatch = boto3.client("cloudwatch", "eu-west-1")
|
||||||
# Verify that only a single page of metrics is returned
|
# Verify that only a single page of metrics is returned
|
||||||
cloudwatch.list_metrics()["Metrics"].should.be.empty
|
cloudwatch.list_metrics().shouldnt.have.key("NextToken")
|
||||||
# Verify we can't pass a random NextToken
|
# Verify we can't pass a random NextToken
|
||||||
with pytest.raises(ClientError) as e:
|
with pytest.raises(ClientError) as e:
|
||||||
cloudwatch.list_metrics(NextToken=str(uuid4()))
|
cloudwatch.list_metrics(NextToken=str(uuid4()))
|
||||||
@ -710,6 +711,95 @@ def test_get_metric_data_for_multiple_metrics():
|
|||||||
res2["Values"].should.equal([25.0])
|
res2["Values"].should.equal([25.0])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudwatch
|
||||||
|
@mock_s3
|
||||||
|
def test_cloudwatch_return_s3_metrics():
|
||||||
|
utc_now = datetime.now(tz=pytz.utc)
|
||||||
|
bucket_name = "examplebucket"
|
||||||
|
cloudwatch = boto3.client("cloudwatch", "eu-west-3")
|
||||||
|
|
||||||
|
# given
|
||||||
|
s3 = boto3.resource("s3")
|
||||||
|
s3_client = boto3.client("s3")
|
||||||
|
bucket = s3.Bucket(bucket_name)
|
||||||
|
bucket.create(CreateBucketConfiguration={"LocationConstraint": "eu-west-3"})
|
||||||
|
bucket.put_object(Body=b"ABCD", Key="file.txt")
|
||||||
|
|
||||||
|
# when
|
||||||
|
metrics = cloudwatch.list_metrics(
|
||||||
|
Dimensions=[{"Name": "BucketName", "Value": bucket_name}]
|
||||||
|
)["Metrics"]
|
||||||
|
|
||||||
|
# then
|
||||||
|
metrics.should.have.length_of(2)
|
||||||
|
metrics.should.contain(
|
||||||
|
{
|
||||||
|
"Namespace": "AWS/S3",
|
||||||
|
"MetricName": "NumberOfObjects",
|
||||||
|
"Dimensions": [
|
||||||
|
{"Name": "StorageType", "Value": "AllStorageTypes"},
|
||||||
|
{"Name": "BucketName", "Value": bucket_name},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
metrics.should.contain(
|
||||||
|
{
|
||||||
|
"Namespace": "AWS/S3",
|
||||||
|
"MetricName": "BucketSizeBytes",
|
||||||
|
"Dimensions": [
|
||||||
|
{"Name": "StorageType", "Value": "StandardStorage"},
|
||||||
|
{"Name": "BucketName", "Value": bucket_name},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# when
|
||||||
|
stats = cloudwatch.get_metric_statistics(
|
||||||
|
Namespace="AWS/S3",
|
||||||
|
MetricName="BucketSizeBytes",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "BucketName", "Value": bucket_name},
|
||||||
|
{"Name": "StorageType", "Value": "StandardStorage"},
|
||||||
|
],
|
||||||
|
StartTime=utc_now - timedelta(days=2),
|
||||||
|
EndTime=utc_now,
|
||||||
|
Period=86400,
|
||||||
|
Statistics=["Average"],
|
||||||
|
Unit="Bytes",
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
stats.should.have.key("Label").equal("BucketSizeBytes")
|
||||||
|
stats.should.have.key("Datapoints").length_of(1)
|
||||||
|
data_point = stats["Datapoints"][0]
|
||||||
|
data_point.should.have.key("Average").being.above(0)
|
||||||
|
data_point.should.have.key("Unit").being.equal("Bytes")
|
||||||
|
|
||||||
|
# when
|
||||||
|
stats = cloudwatch.get_metric_statistics(
|
||||||
|
Namespace="AWS/S3",
|
||||||
|
MetricName="NumberOfObjects",
|
||||||
|
Dimensions=[
|
||||||
|
{"Name": "BucketName", "Value": bucket_name},
|
||||||
|
{"Name": "StorageType", "Value": "AllStorageTypes"},
|
||||||
|
],
|
||||||
|
StartTime=utc_now - timedelta(days=2),
|
||||||
|
EndTime=utc_now,
|
||||||
|
Period=86400,
|
||||||
|
Statistics=["Average"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# then
|
||||||
|
stats.should.have.key("Label").equal("NumberOfObjects")
|
||||||
|
stats.should.have.key("Datapoints").length_of(1)
|
||||||
|
data_point = stats["Datapoints"][0]
|
||||||
|
data_point.should.have.key("Average").being.equal(1)
|
||||||
|
data_point.should.have.key("Unit").being.equal("Count")
|
||||||
|
|
||||||
|
s3_client.delete_object(Bucket=bucket_name, Key="file.txt")
|
||||||
|
s3_client.delete_bucket(Bucket=bucket_name)
|
||||||
|
|
||||||
|
|
||||||
@mock_cloudwatch
|
@mock_cloudwatch
|
||||||
def test_put_metric_alarm():
|
def test_put_metric_alarm():
|
||||||
# given
|
# given
|
||||||
|
Loading…
Reference in New Issue
Block a user