Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ init:
test:
pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/*

test-cov-report:
pytest --cov samtranslator --cov-report term-missing --cov-report html --cov-fail-under 95 tests/*

integ-test:
pytest --no-cov integration/*

Expand Down
Empty file.
84 changes: 84 additions & 0 deletions integration/metrics/test_metrics_integration.py
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.
158 changes: 158 additions & 0 deletions samtranslator/metrics/metrics.py
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 = []
5 changes: 3 additions & 2 deletions samtranslator/translator/translator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import copy
from samtranslator.metrics.metrics import DummyMetricsPublisher, Metrics

from samtranslator.feature_toggle.feature_toggle import (
FeatureToggle,
FeatureToggleLocalConfigProvider,
FeatureToggleDefaultConfigProvider,
)
from samtranslator.model import ResourceTypeResolver, sam_resources
Expand Down Expand Up @@ -32,7 +32,7 @@
class Translator:
"""Translates SAM templates into CloudFormation templates"""

def __init__(self, managed_policy_map, sam_parser, plugins=None, boto_session=None):
def __init__(self, managed_policy_map, sam_parser, plugins=None, boto_session=None, metrics=None):
"""
:param dict managed_policy_map: Map of managed policy names to the ARNs
:param sam_parser: Instance of a SAM Parser
Expand All @@ -44,6 +44,7 @@ def __init__(self, managed_policy_map, sam_parser, plugins=None, boto_session=No
self.sam_parser = sam_parser
self.feature_toggle = None
self.boto_session = boto_session
self.metrics = metrics if metrics else Metrics("ServerlessTransform", DummyMetricsPublisher())

if self.boto_session:
ArnGenerator.BOTO_SESSION_REGION_NAME = self.boto_session.region_name
Expand Down
Loading