-
Couldn't load subscription status.
- Fork 2.4k
Adding support for metric publishing, manually tested and tests pending. #2062
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
252d835
Adding support for metric publishing, manually tested and tests pending.
a6891b1
Adding make action for running test with html coverage.
acd322f
Adding unit tests for metrics.
51f8c39
Writing integration tests for metrics publishing.
89b620b
Black reformatting
f2d1236
Fixing unit test.
aa24ad8
Addressing PR comments.
26403fe
Clearing __init__py from test module.
953cce8
Updating documentation for publish method.
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import boto3 | ||
| import time | ||
| import uuid | ||
| from datetime import datetime, timedelta | ||
| from unittest import TestCase | ||
| from samtranslator.metrics.metrics import ( | ||
| Metrics, | ||
| CWMetricsPublisher, | ||
| ) | ||
|
|
||
|
|
||
| class MetricsIntegrationTest(TestCase): | ||
| """ | ||
| This class will use a unique metric namespace to create metrics. There is no cleanup done here | ||
| because if a particular namespace is unsed for 2 weeks it'll be cleanedup by cloudwatch. | ||
| """ | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| cls.cw_client = boto3.client("cloudwatch") | ||
| cls.cw_metric_publisher = CWMetricsPublisher(cls.cw_client) | ||
|
|
||
| def test_publish_single_metric(self): | ||
| now = datetime.now() | ||
| tomorrow = now + timedelta(days=1) | ||
| namespace = self.get_unique_namespace() | ||
| metrics = Metrics(namespace, CWMetricsPublisher(self.cw_client)) | ||
| dimensions = [{"Name": "Dim1", "Value": "Val1"}, {"Name": "Dim2", "Value": "Val2"}] | ||
| metrics.record_count("TestCountMetric", 1, dimensions=dimensions) | ||
| metrics.record_count("TestCountMetric", 3, dimensions=dimensions) | ||
| metrics.record_latency("TestLatencyMetric", 1200, dimensions=dimensions) | ||
| metrics.record_latency("TestLatencyMetric", 1600, dimensions=dimensions) | ||
| metrics.publish() | ||
| total_count = self.get_metric_data( | ||
| namespace, | ||
| "TestCountMetric", | ||
| dimensions, | ||
| datetime(now.year, now.month, now.day), | ||
| datetime(tomorrow.year, tomorrow.month, tomorrow.day), | ||
| ) | ||
| latency_avg = self.get_metric_data( | ||
| namespace, | ||
| "TestLatencyMetric", | ||
| dimensions, | ||
| datetime(now.year, now.month, now.day), | ||
| datetime(tomorrow.year, tomorrow.month, tomorrow.day), | ||
| stat="Average", | ||
| ) | ||
|
|
||
| self.assertEqual(total_count[0], 1 + 3) | ||
| self.assertEqual(latency_avg[0], 1400) | ||
|
|
||
| def get_unique_namespace(self): | ||
| namespace = "SinglePublishTest-{}".format(uuid.uuid1()) | ||
| while True: | ||
| response = self.cw_client.list_metrics(Namespace=namespace) | ||
| if not response["Metrics"]: | ||
| return namespace | ||
| namespace = "SinglePublishTest-{}".format(uuid.uuid1()) | ||
|
|
||
| def get_metric_data(self, namespace, metric_name, dimensions, start_time, end_time, stat="Sum"): | ||
| retries = 3 | ||
| while retries > 0: | ||
| retries -= 1 | ||
| response = self.cw_client.get_metric_data( | ||
| MetricDataQueries=[ | ||
| { | ||
| "Id": namespace.replace("-", "_").lower(), | ||
| "MetricStat": { | ||
| "Metric": {"Namespace": namespace, "MetricName": metric_name, "Dimensions": dimensions}, | ||
| "Period": 60, | ||
| "Stat": stat, | ||
| }, | ||
| } | ||
| ], | ||
| StartTime=start_time, | ||
| EndTime=end_time, | ||
| ) | ||
| values = response["MetricDataResults"][0]["Values"] | ||
| if values: | ||
| return values | ||
| print("No values found by for metric: {}. Waiting for 5 seconds...".format(metric_name)) | ||
| time.sleep(5) | ||
| return [0] |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| """ | ||
| Helper classes to publish metrics | ||
| """ | ||
| import logging | ||
|
|
||
| LOG = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class MetricsPublisher: | ||
| """Interface for all MetricPublishers""" | ||
|
|
||
| def __init__(self): | ||
| pass | ||
|
|
||
| def publish(self, namespace, metrics): | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| class CWMetricsPublisher(MetricsPublisher): | ||
| BATCH_SIZE = 20 | ||
|
|
||
| def __init__(self, cloudwatch_client): | ||
| """ | ||
| Constructor | ||
|
|
||
| :param cloudwatch_client: cloudwatch client required to publish metrics to cloudwatch | ||
| """ | ||
| MetricsPublisher.__init__(self) | ||
| self.cloudwatch_client = cloudwatch_client | ||
|
|
||
| def publish(self, namespace, metrics): | ||
| """ | ||
| Method to publish all metrics to Cloudwatch. | ||
|
|
||
| :param namespace: namespace applied to all metrics published. | ||
| :param metrics: list of metrics to be published | ||
| """ | ||
| batch = [] | ||
| for metric in metrics: | ||
| batch.append(metric) | ||
| # Cloudwatch recommends not to send more than 20 metrics at a time | ||
| if len(batch) == self.BATCH_SIZE: | ||
| self._flush_metrics(namespace, batch) | ||
| batch = [] | ||
| self._flush_metrics(namespace, batch) | ||
|
|
||
| def _flush_metrics(self, namespace, metrics): | ||
| """ | ||
| Internal method to publish all provided metrics to cloudwatch, please make sure that array size of metrics is <= 20. | ||
| """ | ||
| metric_data = list(map(lambda m: m.get_metric_data(), metrics)) | ||
| try: | ||
| self.cloudwatch_client.put_metric_data(Namespace=namespace, MetricData=metric_data) | ||
| except Exception as e: | ||
| LOG.exception("Failed to report {} metrics".format(len(metric_data)), exc_info=e) | ||
|
|
||
|
|
||
| class DummyMetricsPublisher(MetricsPublisher): | ||
| def __init__(self): | ||
| MetricsPublisher.__init__(self) | ||
|
|
||
| def publish(self, namespace, metrics): | ||
| """Do not publish any metric, this is a dummy publisher used for offline use.""" | ||
| LOG.debug("Dummy publisher ignoring {} metrices".format(len(metrics))) | ||
|
|
||
|
|
||
| class Unit: | ||
| Seconds = "Seconds" | ||
| Microseconds = "Microseconds" | ||
| Milliseconds = "Milliseconds" | ||
| Bytes = "Bytes" | ||
| Kilobytes = "Kilobytes" | ||
| Megabytes = "Megabytes" | ||
| Bits = "Bits" | ||
| Kilobits = "Kilobits" | ||
| Megabits = "Megabits" | ||
| Percent = "Percent" | ||
| Count = "Count" | ||
|
|
||
|
|
||
| class MetricDatum: | ||
| """ | ||
| Class to hold Metric data. | ||
| """ | ||
|
|
||
| def __init__(self, name, value, unit, dimensions=None): | ||
| """ | ||
| Constructor | ||
|
|
||
| :param name: metric name | ||
| :param value: value of metric | ||
| :param unit: unit of metric (try using values from Unit class) | ||
| :param dimensions: array of dimensions applied to the metric | ||
| """ | ||
| self.name = name | ||
| self.value = value | ||
| self.unit = unit | ||
| self.dimensions = dimensions if dimensions else [] | ||
|
|
||
| def get_metric_data(self): | ||
| return {"MetricName": self.name, "Value": self.value, "Unit": self.unit, "Dimensions": self.dimensions} | ||
|
|
||
|
|
||
| class Metrics: | ||
| def __init__(self, namespace="ServerlessTransform", metrics_publisher=None): | ||
| """ | ||
| Constructor | ||
|
|
||
| :param namespace: namespace under which all metrics will be published | ||
| :param metrics_publisher: publisher to publish all metrics | ||
| """ | ||
| self.metrics_publisher = metrics_publisher if metrics_publisher else DummyMetricsPublisher() | ||
| self.metrics_cache = [] | ||
| self.namespace = namespace | ||
|
|
||
| def __del__(self): | ||
| if len(self.metrics_cache) > 0: | ||
| # attempting to publish if user forgot to call publish in code | ||
| LOG.warn("There are unpublished metrics. Please make sure you call publish after you record all metrics.") | ||
| self.publish() | ||
|
|
||
| def _record_metric(self, name, value, unit, dimensions=[]): | ||
| """ | ||
| Create and save metric objects to an array. | ||
|
|
||
| :param name: metric name | ||
| :param value: value of metric | ||
| :param unit: unit of metric (try using values from Unit class) | ||
| :param dimensions: array of dimensions applied to the metric | ||
| """ | ||
| self.metrics_cache.append(MetricDatum(name, value, unit, dimensions)) | ||
|
|
||
| def record_count(self, name, value, dimensions=[]): | ||
| """ | ||
| Create metric with unit Count. | ||
|
|
||
| :param name: metric name | ||
| :param value: value of metric | ||
| :param unit: unit of metric (try using values from Unit class) | ||
| :param dimensions: array of dimensions applied to the metric | ||
| """ | ||
| self._record_metric(name, value, Unit.Count, dimensions) | ||
|
|
||
| def record_latency(self, name, value, dimensions=[]): | ||
| """ | ||
| Create metric with unit Milliseconds. | ||
|
|
||
| :param name: metric name | ||
| :param value: value of metric | ||
| :param unit: unit of metric (try using values from Unit class) | ||
| :param dimensions: array of dimensions applied to the metric | ||
| """ | ||
| self._record_metric(name, value, Unit.Milliseconds, dimensions) | ||
|
|
||
| def publish(self): | ||
| """Calls publish method from the configured metrics publisher to publish metrics""" | ||
| self.metrics_publisher.publish(self.namespace, self.metrics_cache) | ||
| self.metrics_cache = [] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.