Skip to content

Commit

Permalink
Merge pull request #423 from /issues/418
Browse files Browse the repository at this point in the history
Fixes #418 - Metrics Provider
  • Loading branch information
jantman committed Aug 30, 2019
2 parents aad5ba6 + 3e022f9 commit 7f7aade
Show file tree
Hide file tree
Showing 25 changed files with 1,364 additions and 56 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ What It Does
an optional maximum time limit). See
`Getting Started - Trusted Advisor <http://awslimitchecker.readthedocs.io/en/latest/getting_started.html#trusted-advisor>`_
for more information.
- Optionally send current usage and limit metrics to a metrics store, such as Datadog.

Requirements
------------
Expand Down
12 changes: 12 additions & 0 deletions awslimitchecker/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,15 @@ def get_required_iam_policy(self):
}],
}
return policy

@property
def region_name(self):
"""
Return the name of the AWS region that we're checking.
:return: AWS region name
:rtype: str
"""
kwargs = self._boto_conn_kwargs
conn = boto3.client('ec2', **kwargs)
return conn._client_config.region_name
42 changes: 42 additions & 0 deletions awslimitchecker/metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
awslimitchecker/metrics/__init__.py
The latest version of this package is available at:
<https://github.com/jantman/awslimitchecker>
################################################################################
Copyright 2015-2019 Jason Antman <jason@jasonantman.com>
This file is part of awslimitchecker, also known as awslimitchecker.
awslimitchecker is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
awslimitchecker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with awslimitchecker. If not, see <http://www.gnu.org/licenses/>.
The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/awslimitchecker> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################
AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
"""

from .base import MetricsProvider
from .dummy import Dummy
from .datadog import Datadog
121 changes: 121 additions & 0 deletions awslimitchecker/metrics/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
awslimitchecker/metrics/base.py
The latest version of this package is available at:
<https://github.com/jantman/awslimitchecker>
################################################################################
Copyright 2015-2019 Jason Antman <jason@jasonantman.com>
This file is part of awslimitchecker, also known as awslimitchecker.
awslimitchecker is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
awslimitchecker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with awslimitchecker. If not, see <http://www.gnu.org/licenses/>.
The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/awslimitchecker> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################
AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
"""

import logging
from abc import ABCMeta, abstractmethod

logger = logging.getLogger(__name__)


class MetricsProvider(object):

__metaclass__ = ABCMeta

def __init__(self, region_name):
"""
Initialize a MetricsProvider class. This MUST be overridden by
subclasses. All configuration must be passed as keyword arguments
to the class constructor (these come from ``--metrics-config`` CLI
arguments). Any dependency imports must be made in the constructor.
The constructor should do as much as possible to validate configuration.
:param region_name: the name of the region we're connected to
:type region_name: str
"""
self._region_name = region_name
self._duration = 0.0
self._limits = []

def set_run_duration(self, duration):
"""
Set the duration for the awslimitchecker run (the time taken to check
usage against limits).
:param duration: time taken to check limits
:type duration: float
"""
self._duration = duration

def add_limit(self, limit):
"""
Cache a given limit for later sending to the metrics store.
:param limit: a limit to cache
:type limit: AwsLimit
"""
self._limits.append(limit)

@abstractmethod
def flush(self):
"""
Flush all metrics to the provider. This is the method that actually
sends data to your metrics provider/store. It should iterate over
``self._limits`` and send metrics for them, as well as for
``self._duration``.
"""
raise NotImplementedError()

@staticmethod
def providers_by_name():
"""
Return a dict of available MetricsProvider subclass names to the class
objects.
:return: MetricsProvider class names to classes
:rtype: dict
"""
return {x.__name__: x for x in MetricsProvider.__subclasses__()}

