From 4c1ff8d9243d062fdc7c494072f44bbf12eafd63 Mon Sep 17 00:00:00 2001 From: Jason Derrett Date: Wed, 6 Sep 2017 13:17:41 -0500 Subject: [PATCH 1/4] Add get_measurements() and get_composite_tagged() --- librato/__init__.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/librato/__init__.py b/librato/__init__.py index b92c206..4f4e8bb 100644 --- a/librato/__init__.py +++ b/librato/__init__.py @@ -272,7 +272,7 @@ def list_all_metrics(self, **query_props): return self._get_paginated_results("metrics", Metric, **query_props) def submit(self, name, value, type="gauge", **query_props): - if 'tags' in query_props: + if 'tags' in query_props or self.get_tags(): self.submit_tagged(name, value, **query_props) else: payload = {'gauges': [], 'counters': []} @@ -323,14 +323,29 @@ def get_tagged(self, name, **query_props): return self._mexe("measurements/%s" % self.sanitize(name), method="GET", query_props=query_props) + def get_measurements(self, name, **query_props): + return self.get_tagged(name, **query_props) + def get_composite(self, compose, **query_props): + if self.get_tags(): + return self.get_composite_tagged(compose, **query_props) + else: + if 'resolution' not in query_props: + # Default to raw resolution + query_props['resolution'] = 1 + if 'start_time' not in query_props: + raise Exception("You must provide a 'start_time'") + query_props['compose'] = compose + return self._mexe('metrics', method="GET", query_props=query_props) + + def get_composite_tagged(self, compose, **query_props): if 'resolution' not in query_props: # Default to raw resolution query_props['resolution'] = 1 if 'start_time' not in query_props: raise Exception("You must provide a 'start_time'") query_props['compose'] = compose - return self._mexe("metrics", method="GET", query_props=query_props) + return self._mexe('measurements', method="GET", query_props=query_props) def create_composite(self, name, compose, **query_props): query_props['composite'] = compose From 599b9c0f96f80478970256f3d3f56fb66ed7335a Mon Sep 17 00:00:00 2001 From: Jason Derrett Date: Wed, 6 Sep 2017 13:18:41 -0500 Subject: [PATCH 2/4] Allow composite metric type in metric listing --- librato/metrics.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/librato/metrics.py b/librato/metrics.py index c1ad3bd..79d370c 100644 --- a/librato/metrics.py +++ b/librato/metrics.py @@ -47,10 +47,14 @@ def get(self, name, default=None): def from_dict(cls, connection, data): """Returns a metric object from a dictionary item, which is usually from librato's API""" - if data.get('type') == "gauge": + metric_type = data.get('type') + if metric_type == "gauge": cls = Gauge - elif data.get('type') == "counter": + elif metric_type == "counter": cls = Counter + elif metric_type == "composite": + # Since we don't have a formal Composite class, use Gauge for now + cls = Gauge obj = cls(connection, data['name']) obj.period = data['period'] @@ -58,7 +62,8 @@ def from_dict(cls, connection, data): obj.description = data['description'] if 'description' in data else None obj.measurements = data['measurements'] if 'measurements' in data else {} obj.query = data['query'] if 'query' in data else {} - obj.composite = data['composite'] if 'composite' in data else None + obj.composite = data.get('composite', None) + obj.source_lag = data.get('source_lag', None) return obj From 18280dda25c6d93307494eaf6aac9097ecca6ba9 Mon Sep 17 00:00:00 2001 From: Jason Derrett Date: Wed, 6 Sep 2017 13:19:03 -0500 Subject: [PATCH 3/4] Update README to reflect current state. --- README.md | 271 ++++++++++++------------------------------------------ 1 file changed, 57 insertions(+), 214 deletions(-) diff --git a/README.md b/README.md index 01bf082..bfbb081 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ python-librato A Python wrapper for the Librato Metrics API. -NOTE: Starting in version 3, we have deprecated Dashboards and Instruments in favor of Spaces and Charts. +## Source-based Librato users (legacy) +Starting in version 3, we have deprecated Dashboards and Instruments in favor of Spaces and Charts. ## Installation @@ -23,19 +24,21 @@ From your application or script: ## Authentication - We first use our credentials to connect to the API. I am assuming you have -[a librato account for Metrics](https://metrics.librato.com/). Go to your -[account settings page](https://metrics.librato.com/account) and save your +Assuming you have +[a Librato account](https://metrics.librato.com/), go to your +[account settings page](https://metrics.librato.com/account) and get your username (email address) and token (long hexadecimal string). ```python api = librato.connect('email', 'token') ``` +### Metric name sanitization + When creating your connection you may choose to provide a sanitization function. This will be applied to any metric name you pass in. For example we provide a sanitization function that will ensure your metrics are legal librato names. -This can be set as such +This can be set as such: ```python api = librato.connect('email', 'token', sanitizer=librato.sanitize_metric_name) @@ -52,62 +55,41 @@ To iterate over your metrics: print m.name ``` -or use `list_metrics()` to iterate over all your metrics with +or use `list_all_metrics()` to iterate over all your metrics with transparent pagination. -Let's now create a Metric: - -```python - api.submit("temperature", 10, description="temperature at home") -``` - -By default ```submit()``` will create a gauge metric. The metric will be -created automatically by the server if it does not exist - -To create a counter metric (note: counters are expected to be *absolute* counters and take a monotonically increasing value such as network throughput): +Let's now create a metric: ```python - api.submit("connections", 20, type="counter", description="server connections") + api.submit("temperature", 80, tags={"city": "sf"}) ``` -To iterate over your metric names: +View your metric names: ```python for m in api.list_metrics(): - print "%s: %s" % (m.name, m.description) + print(m.name) ``` -To retrieve a specific metric: +To retrieve a metric: ```python # Retrieve metric metadata ONLY gauge = api.get("temperature") gauge.name # "temperature" - gauge.description # "temperature at home" - gauge.measurements # {} - # Retrive metric with the last measurement seen - gauge = api.get("temperature", count=1, resolution=1) - gauge.measurements - # {u'unassigned': [{u'count': 1, u'sum_squares': 100.0, u'min': 10.0, u'measure_time': 1474988647, u'max': 10.0, u'sum': 10.0, u'value': 10.0}]} -``` - -Iterate over measurements: -```python - metric = api.get("temperature", count=100, resolution=1) - source = 'unassigned' - for m in metric.measurements[source]: - print "%s: %s" % (m['value'], m['measure_time']) + # Retrieve measurements from last 15 minutes + resp = api.get_measurements("temperature", duration=900, resolution=1) + # {u'name': u'temperature', + # u'links': [], + # u'series': [{u'measurements': [ + # {u'value': 80.0, u'time': 1502917147} + # ], + # u'tags': {u'city': u'sf'}}], + # u'attributes': {u'created_by_ua': u'python-librato/2.0.0...' + # , u'aggregate': False}, u'resolution': 1} ``` -Notice a couple of things here. First, we are using the key `unassigned` since -we have not associated our measurements to any source. If we had specified a -source such as `sf` we could use it in the same fashion. Read more the -[API documentation](https://www.librato.com/docs/api/). In addition, notice how -we are passing the count and resolution parameters to make sure the API -returns measurements in its answer and not only the metric properties. -Read more about them [here](https://www.librato.com/docs/api/#retrieve-a-metric-by-name). - To retrieve a composite metric: ```python @@ -115,23 +97,32 @@ To retrieve a composite metric: compose = 'mean(s("temperature", "*", {function: "mean", period: "3600"}))' import time start_time = int(time.time()) - 8 * 3600 - resp = api.get_composite(compose, start_time=start_time) - resp['measurements'][0]['series'] + + # For tag-based (new) accounts. + # Will be deprecated in favor of `get_composite` in a future tags-only release + resp = api.get_composite_tagged(compose, start_time=start_time) + resp['series'] # [ - # {u'measure_time': 1421744400, u'value': 41.23944444444444}, - # {u'measure_time': 1421748000, u'value': 40.07611111111111}, - # {u'measure_time': 1421751600, u'value': 38.77444444444445}, - # {u'measure_time': 1421755200, u'value': 38.05833333333333}, - # {u'measure_time': 1421758800, u'value': 37.983333333333334}, - # {u'measure_time': 1421762400, u'value': 38.93333333333333}, - # {u'measure_time': 1421766000, u'value': 40.556666666666665} + # { + # u'query': {u'metric': u'temperature', u'tags': {}}, + # u'metric': {u'attributes': {u'created_by_ua': u'statsd-librato-backend/0.1.7'}, + # u'type': u'gauge', + # u'name': u'temperature'}, + # u'measurements': [{u'value': 42.0, u'time': 1504719992}], + # u'tags': {u'one': u'1'}}], + # u'compose': u's("foo", "*")', + # u'resolution': 1 + # } # ] + + # For backward compatibility in legacy Librato (source-based) + resp = api.get_composite(compose, start_time=start_time) ``` To create a saved composite metric: ```python - api.create_composite('humidity', 'sum(s("all.*", "*"))', + api.create_composite('composite.humidity', 'sum(s("humidity", "*"))', description='a test composite') ``` @@ -151,11 +142,9 @@ ready, they will be submitted in an efficient manner. Here is an example: ```python api = librato.connect('email', 'token') -q = api.new_queue() -q.add('temperature', 22.1, source='upstairs') -q.add('temperature', 23.1, source='dowstairs') -q.add('num_requests', 100, type='counter', source='server1') -q.add('num_requests', 102, type='counter', source='server2') +q = api.new_queue() +q.add('temperature', 22.1, tags={'location': 'downstairs'}) +q.add('temperature', 23.1, tags={'location': 'upstairs'}) q.submit() ``` @@ -166,55 +155,29 @@ submit the first measurement as it was the only one successfully added. If the operation succeeds both measurements will be submitted. ```python -api = librato.connect('email', 'token') with api.new_queue() as q: - q.add('temperature', 22.1, source='upstairs') + q.add('temperature', 22.1, tags={'location': 'downstairs'}) potentially_dangerous_operation() - q.add('num_requests', 100, type='counter', source='server1') + q.add('num_requests', 100, tags={'host': 'server1') ``` Queues by default will collect metrics until they are told to submit. You may create a queue that autosubmits based on metric volume. ```python -api = librato.connect('email', 'token') # Submit when the 400th metric is queued q = api.new_queue(auto_submit_count=400) ``` -## Submitting tagged measurements - -NOTE: **Tagged measurements are only available in the Tags Beta. Please [contact Librato support](mailto:support@librato.com) to join the beta.** - -We can use tags in the submit method in order to associate key value pairs with our -measurements: - -```python - api.submit("temperature", 22, tags={'city': 'austin', 'station': '27'}) -``` - -Queues also support tags. When adding measurements to a queue, we can associate tags to them -in the same way we do with the submit method: - -```python - q = api.new_queue() - q.add('temperature', 12, tags={'city': 'sf' , 'station': '12'}) - q.add('temperature', 14, tags={'city': 'new york', 'station': '1'}) - q.add('temperature', 22, tags={'city': 'austin' , 'station': '112'}) - q.submit() -``` - ## Updating Metric Attributes You can update the information for a metric by using the `update` method, for example: ```python -api = librato.connect('email', 'token') -for metric in api.list_metrics(name=" "): - gauge = api.get(metric.name) - attrs = gauge.attributes - attrs['display_units_long'] = 'ms' +for metric in api.list_metrics(name="abc*"): + attrs = metric.attributes + attrs['display_units_short'] = 'ms' api.update(metric.name, attributes=attrs) ``` @@ -298,67 +261,8 @@ space.delete() ``` ### Create a Chart -```python -# Create a Chart directly via API (defaults to line chart) -space = api.find_space('Production') -chart = api.create_chart( - 'cpu', - space, - streams=[{'metric': 'cpu.idle', 'source': '*'}] -) -``` -```python -# Create line chart using the Space model -space = api.find_space('Production') - -# You can actually create an empty chart (default to line) -chart = space.add_chart('cpu') - -# Create a chart with all attributes -chart = space.add_chart( - 'memory', - type='line', - streams=[ - {'metric': 'memory.free', 'source': '*'}, - {'metric': 'memory.used', 'source': '*', - 'group_function': 'breakout', 'summary_function': 'average'} - ], - min=0, - max=50, - label='the y axis label', - use_log_yaxis=True, - related_space=1234 -) -``` - -```python -# Shortcut to create a line chart with a single metric on it -chart = space.add_single_line_chart('my chart', 'my.metric', '*') -chart = space.add_single_line_chart('my chart', metric='my.metric', source='*') -``` - -```python -# Shortcut to create a stacked chart with a single metric on it -chart = space.add_single_stacked_chart('my chart', 'my.metric', '*') -``` - -```python -# Create a big number chart -bn = space.add_chart( - 'memory', - type='bignumber', - streams=[{'metric': 'my.metric', 'source': '*'}] -) -# Shortcut to add big number chart -bn = space.add_bignumber_chart('My Chart', 'my.metric', '*') -bn = space.add_bignumber_chart('My Chart', 'my.metric', - source='*', - group_function='sum', - summary_function='sum', - use_last_value=True -) -``` +Not yet supported for tags-based accounts. ### Find a Chart ```python @@ -368,12 +272,8 @@ chart = api.get_chart(chart_id, space) ``` ### Update a Chart -```python -chart = api.get_chart(chart_id, space_id) -chart.min = 0 -chart.max = 50 -chart.save() -``` + +Not yet supported for tags-based accounts. ### Rename a Chart ```python @@ -382,22 +282,12 @@ chart = api.get_chart(chart_id, space_id) chart.rename('new chart name') ``` -### Add new metrics to a Chart -```python -chart = space.charts()[-1] -chart.new_stream('foo', '*') -chart.new_stream(metric='foo', source='*') -chart.new_stream(composite='s("foo", "*")') -chart.save() -``` - ### Delete a Chart ```python chart = api.get_chart(chart_id, space_id) chart.delete() ``` - ## Alerts List all alerts: @@ -473,53 +363,6 @@ print(alert.conditions) print(alert.services) ``` - -## Client-side Aggregation - -You can aggregate measurements before submission using the `Aggregator` class. Optionally, specify a `measure_time` to submit that timestamp to the API. You may also optionally specify a `period` to floor the timestamp to a particular interval. If `period` is specified without a `measure_time`, the current timestamp will be used, and floored to `period`. Specifying an optional `source` allows the aggregated measurement to report a source name. - -Aggregator instances can be sent immediately by calling `submit()` or added to a `Queue` by calling `queue.add_aggregator()`. - -```python -from librato.aggregator import Aggregator - -api = librato.connect('email', 'token') - -a = Aggregator(api) -a.add("foo", 42) -a.add("foo", 5) -# count=2, min=5, max=42, sum=47 (value calculated by API = mean = 23.5), source=unassigned -# measure_time = -a.submit() - -a = Aggregator(api, source='my.source', period=60) -a.add("foo", 42) -a.add("foo", 5) -# count=2, min=5, max=42, sum=47 (value calculated by API = mean = 23.5), source=my.source -# measure_time = - ( % 60) -a.submit() - -a = Aggregator(api, period=60, measure_time=1419302671) -a.add("foo", 42) -a.add("foo", 5) -# count=2, min=5, max=42, sum=47 (value calculated by API = mean = 23.5), source=unassigned -# measure_time = 1419302671 - (1419302671 % 60) = 1419302671 - 31 = 1419302640 -a.submit() - -a = Aggregator(api, measure_time=1419302671) -a.add("foo", 42) -a.add("foo", 5) -# count=2, min=5, max=42, sum=47 (value calculated by API = mean = 23.5), source=unassigned -# measure_time = 1419302671 -a.submit() - - -# You can also add an Aggregator instance to a queue -q = librato.queue.Queue(api) -q.add_aggregator(a) -q.submit() -``` - ## Misc ### Timeouts @@ -529,8 +372,8 @@ that by using `api.set_timeout(timeout)`. ## Contribution -Do you want to contribute? Do you need a new feature? Please open a -[ticket](https://github.com/librato/python-librato/issues). +Want to contribute? Need a new feature? Please open an +[issue](https://github.com/librato/python-librato/issues). ## Contributors @@ -539,4 +382,4 @@ graciously handed over maintainership of the project to us and we're super-appre ## Copyright -Copyright (c) 2011-2016 [Librato Inc.](http://librato.com) See LICENSE for details. +Copyright (c) 2011-2017 [Librato Inc.](http://librato.com) See LICENSE for details. From 35c4e59da9f3df5280eff7b8826f31f5cc3ad337 Mon Sep 17 00:00:00 2001 From: Jason Derrett Date: Wed, 6 Sep 2017 13:53:51 -0500 Subject: [PATCH 4/4] Update README notes to point to legacy --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bfbb081..db0fd3b 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,12 @@ python-librato A Python wrapper for the Librato Metrics API. -## Source-based Librato users (legacy) -Starting in version 3, we have deprecated Dashboards and Instruments in favor of Spaces and Charts. +## Documentation Notes + +- New accounts + - Refer to [master](https://github.com/librato/python-librato/tree/master) for the latest documentation. +- Legacy (source-based) Librato users + - Please see the [legacy documentation](https://github.com/librato/python-librato/tree/v2.1.2) ## Installation @@ -320,13 +324,6 @@ alert.add_condition_for('metric_name').stops_reporting_for(5) # duration in minu alert.save() ``` -Restrict the condition to a specific source (default is `*`): -```python -alert = api.create_alert('my.alert') -alert.add_condition_for('metric_name', 'mysource') -alert.save() -``` - View all outbound services for the current user ```python for service in api.list_services():