diff --git a/datadog/api/base.py b/datadog/api/base.py index 9abe00b26..74f067743 100644 --- a/datadog/api/base.py +++ b/datadog/api/base.py @@ -6,7 +6,7 @@ # datadog from datadog.api.exceptions import ClientError, ApiError, HttpBackoff, \ HttpTimeout, ApiNotInitialized -from datadog.api import _api_version, _timeout, _max_timeouts, _backoff_period +from datadog.api import _api_version, _max_timeouts, _backoff_period from datadog.util.compat import json, is_p3k log = logging.getLogger('dd.datadogpy') @@ -22,7 +22,6 @@ class HTTPClient(object): _backoff_timestamp = None _timeout_counter = 0 _api_version = _api_version - _timeout = _timeout @classmethod def request(cls, method, path, body=None, attach_host_name=False, response_formatter=None, @@ -62,7 +61,7 @@ def request(cls, method, path, body=None, attach_host_name=False, response_forma # Import API, User and HTTP settings from datadog.api import _api_key, _application_key, _api_host, \ - _swallow, _host_name, _proxies, _max_retries + _swallow, _host_name, _proxies, _max_retries, _timeout # Check keys and add then to params if _api_key is None: @@ -112,7 +111,7 @@ def request(cls, method, path, body=None, attach_host_name=False, response_forma headers=headers, params=params, data=body, - timeout=cls._timeout, + timeout=_timeout, proxies=_proxies) result.raise_for_status() @@ -120,7 +119,7 @@ def request(cls, method, path, body=None, attach_host_name=False, response_forma raise ClientError("Could not request %s %s%s: %s" % (method, _api_host, url, e)) except requests.exceptions.Timeout as e: cls._timeout_counter += 1 - raise HttpTimeout('%s %s timed out after %d seconds.' % (method, url, cls._timeout)) + raise HttpTimeout('%s %s timed out after %d seconds.' % (method, url, _timeout)) except requests.exceptions.HTTPError as e: if e.response.status_code == 404 or e.response.status_code == 400: pass diff --git a/datadog/api/graphs.py b/datadog/api/graphs.py index 4f3759d5d..60746fb4d 100644 --- a/datadog/api/graphs.py +++ b/datadog/api/graphs.py @@ -16,10 +16,10 @@ def create(cls, **params): :param metric_query: metric query :type metric_query: string query - :param start: timestamp of the start of the query. + :param start: query start timestamp :type start: POSIX timestamp - :param end: timestamp of the end of the query. + :param end: query end timestamp :type end: POSIX timestamp :param event_query: a query that will add event bands to the graph diff --git a/datadog/api/metrics.py b/datadog/api/metrics.py index a821135cf..49135e99f 100644 --- a/datadog/api/metrics.py +++ b/datadog/api/metrics.py @@ -1,16 +1,19 @@ import time -from datadog.api.base import SendableAPIResource +from datadog.api.base import SearchableAPIResource, SendableAPIResource +from datadog.api.exceptions import ApiError -class Metric(SendableAPIResource): - +class Metric(SearchableAPIResource, SendableAPIResource): """ - A wrapper around Metric HTTP API. + A wrapper around Metric HTTP API """ - _class_url = '/series' + _class_url = None _json_name = 'series' + _METRIC_QUERY_ENDPOINT = '/query' + _METRIC_SUBMIT_ENDPOINT = '/series' + @classmethod def _process_points(cls, points): now = time.time() @@ -42,6 +45,9 @@ def send(cls, *metrics, **single_metric): :returns: JSON response from HTTP request """ + # Set the right endpoint + cls._class_url = cls._METRIC_SUBMIT_ENDPOINT + try: if metrics: for metric in metrics: @@ -57,3 +63,39 @@ def send(cls, *metrics, **single_metric): raise KeyError("'points' parameter is required") return super(Metric, cls).send(attach_host_name=True, **metrics_dict) + + @classmethod + def query(cls, **params): + """ + Query metrics from Datadog + + :param start: query start timestamp + :type start: POSIX timestamp + + :param end: query end timestamp + :type end: POSIX timestamp + + :param query: metric query + :type query: string query + + :return: JSON response from HTTP request + + *start* and *end* should be less than 24 hours apart. + It is *not* meant to retrieve metric data in bulk. + + >>> api.Metric.query(start=int(time.time()) - 3600, end=int(time.time()), + query='avg:system.cpu.idle{*}') + """ + # Set the right endpoint + cls._class_url = cls._METRIC_QUERY_ENDPOINT + + # `from` is a reserved keyword in Python, therefore + # `api.Metric.query(from=...)` is not permited + # -> map `start` to `from` and `end` to `to` + try: + params['from'] = params.pop('start') + params['to'] = params.pop('end') + except KeyError as e: + raise ApiError("The parameter '{0}' is required".format(e.args[0])) + + return super(Metric, cls)._search(**params) diff --git a/tests/integration/api/test_api.py b/tests/integration/api/test_api.py index 61f34f9a7..98f7cc91a 100644 --- a/tests/integration/api/test_api.py +++ b/tests/integration/api/test_api.py @@ -11,10 +11,10 @@ from nose.tools import assert_true as ok # datadog -import datadog.util.compat.json as json from datadog import initialize from datadog import api as dog from datadog.api.exceptions import ApiError +from datadog.util.compat import json from tests.util.snapshot_test_utils import ( assert_snap_not_blank, assert_snap_has_no_events ) @@ -311,17 +311,23 @@ def test_search(self): assert len(results['results']['hosts']) > 0 assert len(results['results']['metrics']) > 0 + @attr("metric") def test_metrics(self): now = datetime.datetime.now() now_ts = int(time.mktime(now.timetuple())) + metric_name = "test.metric." + str(now_ts) + host_name = "test.host." + str(now_ts) - dog.Metric.send(metric='test.metric.' + str(now_ts), - points=1, host="test.host." + str(now_ts)) + dog.Metric.send(metric=metric_name, points=1, host=host_name) time.sleep(self.wait_time) - results = dog.Infrastructure.search(q='metrics:test.metric.' + str(now_ts)) + metric_query = dog.Metric.query(start=now_ts - 3600, end=now_ts + 3600, + query="avg:%s{host:%s}" % (metric_name, host_name)) + assert len(metric_query['series']) == 1, metric_query + + # results = dog.Infrastructure.search(q='metrics:test.metric.' + str(now_ts)) # TODO mattp: cache issue. move this test to server side. - # assert len(results['metrics']) == 1, results + # assert len(results['results']['metrics']) == 1, results matt_series = [ (int(time.mktime((now - datetime.timedelta(minutes=25)).timetuple())), 5), diff --git a/tests/unit/api/helper.py b/tests/unit/api/helper.py index fcec16324..610a81578 100644 --- a/tests/unit/api/helper.py +++ b/tests/unit/api/helper.py @@ -5,7 +5,7 @@ from datadog import initialize, api from datadog.api.base import CreateableAPIResource, UpdatableAPIResource, DeletableAPIResource,\ GetableAPIResource, ListableAPIResource, ActionAPIResource -from datadog.util.compat import is_p3k, json +from datadog.util.compat import iteritems, json # 3p import requests @@ -81,14 +81,9 @@ def request_called_with(self, method, url, data=None, params=None): if params: assert 'params' in others - if is_p3k(): - for (k, v) in params.items(): - assert k in others['params'], others['params'] - assert v == others['params'][k] - else: - for (k, v) in params.iteritems(): - assert k in others['params'], others['params'] - assert v == others['params'][k] + for (k, v) in iteritems(params): + assert k in others['params'], others['params'] + assert v == others['params'][k] def tearDown(self): self.request_patcher.stop() diff --git a/tests/unit/api/test_api.py b/tests/unit/api/test_api.py index a1cf6d935..8e9618c83 100644 --- a/tests/unit/api/test_api.py +++ b/tests/unit/api/test_api.py @@ -9,8 +9,9 @@ # datadog from datadog import initialize, api -from datadog.util.compat import is_p3k +from datadog.api import Metric from datadog.api.exceptions import ApiNotInitialized +from datadog.util.compat import is_p3k from tests.unit.api.helper import ( DatadogAPIWithInitialization, DatadogAPINoInitialization, @@ -192,3 +193,15 @@ def test_actionable(self): MyActionable.trigger_action('POST', "actionname", id=actionable_object_id, mydata="val") self.request_called_with('POST', "host/api/v1/actionname/" + str(actionable_object_id), data={'mydata': "val"}) + + def test_metric_submit_query_switch(self): + """ + Specific to Metric subpackages: endpoints are different for submission and queries + """ + Metric.send(points="val") + self.request_called_with('POST', "host/api/v1/series", + data={'series': [{'points': "val", 'host': api._host_name}]}) + + Metric.query(start="val1", end="val2") + self.request_called_with('GET', "host/api/v1/query", + params={'from': "val1", 'to': "val2"})