Skip to content

Commit

Permalink
Merge pull request #45 from DataDog/yann/query-endpoint
Browse files Browse the repository at this point in the history
metric query API support
  • Loading branch information
yannmh committed May 18, 2015
2 parents 5806124 + 51f755e commit 9225a63
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 27 deletions.
9 changes: 4 additions & 5 deletions datadog/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -112,15 +111,15 @@ 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()
except requests.ConnectionError as e:
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
Expand Down
4 changes: 2 additions & 2 deletions datadog/api/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 47 additions & 5 deletions datadog/api/metrics.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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)
16 changes: 11 additions & 5 deletions tests/integration/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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),
Expand Down
13 changes: 4 additions & 9 deletions tests/unit/api/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
15 changes: 14 additions & 1 deletion tests/unit/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"})

0 comments on commit 9225a63

Please sign in to comment.