diff --git a/hawkular/metrics.py b/hawkular/metrics.py index 976139f..9669290 100644 --- a/hawkular/metrics.py +++ b/hawkular/metrics.py @@ -2,6 +2,7 @@ import urllib2 import urllib import time +import collections """ TODO: Remember to do imports for Python 3 also and check the compatibility.. @@ -9,23 +10,25 @@ 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 @@ -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: @@ -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' @@ -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): """ @@ -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): """ @@ -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): """ @@ -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): @@ -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: @@ -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): """ @@ -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 } diff --git a/hawkular/metrics_test.py b/hawkular/metrics_test.py index 632b912..306435d 100644 --- a/hawkular/metrics_test.py +++ b/hawkular/metrics_test.py @@ -1,7 +1,5 @@ import unittest import uuid -# import metrics -# from metrics import Availability, MetricType, HawkularMetricsError from metrics import * class TestMetricFunctionsBase(unittest.TestCase): @@ -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']) @@ -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 @@ -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): @@ -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') @@ -153,8 +148,8 @@ 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) @@ -162,32 +157,33 @@ def test_add_numeric_multi_datapoint(self): 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)) @@ -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) diff --git a/setup.py b/setup.py index e711fd7..50f678c 100644 --- a/setup.py +++ b/setup.py @@ -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',