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
81 changes: 49 additions & 32 deletions hawkular/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,33 @@
import urllib2
import urllib
import time
import collections

"""
TODO: Remember to do imports for Python 3 also and check the compatibility..
TODO: Search datapoints with tags.. tag datapoints.
TODO: Allow changing instance's tenant?
TODO: Authentication when it's done..
TODO: Remove HawkularMetricsConnectionError and use HawkularMetricsError only?
TODO: 0.3.3 will change /hawkular-metrics to hawkular/metrics and the REST-interfaces
TODO: 0.3.3 will before release move the tenantId to headers..
"""

class MetricType:
Numeric = 'numeric'
Gauge = 'gauges'
Availability = 'availability'
Counter = 'counters'

@staticmethod
def short(metric_type):
if metric_type is 'numeric':
return 'num'
if metric_type is MetricType.Gauge:
return 'gauge'
else:
return 'avail'
return 'availability'

class Availability:
Down = 'down'
Up = 'up'
Unknown = 'unknown'

class HawkularMetricsError(urllib2.HTTPError):
pass
Expand Down Expand Up @@ -53,8 +56,8 @@ class HawkularMetricsClient:
def __init__(self,
tenant_id,
host='localhost',
port=8081,
path='hawkular-metrics'):
port=8080,
path='hawkular/metrics'):
"""
A new instance of HawkularMetricsClient is created with the following defaults:

Expand Down Expand Up @@ -84,14 +87,11 @@ def _clean_metric_id(metric_id):
def _get_base_url(self):
return "http://{0}:{1}/{2}/".format(self.host, str(self.port), self.path)

def _get_url(self, service):
return self._get_base_url() + '{0}/{1}'.format(self.tenant_id, service)

def _get_metrics_url(self, metric_type):
return self._get_url('metrics') + "/{0}".format(metric_type)
def _get_url(self, metric_type):
return self._get_base_url() + '{0}/{1}'.format(self.tenant_id, metric_type)

def _get_metrics_single_url(self, metric_type, metric_id):
return self._get_metrics_url(metric_type) + '/{0}'.format(self._clean_metric_id(metric_id))
return self._get_url(metric_type) + '/{0}'.format(self._clean_metric_id(metric_id))

def _get_metrics_data_url(self, metrics_url):
return metrics_url + '/data'
Expand Down Expand Up @@ -185,17 +185,27 @@ def _isfloat(value):
Instance methods
"""

def put(self, metric_type, data):
def put(self, data):
"""
Send multiple different metric_ids to the server in a single
batch.
Send multiple different metric_ids to the server in a single batch. Metrics can be a mixture
of types.

data is a dict or a list of dicts created with create_metric(metric_id, metric_dict)
data is a dict or a list of dicts created with create_metric(metric_type, metric_id, datapoints)
"""
if not isinstance(data, list):
data = [data]

self._post(self._get_metrics_data_url(self._get_metrics_url(metric_type)), data)

r = collections.defaultdict(list)

for d in data:
metric_type = d.pop('type', None)
if metric_type is None:
raise HawkularMetricsError('Undefined MetricType')
r[metric_type].append(d)

# This isn't transactional, but .. ouh well. One can always repost everything.
for l in r:
self._post(self._get_metrics_data_url(self._get_url(l)), r[l])

def push(self, metric_type, metric_id, value, timestamp=None, **tags):
"""
Expand All @@ -204,8 +214,8 @@ def push(self, metric_type, metric_id, value, timestamp=None, **tags):
This method is an assistant method for the put method by removing the need to
create data structures first.
"""
item = create_metric(metric_id, create_datapoint(value, timestamp, **tags))
self.put(metric_type, item)
item = create_metric(metric_type, metric_id, create_datapoint(value, timestamp, **tags))
self.put(item)

def query_metric(self, metric_type, metric_id, **search_options):
"""
Expand All @@ -224,7 +234,7 @@ def query_single_numeric(self, metric_id, **search_options):
"""
See query_metric
"""
return self.query_metric(MetricType.Numeric, metric_id, **search_options)
return self.query_metric(MetricType.Gauge, metric_id, **search_options)

def query_single_availability(self, metric_id, **search_options):
"""
Expand All @@ -234,12 +244,12 @@ def query_single_availability(self, metric_id, **search_options):

def query_definitions(self, query_type):
"""
Query available metric definitions. Use 'avail' or 'num' or MetricType.Availability / MetricType.Numeric
Query available metric definitions.
"""
if isinstance(query_type, MetricType):
query_type = MetricType.short(query_type)
# if isinstance(query_type, MetricType):
# query_type = MetricType.short(query_type)

definition_url = self._get_url('metrics') + '?type=' + MetricType.short(query_type)
definition_url = self._get_url('metrics') + '?type=' + query_type
return self._get(definition_url)

def create_metric_definition(self, metric_type, metric_id, **tags):
Expand All @@ -248,7 +258,7 @@ def create_metric_definition(self, metric_type, metric_id, **tags):
units, env ..