@staticmethod
def get_provider_by_name(name):
"""
Get a reference to the provider class with the specified name.
:param name: name of the MetricsProvider subclass
:type name: str
:return: MetricsProvider subclass
:rtype: ``class``
:raises: RuntimeError
"""
try:
return MetricsProvider.providers_by_name()[name]
except KeyError:
raise RuntimeError(
'ERROR: "%s" is not a valid MetricsProvider class name' % name
)
169 changes: 169 additions & 0 deletions awslimitchecker/metrics/datadog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""
awslimitchecker/metrics/datadog.py
The latest version of this package is available at:
<https://github.com/jantman/awslimitchecker>
################################################################################
Copyright 2015-2018 Jason Antman <jason@jasonantman.com>
This file is part of awslimitchecker, also known as awslimitchecker.
awslimitchecker is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
awslimitchecker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with awslimitchecker. If not, see <http://www.gnu.org/licenses/>.
The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/awslimitchecker> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################
AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
"""

import os
import logging
import urllib3
import time
import re
import json
from awslimitchecker.metrics.base import MetricsProvider

logger = logging.getLogger(__name__)


class Datadog(MetricsProvider):
"""Send metrics to Datadog."""

def __init__(
self, region_name, prefix='awslimitchecker.', api_key=None,
extra_tags=None
):
"""
Initialize the Datadog metrics provider. This class does not have any
additional requirements. You must specify at least the ``api_key``
configuration option.
:param region_name: the name of the region we're connected to. This
parameter is automatically passed in by the Runner class.
:type region_name: str
:param prefix: Datadog metric prefix
:type prefix: str
:param api_key: Datadog API key. May alternatively be specified by the
``DATADOG_API_KEY`` environment variable.
:type api_key: str
:param extra_tags: CSV list of additional tags to send with metrics.
All metrics will automatically be tagged with ``region:<region name>``
:type extra_tags: str
"""
super(Datadog, self).__init__(region_name)
self._prefix = prefix
self._tags = ['region:%s' % region_name]
if extra_tags is not None:
self._tags.extend(extra_tags.split(','))
self._api_key = os.environ.get('DATADOG_API_KEY')
if api_key is not None:
self._api_key = api_key
if self._api_key is None:
raise RuntimeError(
'ERROR: Datadog metrics provider requires datadog API key.'
)
self._http = urllib3.PoolManager()
self._validate_auth(self._api_key)

def _validate_auth(self, api_key):
url = 'https://api.datadoghq.com/api/v1/validate?api_key=%s'
logger.debug('Validating Datadog API key: GET %s', url)
url = url % api_key
r = self._http.request('GET', url)
if r.status != 200:
raise RuntimeError(
'ERROR: Datadog API key validation failed with HTTP %s: %s' % (
r.status, r.data
)
)

def _name_for_metric(self, service, limit):
"""
Return a metric name that's safe for datadog
:param service: service name
:type service: str
:param limit: limit name
:type limit: str
:return: datadog metric name
:rtype: str
"""
return ('%s%s.%s' % (
self._prefix,
re.sub(r'[^0-9a-zA-Z]+', '_', service),
re.sub(r'[^0-9a-zA-Z]+', '_', limit)
)).lower()

def flush(self):
ts = int(time.time())
logger.debug('Flushing metrics to Datadog.')
series = [{
'metric': '%sruntime' % self._prefix,
'points': [[ts, self._duration]],
'type': 'gauge',
'tags': self._tags
}]
for lim in self._limits:
u = lim.get_current_usage()
if len(u) == 0:
max_usage = 0
else:
max_usage = max(u).get_value()
mname = self._name_for_metric(lim.service.service_name, lim.name)
series.append({
'metric': '%s.max_usage' % mname,
'points': [[ts, max_usage]],
'type': 'gauge',
'tags': self._tags
})
limit = lim.get_limit()
if limit is not None:
series.append({
'metric': '%s.limit' % mname,
'points': [[ts, limit]],
'type': 'gauge',
'tags': self._tags
})
logger.info('POSTing %d metrics to datadog', len(series))
data = {'series': series}
encoded = json.dumps(data).encode('utf-8')
url = 'https://api.datadoghq.com/api/v1/series' \
'?api_key=%s' % self._api_key
resp = self._http.request(
'POST', url,
headers={'Content-type': 'application/json'},
body=encoded
)
if resp.status < 300:
logger.debug(
'Successfully POSTed to Datadog; HTTP %d: %s',
resp.status, resp.data
)
return
raise RuntimeError(
'ERROR sending metrics to Datadog; API responded HTTP %d: %s' % (
resp.status, resp.data
)
)

0 comments on commit 7f7aade

Please sign in to comment.