Skip to content

Commit

Permalink
Merge pull request #44 from burmanm/feature/43
Browse files Browse the repository at this point in the history
Allow use of datetime or timedelta where approriate, fixes #43
  • Loading branch information
burmanm authored Mar 28, 2017
2 parents 19bc77d + 2cc57e5 commit 86dabf5
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 12 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This repository includes the necessary Python client libraries to access Hawkula

## Introduction

Python client to access Hawkular-Metrics, an abstraction to invoke REST-methods on the server endpoint using urllib2. No external dependencies, works with Python 2.7.x (tested on 2.7.5/2.7.6 and 2.7.10) and Python 3.4.x (tested with the Python 3.4.2, might work with newer versions also)
Python client to access Hawkular-Metrics, an abstraction to invoke REST-methods on the server endpoint using urllib2. No external dependencies, works with Python 2.7.x (tested on 2.7.5/2.7.6 and 2.7.10/2.7.13) and Python 3.4.x / Python 3.5.x (tested with the Python 3.4.2 and Python 3.5.3, might work with newer versions also).

## License and copyright

Expand Down Expand Up @@ -36,7 +36,7 @@ To install, run ``python setup.py install`` if you installed from source code, o

To use hawkular-client-python in your own program, after installation import from hawkular the class HawkularMetricsClient and instantiate it. After this, push dicts with keys id, timestamp and value with put or use assistant method create to send events. pydoc gives the list of allowed parameters for each function.

Timestamps should be in the milliseconds after epoch and numeric values should be float. The client provides a method to request current time in milliseconds, ``time_millis()``
The client provides a method to request current time in milliseconds, ``time_millis()`` that's accepted by the methods, but you can use ``datetime`` and ``timedelta`` to control the time also when sending requests to the Hawkular-Metrics.