Use methods create_numeric_definition and create_availability_definition to avoid using
MetricType.Numeric / MetricType.Availability
MetricType.Gauge / MetricType.Availability
"""
item = { 'id': metric_id }
if len(tags) > 0:
Expand All @@ -261,19 +271,26 @@ def create_metric_definition(self, metric_type, metric_id, **tags):
item['tags'] = tags

json_data = json.dumps(item, indent=2)
self._post(self._get_metrics_url(metric_type), json_data)
try:
self._post(self._get_url(metric_type), json_data)
except HawkularMetricsError as e:
if e.code == 409:
return False
raise e

return True

def create_numeric_definition(self, metric_id, **tags):
"""
See create_metric_definition
"""
self.create_metric_definition(MetricType.Numeric, metric_id, **tags)
return self.create_metric_definition(MetricType.Gauge, metric_id, **tags)

def create_availability_definition(self, metric_id, **tags):
"""
See create_metric_definition
"""
self.create_metric_definition(MetricType.Availability, metric_id, **tags)
return self.create_metric_definition(MetricType.Availability, metric_id, **tags)

def query_metric_tags(self, metric_type, metric_id):
"""
Expand Down Expand Up @@ -348,12 +365,12 @@ def create_datapoint(value, timestamp=None, **tags):

return item

def create_metric(metric_id, data):
def create_metric(metric_type, metric_id, data):
"""
Create Hawkular-Metrics' submittable structure, data is a datapoint or list of datapoints
"""
if not isinstance(data, list):
data = [data]

return { 'id': metric_id, 'data': data }
return { 'type': metric_type,'id': metric_id, 'data': data }

82 changes: 39 additions & 43 deletions hawkular/metrics_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import unittest
import uuid
# import metrics
# from metrics import Availability, MetricType, HawkularMetricsError
from metrics import *