See metrics_test.py for more detailed examples and [Hawkular-Metrics documentation](http://www.hawkular.org/docs/components/metrics/index.html) for more detailed explanation of available features.

Expand Down Expand Up @@ -100,8 +100,9 @@ Example pushing a multiple values:

```python
>>> from hawkular.metrics import create_datapoint, create_metric, time_millis
>>> datapoint = create_datapoint(float(4.35), time_millis())
>>> datapoint2 = create_datapoint(float(4.42), time_millis() + 10)
>>> t = datetime.utcnow()
>>> datapoint = create_datapoint(float(4.35), t)
>>> datapoint2 = create_datapoint(float(4.42), t + timedelta(seconds=10))
>>> metric = create_metric(MetricType.Gauge, 'example.doc.1', [datapoint, datapoint2])
>>> client.put(metric)
```
Expand Down
58 changes: 54 additions & 4 deletions hawkular/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import collections
import base64
import ssl
from datetime import datetime, timedelta

try:
import simplejson as json
Expand Down Expand Up @@ -58,6 +59,8 @@ class HawkularMetricsClient(HawkularBaseClient):
"""
Internal methods
"""
epoch = datetime.utcfromtimestamp(0)

def _get_url(self, metric_type=None):
if metric_type is None:
metric_type = MetricType._Metrics
Expand Down Expand Up @@ -139,32 +142,70 @@ def push(self, metric_type, metric_id, value, timestamp=None):
:param metric_type: MetricType to be matched (required)
:param metric_id: Exact string matching metric id
:param value: Datapoint value (depending on the MetricType)
:param timestamp: Timestamp of the datapoint. If left empty, uses current client time.
:param timestamp: Timestamp of the datapoint. If left empty, uses current client time. Can be milliseconds since epoch or datetime instance
"""
if type(timestamp) is datetime:
timestamp = datetime_to_time_millis(timestamp)

item = create_metric(metric_type, metric_id, create_datapoint(value, timestamp))
self.put(item)

def query_metric(self, metric_type, metric_id, **query_options):
def query_metric(self, metric_type, metric_id, start=None, end=None, **query_options):
"""
Query for metrics datapoints from the server.
:param metric_type: MetricType to be matched (required)
:param metric_id: Exact string matching metric id
:param start: Milliseconds since epoch or datetime instance
:param end: Milliseconds since epoch or datetime instance
:param query_options: For possible query_options, see the Hawkular-Metrics documentation.
"""
if start is not None:
if type(start) is datetime:
query_options['start'] = datetime_to_time_millis(start)
else:
query_options['start'] = start

if end is not None:
if type(end) is datetime:
query_options['end'] = datetime_to_time_millis(end)
else:
query_options['end'] = end

return self._get(
self._get_metrics_raw_url(
self._get_metrics_single_url(metric_type, metric_id)),
**query_options)

def query_metric_stats(self, metric_type, metric_id, **query_options):
def query_metric_stats(self, metric_type, metric_id, start=None, end=None, bucketDuration=None, **query_options):
"""
Query for metric aggregates from the server. This is called buckets in the Hawkular-Metrics documentation.
:param metric_type: MetricType to be matched (required)
:param metric_id: Exact string matching metric id
:param start: Milliseconds since epoch or datetime instance
:param end: Milliseconds since epoch or datetime instance
:param bucketDuration: The timedelta or duration of buckets. Can be a string presentation or timedelta object
:param query_options: For possible query_options, see the Hawkular-Metrics documentation.
"""
if start is not None:
if type(start) is datetime:
query_options['start'] = datetime_to_time_millis(start)
else:
query_options['start'] = start

if end is not None:
if type(end) is datetime:
query_options['end'] = datetime_to_time_millis(end)
else:
query_options['end'] = end

if bucketDuration is not None:
if type(bucketDuration) is timedelta:
query_options['bucketDuration'] = timedelta_to_duration(bucketDuration)
else:
query_options['bucketDuration'] = bucketDuration

return self._get(
self._get_metrics_stats_url(
self._get_metrics_single_url(metric_type, metric_id)),
Expand Down Expand Up @@ -312,17 +353,26 @@ def time_millis():
"""
return int(round(time.time() * 1000))

def timedelta_to_duration(td):
return '{}s'.format(int(td.total_seconds()))

def datetime_to_time_millis(dt):
return '{:.0f}'.format((dt - HawkularMetricsClient.epoch).total_seconds() * 1000)

def create_datapoint(value, timestamp=None, **tags):
"""
Creates a single datapoint dict with a value, timestamp and tags.
:param value: Value of the datapoint. Type depends on the id's MetricType
:param timestamp: Optional timestamp of the datapoint. Uses client current time if not set. Millisecond accuracy
:param timestamp: Optional timestamp of the datapoint. Uses client current time if not set. Millisecond accuracy. Can be datetime instance also.
:param tags: Optional datapoint tags. Not to be confused with metric definition tags
"""
if timestamp is None:
timestamp = time_millis()

if type(timestamp) is datetime:
timestamp = datetime_to_time_millis(timestamp)

item = { 'timestamp': timestamp,
'value': value }

Expand Down
24 changes: 20 additions & 4 deletions tests/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from hawkular.metrics import *
import os
import base64
from datetime import datetime, timedelta

try:
import mock
Expand Down Expand Up @@ -196,7 +197,8 @@ def test_add_gauge_single(self):
self.client.push(MetricType.Gauge, 'test.gauge.single.tags', value)

# Fetch results
data = self.client.query_metric(MetricType.Gauge, 'test.gauge.single.tags')
now = datetime.utcnow()
data = self.client.query_metric(MetricType.Gauge, 'test.gauge.single.tags', start=now-timedelta(minutes = 1), end=now)
self.assertEqual(value, float(data[0]['value']))

def test_add_availability_single(self):
Expand Down Expand Up @@ -262,9 +264,9 @@ def test_add_mixed_metrics_and_datapoints(self):

def test_query_options(self):
# Create metric with two values
t = time_millis()
t = datetime.utcnow()
v1 = create_datapoint(float(1.45), t)
v2 = create_datapoint(float(2.00), (t - 2000))
v2 = create_datapoint(float(2.00), (t - timedelta(seconds=2)))

m = create_metric(MetricType.Gauge, 'test.query.gauge.1', [v1, v2])
self.client.put(m)
Expand All @@ -274,7 +276,7 @@ def test_query_options(self):
self.assertEqual(2, len(d))

# Query for data which has start time limitation
d = self.client.query_metric(MetricType.Gauge, 'test.query.gauge.1', start=(t - 1000))
d = self.client.query_metric(MetricType.Gauge, 'test.query.gauge.1', start=(t - timedelta(seconds=1)))
self.assertEqual(1, len(d))

def test_stats_queries(self):
Expand All @@ -299,6 +301,11 @@ def test_stats_queries(self):
self.assertEqual(10, bp[0]['samples'])
self.assertEqual(2, len(bp[0]['percentiles']))

now = datetime.utcfromtimestamp(t/1000)

bp = self.client.query_metric_stats(MetricType.Gauge, 'test.buckets.1', bucketDuration=timedelta(seconds=2), start=now-timedelta(seconds=10), end=now, distinct=True)
self.assertEqual(5, len(bp), "Single bucket is two seconds")

def test_tenant_changing(self):
self.client.create_metric_definition(MetricType.Availability, 'test.tenant.avail.1')
# Fetch metrics and check that it did appear
Expand Down Expand Up @@ -337,6 +344,15 @@ def test_get_metrics_stats_url(self):
url = self.client._get_metrics_stats_url('some.key')
self.assertEqual('some.key/stats', url)

def test_timedelta_to_duration_string(self):
s = timedelta_to_duration(timedelta(hours=1, minutes=3, seconds=4))
self.assertEqual('3784s', s)

s = timedelta_to_duration(timedelta(hours=1, seconds=4))
self.assertEqual('3604s', s)

s = timedelta_to_duration(timedelta(days=4))
self.assertEqual('345600s', s)

if __name__ == '__main__':
unittest.main()

0 comments on commit 86dabf5

Please sign in to comment.