class TestMetricFunctionsBase(unittest.TestCase):
Expand Down Expand Up @@ -48,12 +46,15 @@ def test_numeric_creation(self):
Test creating numeric metric definitions with different tags and definition.
"""
# Create numeric metrics with empty details and added details
self.client.create_numeric_definition('test.create.numeric.1')
self.client.create_numeric_definition('test.create.numeric.2', dataRetention=90)
self.client.create_numeric_definition('test.create.numeric.3', dataRetention=90, units='bytes', env='qa')
md1 = self.client.create_numeric_definition('test.create.numeric.1')
md2 = self.client.create_numeric_definition('test.create.numeric.2', dataRetention=90)
md3 = self.client.create_numeric_definition('test.create.numeric.3', dataRetention=90, units='bytes', env='qa')
self.assertTrue(md1)
self.assertTrue(md2)
self.assertTrue(md3)

# Fetch metrics definition and check that the ones we created appeared also
m = self.client.query_definitions(MetricType.Numeric)
m = self.client.query_definitions(MetricType.Gauge)
self.assertEqual(3, len(m))
self.assertEqual(self.test_tenant, m[0]['tenantId'])
self.assertEqual('bytes', m[2]['tags']['units'])
Expand All @@ -69,13 +70,8 @@ def test_numeric_creation(self):
self.assertEqual(m, expect) # Did it?

# Lets try creating a duplicate metric
try:
self.client.create_numeric_definition('test.create.numeric.1')
self.fail('Should have received an exception, metric with the same name was already created')
except HawkularMetricsError, e:
# Check return code 400 and that the failure message was correctly parsed
self.assertEqual(409, e.code)
self.assertEqual('A metric with name [test.create.numeric.1] already exists', e.msg)
md4 = self.client.create_numeric_definition('test.create.numeric.1')
self.assertFalse(md4, 'Should have received an exception, metric with the same name was already created')

def test_availability_creation(self):
# Create availability metric
Expand All @@ -92,19 +88,19 @@ def test_tags_modifications(self):
m = 'test.create.tags.1'
# Create metric without tags
self.client.create_numeric_definition(m)
e = self.client.query_metric_tags(MetricType.Numeric, m)
e = self.client.query_metric_tags(MetricType.Gauge, m)
self.assertIsNotNone(e)
self.assertEqual({}, e)
# Add tags
self.client.update_metric_tags(MetricType.Numeric, m, hostname='machine1', a='b')
self.client.update_metric_tags(MetricType.Gauge, m, hostname='machine1', a='b')
# Fetch metric - check for tags
tags = self.client.query_metric_tags(MetricType.Numeric, m)
tags = self.client.query_metric_tags(MetricType.Gauge, m)
self.assertEqual(2, len(tags))
self.assertEqual("b", tags['a'])
# Delete some metric tags
self.client.delete_metric_tags(MetricType.Numeric, m, a='b', hostname='machine1')
self.client.delete_metric_tags(MetricType.Gauge, m, a='b', hostname='machine1')
# Fetch metric - check that tags were deleted
tags_2 = self.client.query_metric_tags(MetricType.Numeric, m)
tags_2 = self.client.query_metric_tags(MetricType.Gauge, m)
self.assertEqual(0, len(tags_2))

# def test_tags_behavior(self):
Expand All @@ -120,19 +116,18 @@ def test_tags_modifications(self):
# print 'END: TEST TAGS'

def test_add_numeric_single(self):

# Normal way
value = float(4.35)
datapoint = create_datapoint(value, time_millis())
metric = create_metric('test.numeric./', datapoint)
self.client.put(MetricType.Numeric, metric)
metric = create_metric(MetricType.Gauge, 'test.numeric./', datapoint)
self.client.put(metric)

# Fetch results
data = self.client.query_single_numeric('test.numeric./')
self.assertEqual(float(data[0]['value']), value)

# Shortcut method with tags
self.client.push(MetricType.Numeric, 'test.numeric.single.tags', value, hostname='localhost')
self.client.push(MetricType.Gauge, 'test.numeric.single.tags', value, hostname='localhost')

# Fetch results
data = self.client.query_single_numeric('test.numeric.single.tags')
Expand All @@ -153,41 +148,42 @@ def test_add_numeric_multi_datapoint(self):
metric_1v = create_datapoint(float(1.45))
metric_2v = create_datapoint(float(2.00), (time_millis() - 2000))

metric = create_metric('test.numeric.multi', [metric_1v, metric_2v])
self.client.put(MetricType.Numeric, metric)
metric = create_metric(MetricType.Gauge, 'test.numeric.multi', [metric_1v, metric_2v])
self.client.put(metric)

data = self.client.query_single_numeric('test.numeric.multi')
self.assertEqual(len(data), 2)
self.assertEqual(data[0]['value'], float(1.45))
self.assertEqual(data[1]['value'], float(2.00))

def test_add_availability_multi_datapoint(self):
up = create_datapoint('up', (time_millis() - 2000))
down = create_datapoint('down')
t = time_millis()
up = create_datapoint('up', (t - 2000))
down = create_datapoint('down', t)

m = create_metric('test.avail.multi', [up, down])
m = create_metric(MetricType.Availability, 'test.avail.multi', [up, down])

self.client.put(MetricType.Availability, m)
self.client.put(m)
data = self.client.query_single_availability('test.avail.multi')

self.assertEqual(len(data), 2)
self.assertEqual(data[0]['value'], 'down')
self.assertEqual(data[1]['value'], 'up')
self.assertEqual(data[0]['value'], 'up')
self.assertEqual(data[1]['value'], 'down')

def test_add_multi_metrics_and_datapoints(self):
def test_add_mixed_metrics_and_datapoints(self):
metric1 = create_datapoint(float(1.45))
metric1_2 = create_datapoint(float(2.00), (time_millis() - 2000))

metric_multi = create_metric('test.multi.numeric.1', [metric1, metric1_2])
metric_multi = create_metric(MetricType.Gauge, 'test.multi.numeric.1', [metric1, metric1_2])

metric2 = create_datapoint(float(1.55))
metric2_multi = create_metric('test.multi.numeric.2', [metric2])
metric2 = create_datapoint(Availability.Up)
metric2_multi = create_metric(MetricType.Availability,'test.multi.numeric.2', [metric2])

self.client.put(MetricType.Numeric, [metric_multi, metric2_multi])
self.client.put([metric_multi, metric2_multi])

# Check that both were added correctly..
metric1_data = self.client.query_single_numeric('test.multi.numeric.1')
metric2_data = self.client.query_single_numeric('test.multi.numeric.2')
metric2_data = self.client.query_single_availability('test.multi.numeric.2')

self.assertEqual(2, len(metric1_data))
self.assertEqual(1, len(metric2_data))
Expand All @@ -198,29 +194,29 @@ def test_query_options(self):
v1 = create_datapoint(float(1.45), t)
v2 = create_datapoint(float(2.00), (t - 2000))

m = create_metric('test.query.numeric.1', [v1, v2])
self.client.put(MetricType.Numeric, m)
m = create_metric(MetricType.Gauge, 'test.query.numeric.1', [v1, v2])
self.client.put(m)

# Query first without limitations
d = self.client.query_metric(MetricType.Numeric, 'test.query.numeric.1')
d = self.client.query_metric(MetricType.Gauge, 'test.query.numeric.1')
self.assertEqual(2, len(d))

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

# This feature isn't really ready for prime time in Hawkular-Metrics yet..
# def test_tags_finding(self):
# # Create metrics with tags
# m = 'test.create.data.tags.1'
# self.client.create_metric_definition(MetricType.Numeric, m, ab='cd')
# self.client.create_metric_definition(MetricType.Gauge, m, ab='cd')
# # Push some data to them
# t = time_millis()
# v = float(1.4)
# self.client.push(MetricType.Numeric, m, v, t)
# self.client.push(MetricType.Gauge, m, v, t)
# # Fetch data with certain tags
# expected = { 'id': m, 'timestamp': t, 'value': v }
# d = self.client.query_data_with_tags(MetricType.Numeric, ab='cd')
# d = self.client.query_data_with_tags(MetricType.Gauge, ab='cd')
# self.assertIsNotNone(d)
# self.assertIn(expected, d)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from distutils.core import setup

setup(name='hawkular-client-python',
version='0.3.2',
version='0.3.3',
description='Python client to communicate with Hawkular over HTTP',
author='Michael Burman',
author_email='miburman@redhat.com',
Expand Down