diff --git a/README.md b/README.md index 80b5248..646346e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 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. +## Sections ## Installation @@ -58,81 +58,56 @@ transparent pagination. Let's now create a Metric: ```python - api.submit("temperature", 10, description="temperature at home") + api.submit("temperature", 10, tags={'city': 'Barcelona'}) ``` -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): - -```python - api.submit("connections", 20, type="counter", description="server connections") -``` +The metric will be created automatically by the server if it does not exist. To iterate over your metric names: ```python - for m in api.list_metrics(): - print "%s: %s" % (m.name, m.description) + for m in api.list_metrics(): + print "%s: %s" % (m.name, m.description) ``` To retrieve a specific 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}]} + tags = {"city": "Barcelona} + metric = api.get_metric("temperature") + metric.name # "temperature" + + # Retrieve the last measurements seen on that metric + data = api.get("temperature", duration=60, tags=tags) + data = self.api.get(name, duration=60, tags=tags) + data['series'][0]['measurements'] ``` 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']) + data = api.get("temperature", duration=60, tags=tags) + data = self.api.get(name, duration=60, tags=tags) + data['series'][0]['measurements'] + for m in data['series'][0]['measurements']: + print m ``` -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-metric-by-name). - To retrieve a composite metric: ```python - # Get average temperature across all cities for last 8 hours - 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'] - # [ - # {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} - # ] + compose = 'mean(s("' + name + '", "*", {function: "mean", period: "3600"}))' + import time + start_time = int(time.time()) - 3600 + data = self.api.get_composite(compose, start_time=start_time) + for m in data['series'][0]['measurements']: + print m ``` To create a saved composite metric: ```python - api.create_composite('humidity', 'sum(s("all.*", "*"))', - description='a test composite') + api.create_composite('humidity', 'sum(s("all.*", "*"))', description='a test composite') ``` Delete a metric: @@ -150,13 +125,11 @@ in batch mode. We push measurements that are stored and when we are 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.submit() + 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() ``` Queues can also be used as context managers. Once the context block is complete the queue @@ -168,40 +141,18 @@ 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={'city': 'sf', 'station': '12'}) potentially_dangerous_operation() - q.add('num_requests', 100, type='counter', source='server1') + q.add('num_requests', tags={'city': 'austin', 'station': '112'}) ``` 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() + api = librato.connect('email', 'token') + # Submit when the 400th metric is queued + q = api.new_queue(auto_submit_count=400) ``` ## Updating Metric Attributes @@ -210,12 +161,10 @@ 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' - api.update(metric.name, attributes=attrs) + metric = self.api.get_metric("temperature") + attrs = metric.attributes + attrs['description'] = "A temperature metric" + self.api.update(name, attributes=attrs) ``` ## Annotations @@ -266,138 +215,6 @@ Delete a named annotation stream: api.delete_annotation_stream("testing") ``` -## Spaces API -### List Spaces -```python -# List spaces -spaces = api.list_spaces() -``` - -### Create a Space -```python -# Create a new Space directly via API -space = api.create_space("space_name") -print("Created '%s'" % space.name) - -# Create a new Space via the model, passing the connection -space = Space(api, 'Production') -space.save() -``` - -### Find a Space -```python -space = api.find_space('Production') -``` - -### Delete a Space -```python -space = api.create_space('Test') -api.delete_space(space.id) -# or -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 -) -``` - -### Find a Chart -```python -# Takes either space_id or a space object -chart = api.get_chart(chart_id, space_id) -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() -``` - -### Rename a Chart -```python -chart = api.get_chart(chart_id, space_id) -# save() gets called automatically here -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: @@ -476,44 +293,47 @@ 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. +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`. -Aggregator instances can be sent immediately by calling `submit()` or added to a `Queue` by calling `queue.add_aggregator()`. +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 = Aggregator(api, tags={'host': 'machine1'}) 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 +# count=2, min=5, max=42, sum=47 (value calculated by API = mean = 23.5) # measure_time = a.submit() -a = Aggregator(api, source='my.source', period=60) +a = Aggregator(api, tags={'host': 'machine1'}, 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 +# count=2, min=5, max=42, sum=47 (value calculated by API = mean = 23.5) # measure_time = - ( % 60) a.submit() -a = Aggregator(api, period=60, measure_time=1419302671) +a = Aggregator(api, period=60, measure_time=1419302671, tags={'host': 'machine1'}) 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 +# count=2, min=5, max=42, sum=47 (value calculated by API = mean = 23.5) # measure_time = 1419302671 - (1419302671 % 60) = 1419302671 - 31 = 1419302640 a.submit() -a = Aggregator(api, measure_time=1419302671) +a = Aggregator(api, measure_time=1419302671, tags={'host': 'machine1'}) 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 +# count=2, min=5, max=42, sum=47 (value calculated by API = mean = 23.5) # measure_time = 1419302671 a.submit() - # You can also add an Aggregator instance to a queue q = librato.queue.Queue(api) q.add_aggregator(a) diff --git a/librato/__init__.py b/librato/__init__.py index 898b191..5f0e702 100644 --- a/librato/__init__.py +++ b/librato/__init__.py @@ -35,7 +35,7 @@ import email.message from librato import exceptions from librato.queue import Queue -from librato.metrics import Gauge, Counter +from librato.metrics import Gauge from librato.alerts import Alert, Service from librato.annotations import Annotation from librato.spaces import Space, Chart @@ -248,12 +248,14 @@ def add_tags(self, d): # def list_metrics(self, **query_props): """List a page of metrics""" + from librato.metrics import Metric resp = self._mexe("metrics", query_props=query_props) return self._parse(resp, "metrics", Metric) def list_all_metrics(self, **query_props): """List all avaliable metrics""" + if 'length' not in query_props: query_props['length'] = 100 if 'offset' not in query_props: @@ -267,18 +269,9 @@ def list_all_metrics(self, **query_props): if len(metric_list) < page_size: break - def submit(self, name, value, type="gauge", **query_props): - if 'tags' in query_props: - self.submit_tagged(name, value, **query_props) - else: - payload = {'gauges': [], 'counters': []} - metric = {'name': self.sanitize(name), 'value': value} - for k, v in query_props.items(): - metric[k] = v - payload[type + 's'].append(metric) - self._mexe("metrics", method="POST", query_props=payload) - - def submit_tagged(self, name, value, **query_props): + def submit(self, name, value, **query_props): + """Send measurements for a metric""" + payload = {'measurements': []} if self.tags: @@ -294,17 +287,17 @@ def submit_tagged(self, name, value, **query_props): payload['measurements'].append(measurement) self._mexe("measurements", method="POST", query_props=payload) - def get(self, name, **query_props): + def get_metric(self, name, **query_props): + """Get a metric definition""" + resp = self._mexe("metrics/%s" % self.sanitize(name), method="GET", query_props=query_props) if resp['type'] == 'gauge': return Gauge.from_dict(self, resp) - elif resp['type'] == 'counter': - return Counter.from_dict(self, resp) else: raise Exception('The server sent me something that is not a Gauge nor a Counter.') - def get_tagged(self, name, **query_props): - """Fetches multi-dimensional metrics""" + def get(self, name, **query_props): + """Fetches metric data""" if 'resolution' not in query_props: # Default to raw resolution query_props['resolution'] = 1 @@ -320,23 +313,27 @@ def get_tagged(self, name, **query_props): return self._mexe("measurements/%s" % self.sanitize(name), method="GET", query_props=query_props) def get_composite(self, compose, **query_props): + """Get a composite result""" 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): + """Create a composite""" query_props['composite'] = compose query_props['type'] = 'composite' return self.update(name, **query_props) def update(self, name, **query_props): + """update a metric""" return self._mexe("metrics/%s" % self.sanitize(name), method="PUT", query_props=query_props) def delete(self, names): + """delete a metric or a group of metrics""" if isinstance(names, six.string_types): names = self.sanitize(names) else: @@ -454,9 +451,9 @@ def get_space(self, id, **query_props): return Space.from_dict(self, resp) def find_space(self, name): + """Find specific space by Name""" if type(name) is int: raise ValueError("This method expects name as a parameter, %s given" % name) - """Find specific space by Name""" spaces = self.list_spaces(name=name) # Find the Space by name (case-insensitive) # This returns the first space found matching the name diff --git a/librato/aggregator.py b/librato/aggregator.py index 78d6eb2..65ec6cf 100644 --- a/librato/aggregator.py +++ b/librato/aggregator.py @@ -35,12 +35,9 @@ class Aggregator(object): def __init__(self, connection, **args): self.connection = connection - # Global source for all 'legacy' metrics sent into the aggregator - self.source = args.get('source') # Global tags, which apply to MD metrics only self.tags = dict(args.get('tags', {})) self.measurements = {} - self.tagged_measurements = {} self.period = args.get('period') self.measure_time = args.get('measure_time') @@ -75,56 +72,7 @@ def add(self, name, value): return self.measurements - def add_tagged(self, name, value): - if name not in self.tagged_measurements: - self.tagged_measurements[name] = { - 'count': 1, - 'sum': value, - 'min': value, - 'max': value - } - else: - m = self.tagged_measurements[name] - m['sum'] += value - m['count'] += 1 - if value < m['min']: - m['min'] = value - if value > m['max']: - m['max'] = value - - return self.tagged_measurements - def to_payload(self): - # Map measurements into Librato POST (array) format - # { - # 'gauges': [ - # {'count': 1, 'max': 42, 'sum': 42, 'name': 'foo', 'min': 42} - # ] - # 'measure_time': 1418838418 (optional) - # 'source': 'mysource' (optional) - # } - # Note: hash format would work too, but the mocks aren't currently set up - # for the hash format :-( - # i.e. result = {'gauges': dict(self.measurements)} - - body = [] - for metric_name in self.measurements: - # Create a clone so we don't change self.measurements - vals = dict(self.measurements[metric_name]) - vals["name"] = metric_name - body.append(vals) - - result = {'gauges': body} - if self.source: - result['source'] = self.source - - mt = self.floor_measure_time() - if mt: - result['measure_time'] = mt - - return result - - def to_md_payload(self): # Map measurements into Librato MD POST format # { # 'measures': [ @@ -135,9 +83,9 @@ def to_md_payload(self): # } body = [] - for metric_name in self.tagged_measurements: - # Create a clone so we don't change self.tagged_measurements - vals = dict(self.tagged_measurements[metric_name]) + for metric_name in self.measurements: + # Create a clone so we don't change self.measurements + vals = dict(self.measurements[metric_name]) vals["name"] = metric_name body.append(vals) @@ -179,19 +127,11 @@ def floor_measure_time(self): def clear(self): self.measurements = {} - self.tagged_measurements = {} self.measure_time = None def submit(self): - # Submit any legacy or tagged measurements to API - # This will actually return an empty 200 response (no body) - if self.measurements: - self.connection._mexe("metrics", - method="POST", - query_props=self.to_payload()) - if self.tagged_measurements: - self.connection._mexe("measurements", - method="POST", - query_props=self.to_md_payload()) + self.connection._mexe("measurements", + method="POST", + query_props=self.to_payload()) # Clear measurements self.clear() diff --git a/librato/metrics.py b/librato/metrics.py index c1ad3bd..640131c 100644 --- a/librato/metrics.py +++ b/librato/metrics.py @@ -49,8 +49,6 @@ def from_dict(cls, connection, data): which is usually from librato's API""" if data.get('type') == "gauge": cls = Gauge - elif data.get('type') == "counter": - cls = Counter obj = cls(connection, data['name']) obj.period = data['period'] @@ -67,24 +65,5 @@ def __repr__(self): class Gauge(Metric): - """Librato Gauge metric""" - def add(self, value, source=None, **params): - """Add a new measurement to this gauge""" - if source: - params['source'] = source - return self.connection.submit(self.name, value, type="gauge", **params) - def what_am_i(self): return 'gauges' - - -class Counter(Metric): - """Librato Counter metric""" - def add(self, value, source=None, **params): - if source: - params['source'] = source - - return self.connection.submit(self.name, value, type="counter", **params) - - def what_am_i(self): - return 'counters' diff --git a/librato/queue.py b/librato/queue.py index 0ff3834..ddc9a5d 100644 --- a/librato/queue.py +++ b/librato/queue.py @@ -62,24 +62,7 @@ def set_tags(self, d): def add_tags(self, d): self.tags.update(d) - def add(self, name, value, type='gauge', **query_props): - if len(self.tags) > 0: - query_props['tags'] = self.tags - - if 'tags' in query_props: - self.add_tagged(name, value, **query_props) - else: - nm = {} # new measurement - nm['name'] = self.connection.sanitize(name) - nm['value'] = value - - for pn, v in query_props.items(): - nm[pn] = v - - self._add_measurement(type, nm) - self._auto_submit_if_necessary() - - def add_tagged(self, name, value, **query_props): + def add(self, name, value, **query_props): nm = {} # new measurement nm['name'] = self.connection.sanitize(name) nm['sum'] = value @@ -92,26 +75,12 @@ def add_tagged(self, name, value, **query_props): self._auto_submit_if_necessary() def add_aggregator(self, aggregator): - cloned_measurements = dict(aggregator.measurements) - # Find measure_time, if any mt = aggregator.get_measure_time() - for name in cloned_measurements: - nm = cloned_measurements[name] - # Set metric name - nm['name'] = name - # Set measure_time - if mt: - nm['measure_time'] = mt - # Set source - if aggregator.source: - nm['source'] = aggregator.source - self._add_measurement('gauge', nm) - - tagged_measurements = dict(aggregator.tagged_measurements) - for name in tagged_measurements: - nm = tagged_measurements[name] + measurements = dict(aggregator.measurements) + for name in measurements: + nm = measurements[name] nm['name'] = name if mt: @@ -165,13 +134,13 @@ def _add_tagged_measurement(self, nm): self.tagged_chunks.append({'measurements': []}) self.tagged_chunks[-1]['measurements'].append(nm) - def _current_chunk(self, tagged=False): + def _current_chunk(self, tagged=True): if tagged: return self.tagged_chunks[-1] if self.tagged_chunks else None else: return self.chunks[-1] if self.chunks else None - def _num_measurements_in_current_chunk(self, tagged=False): + def _num_measurements_in_current_chunk(self, tagged=True): if tagged: if self.tagged_chunks: return len(self.tagged_chunks[-1]['measurements']) diff --git a/librato/spaces.py b/librato/spaces.py deleted file mode 100644 index 719db3a..0000000 --- a/librato/spaces.py +++ /dev/null @@ -1,229 +0,0 @@ -from librato.streams import Stream - - -class Space(object): - """Librato Space Base class""" - - def __init__(self, - connection, - name, - id=None, - chart_dicts=None, - tags=False): - self.connection = connection - self.name = name - self.chart_ids = [] - self._charts = None - self.tags = tags - for c in (chart_dicts or []): - self.chart_ids.append(c['id']) - self.id = id - - @classmethod - def from_dict(cls, connection, data): - """ - Returns a Space object from a dictionary item, - which is usually from librato's API - """ - obj = cls(connection, - data['name'], - id=data['id'], - chart_dicts=data.get('charts'), - tags=data.get('tags')) - return obj - - def get_payload(self): - return {'name': self.name} - - def persisted(self): - return self.id is not None - - def charts(self): - if self._charts is None or self._charts == []: - self._charts = self.connection.list_charts_in_space(self) - return self._charts[:] - - # New up a chart - def new_chart(self, name, **kwargs): - return Chart(self.connection, name, space_id=self.id, **kwargs) - - # New up a chart and save it - def add_chart(self, name, **kwargs): - chart = self.new_chart(name, **kwargs) - return chart.save() - - def add_line_chart(self, name, streams=[]): - return self.add_chart(name, streams=streams) - - def add_single_line_chart(self, name, metric=None, source='*', - group_function=None, summary_function=None): - stream = {'metric': metric, 'source': source} - - if group_function: - stream['group_function'] = group_function - if summary_function: - stream['summary_function'] = summary_function - return self.add_line_chart(name, streams=[stream]) - - def add_stacked_chart(self, name, streams=[]): - return self.add_chart(name, type='stacked', streams=streams) - - def add_single_stacked_chart(self, name, metric, source='*'): - stream = {'metric': metric, 'source': source} - return self.add_stacked_chart(name, streams=[stream]) - - def add_bignumber_chart(self, name, metric, source='*', - group_function='average', - summary_function='average', use_last_value=True): - stream = { - 'metric': metric, - 'source': source, - 'group_function': group_function, - 'summary_function': summary_function - } - chart = self.add_chart(name, - type='bignumber', - use_last_value=use_last_value, - streams=[stream]) - return chart - - # This currently only updates the name of the Space - def save(self): - if self.persisted(): - return self.connection.update_space(self) - else: - s = self.connection.create_space(self.name, tags=self.tags) - self.id = s.id - return s - - def rename(self, new_name): - self.name = new_name - self.save() - - def delete(self): - return self.connection.delete_space(self.id) - - -class Chart(object): - # Payload example from /spaces/123/charts/456 API - # { - # "id": 1723352, - # "name": "Hottest City", - # "type": "line", - # "streams": [ - # { - # "id": 19261984, - # "metric": "apparent_temperature", - # "type": "gauge", - # "source": "*", - # "group_function": "max", - # "summary_function": "max" - # } - # ], - # "max": 105, - # "min": 0, - # "related_space": 96893, - # "label": "The y axis label", - # "use_log_yaxis": true - # } - def __init__(self, connection, name=None, id=None, type='line', - space_id=None, streams=[], - min=None, max=None, - label=None, - use_log_yaxis=None, - use_last_value=None, - related_space=None): - self.connection = connection - self.name = name - self.type = type - self.space_id = space_id - self._space = None - self.streams = [] - self.label = label - self.min = min - self.max = max - self.use_log_yaxis = use_log_yaxis - self.use_last_value = use_last_value - self.related_space = related_space - for i in streams: - if isinstance(i, Stream): - self.streams.append(i) - elif isinstance(i, dict): # Probably parsing JSON here - # dict - self.streams.append(Stream(**i)) - else: - # list? - self.streams.append(Stream(*i)) - self.id = id - - @classmethod - def from_dict(cls, connection, data): - """ - Returns a Chart object from a dictionary item, - which is usually from librato's API - """ - obj = cls(connection, - data['name'], - id=data['id'], - type=data.get('type', 'line'), - space_id=data.get('space_id'), - streams=data.get('streams'), - min=data.get('min'), - max=data.get('max'), - label=data.get('label'), - use_log_yaxis=data.get('use_log_yaxis'), - use_last_value=data.get('use_last_value'), - related_space=data.get('related_space')) - return obj - - def space(self): - if self._space is None and self.space_id is not None: - # Find the Space - self._space = self.connection.get_space(self.space_id) - return self._space - - def known_attributes(self): - return ['min', 'max', 'label', 'use_log_yaxis', 'use_last_value', - 'related_space'] - - def get_payload(self): - # Set up the things that we aren't considering just "attributes" - payload = { - 'name': self.name, - 'type': self.type, - 'streams': self.streams_payload() - } - for attr in self.known_attributes(): - if getattr(self, attr) is not None: - payload[attr] = getattr(self, attr) - return payload - - def streams_payload(self): - return [s.get_payload() for s in self.streams] - - def new_stream(self, metric=None, source='*', **kwargs): - stream = Stream(metric, source, **kwargs) - self.streams.append(stream) - return stream - - def persisted(self): - return self.id is not None - - def save(self): - if self.persisted(): - return self.connection.update_chart(self, self.space()) - else: - payload = self.get_payload() - # Don't include name twice - payload.pop('name') - resp = self.connection.create_chart(self.name, self.space(), - **payload) - self.id = resp.id - return resp - - def rename(self, new_name): - self.name = new_name - self.save() - - def delete(self): - return self.connection.delete_chart(self.id, self.space_id) diff --git a/librato/streams.py b/librato/streams.py deleted file mode 100644 index 4bb4a42..0000000 --- a/librato/streams.py +++ /dev/null @@ -1,56 +0,0 @@ -class Stream(object): - def __init__(self, metric=None, source='*', composite=None, - name=None, type=None, id=None, - group_function=None, summary_function=None, - transform_function=None, downsample_function=None, - period=None, split_axis=None, gap_detection=None, - min=None, max=None, - units_short=None, units_long=None, color=None, - position=None, - # deprecated - composite_function=None, **kwargs - ): - self.metric = metric - self.source = source - # Spaces API - self.composite = composite - # For instrument compatibility - self.name = name - self.type = type - self.id = id - # average, sum, min, max, breakout - self.group_function = group_function - # average, sum, min, max, count (or derivative if counter) - self.summary_function = summary_function - self.transform_function = transform_function - self.downsample_function = downsample_function - self.period = period - self.split_axis = split_axis - self.min = min - self.max = max - self.units_short = units_short - self.units_long = units_long - self.color = color - self.gap_detection = gap_detection - self.position = position - - # Pick up any attributes that are not explicitly defined - for attr in kwargs: - setattr(self, attr, kwargs[attr]) - - # Can't have a composite and source/metric - if self.composite: - self.source = None - self.metric = None - - def _attrs(self): - return ['metric', 'source', 'composite', 'name', - 'type', 'id', 'group_function', 'summary_function', 'transform_function', 'downsample_function', - 'period', 'split_axis', 'min', 'max', 'units_short', 'units_long'] - - def get_payload(self): - payload = {} - for attr in self._attrs(): - if getattr(self, attr) is not None: - payload[attr] = getattr(self, attr) - return payload diff --git a/tests/integration.py b/tests/integration.py index 15476e9..76a584d 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -28,7 +28,8 @@ import os from random import randint import time -logging.basicConfig(level=logging.INFO) +#logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.DEBUG) class TestLibratoBase(unittest.TestCase): @@ -42,6 +43,7 @@ def setUpClass(cls): """ Ensure user really wants to run these tests """ are_you_sure = os.environ.get('LIBRATO_ALLOW_INTEGRATION_TESTS') + print are_you_sure assert are_you_sure == 'Y', "INTEGRATION TESTS WILL DELETE METRICS " \ "IN YOUR ACCOUNT!!! " \ "If you are absolutely sure that you want to run tests "\ @@ -59,19 +61,26 @@ def wait_for_replication(self): time.sleep(1) +def tags(): + return {'key1': 'value1'} + + +def m_time(t): + return int(time.time() - t) + + class TestLibratoBasic(TestLibratoBase): def test_list_metrics(self): metrics = self.conn.list_metrics() - def _add_and_verify_metric(self, name, value, desc, connection=None, type='gauge'): + def _add_and_verify_metric(self, name, value, tags, connection=None): if not connection: connection = self.conn - connection.submit(name, value, type=type, description=desc) + connection.submit(name, value, tags=tags) self.wait_for_replication() - metric = connection.get(name) - assert metric and metric.name == connection.sanitize(name) - assert metric.description == desc + metric = connection.get(name, duration=60, tags_search="foo=bar") + assert metric and metric['name'] == connection.sanitize(name) return metric def _delete_and_verify_metric(self, names, connection=None): @@ -87,58 +96,53 @@ def _delete_and_verify_metric(self, names, connection=None): assert(metric is None) def test_long_sanitized_metric(self): - name, desc = 'a' * 256, 'Too long, will error' + name = 'a' * 256 with self.assertRaises(BadRequest): - self._add_and_verify_metric(name, 10, desc, self.conn) - self._add_and_verify_metric(name, 10, desc, self.conn_sanitize) + self._add_and_verify_metric(name, 10, tags(), self.conn) + self._add_and_verify_metric(name, 10, tags(), self.conn_sanitize) self._delete_and_verify_metric(name, self.conn_sanitize) def test_invalid_sanitized_metric(self): - name, desc = r'I AM #*@#@983221 CRazy((\\\\] invalid', 'Crazy invalid' + name = r'I AM #*@#@983221 CRazy((\\\\] invalid' with self.assertRaises(BadRequest): - self._add_and_verify_metric(name, 10, desc, self.conn) - self._add_and_verify_metric(name, 10, desc, self.conn_sanitize) + self._add_and_verify_metric(name, 10, tags(), self.conn) + self._add_and_verify_metric(name, 10, tags(), self.conn_sanitize) self._delete_and_verify_metric(name, self.conn_sanitize) def test_create_and_delete_gauge(self): - name, desc = 'Test', 'Test Gauge to be removed' - self._add_and_verify_metric(name, 10, desc) - self._delete_and_verify_metric(name) - - def test_create_and_delete_counter(self): - name, desc = 'Test_counter', 'Test Counter to be removed' - self._add_and_verify_metric(name, 10, desc, type='counter') + name = 'a_py_metric_del_gau_test' + self._add_and_verify_metric(name, 10, tags()) self._delete_and_verify_metric(name) def test_batch_delete(self): - name_one, desc_one = 'Test_one', 'Test gauge to be removed' - name_two, desc_two = 'Test_two', 'Test counter to be removed' - self._add_and_verify_metric(name_one, 10, desc_one) - self._add_and_verify_metric(name_two, 10, desc_two, type='counter') + name_one = 'Test_one' + name_two = 'Test_two' + self._add_and_verify_metric(name_one, 10, tags()) + self._add_and_verify_metric(name_two, 10, tags()) self._delete_and_verify_metric([name_one, name_two]) def test_save_gauge_metrics(self): - name, desc = 'Test', 'Test Counter to be removed' - self.conn.submit(name, 10, description=desc) - self.conn.submit(name, 20, description=desc) + name = 'py_s_ga_me_test' + self.conn.submit(name, 10, tags=tags()) + self.conn.submit(name, 20, tags=tags()) self.conn.delete(name) def test_send_batch_gauge_measurements(self): - q = self.conn.new_queue() + q = self.conn.new_queue(tags=tags()) for t in range(1, 10): q.add('temperature', randint(20, 40)) q.submit() for t in range(1, 10): - q.add('temperature', randint(20, 40), measure_time=time.time() + t) + q.add('temperature', randint(20, 40), time=m_time(t)) q.submit() for t in range(1, 10): - q.add('temperature', randint(20, 40), source='upstairs', measure_time=time.time() + t) + q.add('temperature', randint(20, 40), tags={'k1': 'v2'}, time=m_time(t)) q.submit() for t in range(1, 50): - q.add('temperature', randint(20, 30), source='downstairs', measure_time=time.time() + t) + q.add('temperature', randint(20, 30), tags={'k2': 'v3'}, time=m_time(t)) q.submit() self.conn.delete('temperature') @@ -146,7 +150,7 @@ def test_batch_sanitation(self): name_one, name_two = 'a' * 500, r'DSJAK#32102391S,m][][[{{]\\' def run_batch(connection): - q = connection.new_queue() + q = connection.new_queue(tags=tags()) q.add(name_one, 10) q.add(name_two, 10) q.submit() @@ -160,39 +164,28 @@ def run_batch(connection): def test_submit_empty_queue(self): self.conn.new_queue().submit() - def test_send_batch_counter_measurements(self): - q = self.conn.new_queue() - for nr in range(1, 2): - q.add('num_req', nr, type='counter', source='server1', measure_time=time.time() - 1) - q.add('num_req', nr, type='counter', source='server2', measure_time=time.time() - 1) - q.submit() - def test_update_metrics_attributes(self): - name, desc = 'Test', 'A great gauge.' - self.conn.submit(name, 10, description=desc) + name = 'py_update_m__att' + self.conn.submit(name, 10, tags=tags()) self.wait_for_replication() - gauge = self.conn.get(name) + gauge = self.conn.get_metric(name) assert gauge and gauge.name == name - assert gauge.description == desc - gauge = self.conn.get(name) attrs = gauge.attributes attrs['display_min'] = 0 self.conn.update(name, attributes=attrs) - gauge = self.conn.get(name) + gauge = self.conn.get_metric(name) assert gauge.attributes['display_min'] == 0 self.conn.delete(name) def test_sanitized_update(self): - name, desc = 'a' * 1000, 'too long, really' - new_desc = 'different' - self.conn_sanitize.submit(name, 10, description=desc) - gauge = self.conn_sanitize.get(name) - assert gauge.description == desc + name = 'a' * 1000 + self.conn_sanitize.submit(name, 10, tags=tags()) + gauge = self.conn_sanitize.get_metric(name) - attrs = gauge.attributes['description'] = new_desc + attrs = gauge.attributes['description'] = 'foo' with self.assertRaises(BadRequest): self.conn.update(name, attributes=attrs) self.conn_sanitize.delete(name) @@ -207,7 +200,7 @@ def setUp(self): # Ensure metric names exist so we can create conditions on them for m in self.gauges_used_during_test: # Create or just update a gauge metric - self.conn.submit(m, 42) + self.conn.submit(m, 42, tags={'number': '1'}) def tearDown(self): for name in self.alerts_created_during_test: @@ -310,111 +303,6 @@ def unique_name(self, prefix): return name -class TestSpacesApi(TestLibratoBase): - @classmethod - def setUpClass(cls): - super(TestSpacesApi, cls).setUpClass() - for space in cls.conn.list_spaces(): - cls.conn.delete_space(space.id) - - def test_create_space(self): - space = self.conn.create_space("my space") - self.assertIsNotNone(space.id) - - def test_get_space_by_id(self): - space = self.conn.create_space("find me by id") - self.wait_for_replication() - found = self.conn.get_space(space.id) - self.assertEqual(space.id, found.id) - - def test_find_space_by_name(self): - # This assumes you only have 1 space with this name (it's not unique) - space = self.conn.create_space("find me by name") - self.wait_for_replication() - found = self.conn.find_space(space.name) - self.assertEqual(space.id, found.id) - space.delete() - - def test_list_spaces(self): - name = 'list me' - space = self.conn.create_space(name) - spaces = self.conn.list_spaces() - self.assertTrue(name in [s.name for s in spaces]) - - def test_delete_space(self): - space = self.conn.create_space("delete me") - self.wait_for_replication() - self.conn.delete_space(space.id) - - def test_create_space_via_model(self): - space = librato.Space(self.conn, 'Production') - self.assertIsNone(space.id) - space.save() - self.assertIsNotNone(space.id) - - def test_delete_space_via_model(self): - space = librato.Space(self.conn, 'delete me') - space.save() - self.wait_for_replication() - space.delete() - self.wait_for_replication() - self.assertRaises(librato.exceptions.NotFound, self.conn.get_space, space.id) - - def test_create_chart(self): - # Ensure metrics exist - self.conn.submit('memory.free', 100) - self.conn.submit('memory.used', 200) - # Create space - space = librato.Space(self.conn, 'my space') - space.save() - # Add chart - 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 - ) - self.wait_for_replication() - self.assertEqual(len(chart.streams), 2) - - def test_create_big_number(self): - # Ensure metrics exist - self.conn.submit('memory.free', 100) - # Create space - space = librato.Space(self.conn, 'my space') - space.save() - self.wait_for_replication() - chart = space.add_chart( - 'memory', - type='bignumber', - streams=[ - {'metric': 'memory.free', 'source': '*'} - ], - use_last_value=False - ) - - # Shortcut - chart2 = space.add_bignumber_chart('memory 2', 'memory.free', 'foo*', - use_last_value=True) - - self.wait_for_replication() - - self.assertEqual(chart.type, 'bignumber') - self.assertEqual(len(chart.streams), 1) - self.assertFalse(chart.use_last_value) - self.assertEqual(chart2.type, 'bignumber') - self.assertEqual(len(chart2.streams), 1) - self.assertTrue(chart2.use_last_value) - - if __name__ == '__main__': # TO run a specific test: # $ nosetests tests/integration.py:TestLibratoBasic.test_update_metrics_attributes diff --git a/tests/mock_connection.py b/tests/mock_connection.py index 4b550a0..09b6e9f 100644 --- a/tests/mock_connection.py +++ b/tests/mock_connection.py @@ -68,14 +68,20 @@ def create_tagged_measurements(self, payload): def_time = payload.get('time', int(time.time())) for metric in payload['measurements']: name = metric['name'] + desc = None + if 'description' in metric: + desc = metric['description'] + self.add_metric_to_store({"name": name, "description": desc}, 'gauge') mt = metric.get('time', def_time) if 'value' in metric: value = metric['value'] elif 'sum' in metric: - if 'count' not in metric or metric['count'] != 1: - raise Exception('mock_connection only supports a count value of one') + # REVIEW + # if 'count' not in metric or metric['count'] != 1: + if 'count' not in metric: + raise Exception('mock_connection only supports a count value of one', metric) value = metric['sum'] else: raise Exception('md submit payload must provide value or sum/count attributes') @@ -133,9 +139,6 @@ def get_instrument(self, uri): else: return json.dumps(self.instruments[int(_id)]).encode('utf-8') - def __an_empty_list_metrics(self): - answer = {} - def create_alert(self, payload): self.last_a_id += 1 payload["id"] = self.last_a_id diff --git a/tests/test_aggregator.py b/tests/test_aggregator.py index e22662c..eb6592e 100644 --- a/tests/test_aggregator.py +++ b/tests/test_aggregator.py @@ -30,7 +30,7 @@ def test_constructor_tags(self): assert 'sky' in tags assert tags['sky'] == 'blue' - def test_add_tags(self): + def test_add(self): agg = Aggregator(self.conn, tags={'sky': 'blue'}) agg.add_tags({'sky': 'red', 'coal': 'black'}) tags = agg.get_tags() @@ -50,10 +50,6 @@ def test_set_tags(self): assert 'coal' in tags assert tags['coal'] == 'black' - def test_initialize_source(self): - assert Aggregator(self.conn).source is None - assert Aggregator(self.conn, source='my.source').source == 'my.source' - def test_initialize_period(self): assert Aggregator(self.conn).period is None assert Aggregator(self.conn, period=300).period == 300 @@ -103,35 +99,20 @@ def test_add_multiple_metrics(self): assert meas['min'] == 42 assert meas['max'] == 44 - # Only gauges are supported (not counters) def test_to_payload(self): - self.agg.source = 'mysource' - self.agg.add('test.metric', 42) - self.agg.add('test.metric', 43) - assert self.agg.to_payload() == { - 'gauges': [ - {'name': 'test.metric', 'count': 2, 'sum': 85, 'min': 42, 'max': 43} - ], - 'source': 'mysource' - } - assert 'gauges' in self.agg.to_payload() - assert 'counters' not in self.agg.to_payload() - - def test_to_payload_no_source(self): - self.agg.source = None - self.agg.add('test.metric', 42) - - assert self.agg.to_payload() == { - 'gauges': [ - { - 'name': 'test.metric', - 'count': 1, - 'sum': 42, - 'min': 42, - 'max': 42 - } - ] - } + a = Aggregator(self.conn, tags={'foo': 'bar'}) + a.add('test.metric', 42) + a.add('test.metric', 43) + print a.to_payload() + assert a.to_payload() == { + 'measurements': [{ + 'count': 2, + 'max': 43, + 'sum': 85, + 'name': 'test.metric', + 'min': 42}], + 'tags': {'foo': 'bar'} + } # If 'value' is specified in the payload, the API will throw an error # This is because it must be calculated at the API via sum/count=avg @@ -182,8 +163,8 @@ def test_measure_time_in_payload(self): self.agg.measure_time = mt self.agg.period = None self.agg.add("foo", 42) - assert 'measure_time' in self.agg.to_payload() - assert self.agg.to_payload()['measure_time'] == mt + assert 'time' in self.agg.to_payload() + assert self.agg.to_payload()['time'] == mt def test_measure_time_not_in_payload(self): self.agg.measure_time = None @@ -224,21 +205,7 @@ def test_floored_measure_time_in_payload(self): # This will occur only if period is set self.agg.measure_time = 1418838418 self.agg.period = 60 - assert self.agg.to_payload()['measure_time'] == 1418838360 - - def test_submit_side_by_side(self): - # Tagged and untagged measurements should be handled as separate - self.agg.add_tags({'hostname': 'web-1'}) - self.agg.add('test.metric', 42) - self.agg.add_tagged('test.metric', 10) - self.agg.submit() - - gauge = self.conn.get('test.metric', duration=60) - assert len(gauge.measurements['unassigned']) == 1 - - resp = self.conn.get_tagged('test.metric', duration=60, tags_search="hostname=web-1") - assert len(resp['series']) == 1 - + assert self.agg.to_payload()['time'] == 1418838360 if __name__ == '__main__': unittest.main() diff --git a/tests/test_charts.py b/tests/test_charts.py deleted file mode 100644 index 5ac0c79..0000000 --- a/tests/test_charts.py +++ /dev/null @@ -1,326 +0,0 @@ -import logging -import unittest -import librato -from librato import Space, Chart -from librato.streams import Stream -from mock_connection import MockConnect, server - -# logging.basicConfig(level=logging.DEBUG) -# Mock the server -librato.HTTPSConnection = MockConnect - - -class ChartsTest(unittest.TestCase): - def setUp(self): - self.conn = librato.connect('user_test', 'key_test') - server.clean() - - -# Charts -class TestChartsConnection(ChartsTest): - def setUp(self): - super(TestChartsConnection, self).setUp() - self.space = self.conn.create_space("My Space") - - def test_create_chart(self): - # Create a couple of metrics - self.conn.submit('my.metric', 42) - self.conn.submit('my.metric2', 43) - # Create charts in the space - chart_name = "Typical chart" - chart = self.conn.create_chart( - chart_name, - self.space, - streams=[ - {'metric': 'my.metric', 'source': '*', 'summary_function': 'max'}, - {'metric': 'my.metric2', 'source': 'foo', 'color': '#FFFFFF'} - ] - ) - self.assertIsInstance(chart, Chart) - self.assertIsNotNone(chart.id) - self.assertEqual(chart.space_id, self.space.id) - self.assertEqual(chart.name, chart_name) - self.assertEqual(chart.streams[0].metric, 'my.metric') - self.assertEqual(chart.streams[0].source, '*') - self.assertEqual(chart.streams[0].summary_function, 'max') - self.assertEqual(chart.streams[1].metric, 'my.metric2') - self.assertEqual(chart.streams[1].source, 'foo') - self.assertEqual(chart.streams[1].color, '#FFFFFF') - - def test_create_chart_without_streams(self): - chart_name = "Empty Chart" - chart = self.conn.create_chart(chart_name, self.space) - self.assertIsInstance(chart, Chart) - self.assertEqual(chart.name, chart_name) - # Line by default - self.assertEqual(chart.type, 'line') - self.assertEqual(len(chart.streams), 0) - - def test_rename_chart(self): - chart = self.conn.create_chart('CPU', self.space) - chart.rename('CPU 2') - self.assertEqual(chart.name, 'CPU 2') - self.assertEqual(self.conn.get_chart(chart.id, self.space).name, 'CPU 2') - - def test_delete_chart(self): - chart = self.conn.create_chart('cpu', self.space) - self.conn.delete_chart(chart.id, self.space.id) - self.assertEqual(len(self.conn.list_charts_in_space(self.space)), 0) - - def test_add_stream_to_chart(self): - chart = self.conn.create_chart("Chart with no streams", self.space) - metric_name = 'my.metric' - self.conn.submit(metric_name, 42, description='metric description') - chart.new_stream(metric=metric_name) - chart.save() - self.assertEqual(len(chart.streams), 1) - stream = chart.streams[0] - self.assertEqual(stream.metric, metric_name) - self.assertIsNone(stream.composite) - - def test_get_chart_from_space(self): - chart = self.conn.create_chart('cpu', self.space) - found = self.conn.get_chart(chart.id, self.space) - self.assertEqual(found.id, chart.id) - self.assertEqual(found.name, chart.name) - - def test_get_chart_from_space_id(self): - chart = self.conn.create_chart('cpu', self.space) - found = self.conn.get_chart(chart.id, self.space.id) - self.assertEqual(found.id, chart.id) - self.assertEqual(found.name, chart.name) - - def test_find_chart_by_name(self): - chart = self.conn.create_chart('cpu', self.space) - found = self.conn.find_chart('cpu', self.space) - self.assertEqual(found.name, 'cpu') - - -class TestChartModel(ChartsTest): - def setUp(self): - super(TestChartModel, self).setUp() - self.space = self.conn.create_space('My Space') - - def test_init_connection(self): - self.assertEqual(Chart(self.conn).connection, self.conn) - - def test_init_name(self): - self.assertIsNone(Chart(self.conn).name) - self.assertEqual(Chart(self.conn, 'cpu').name, 'cpu') - - def test_init_chart_type(self): - # Debated `chart_type` vs `type`, going with `type` - self.assertEqual(Chart(self.conn, type='line').type, 'line') - self.assertEqual(Chart(self.conn, type='stacked').type, 'stacked') - self.assertEqual(Chart(self.conn, type='bignumber').type, 'bignumber') - - def test_init_space_id(self): - self.assertEqual(Chart(self.conn, space_id=42).space_id, 42) - - def test_space_attribute(self): - chart = Chart(self.conn) - chart._space = self.space - self.assertEqual(chart._space, self.space) - - def test_init_streams(self): - self.assertEqual(Chart(self.conn).streams, []) - - s = [Stream('my.metric'), Stream('other.metric')] - chart = Chart(self.conn, streams=s) - self.assertEqual(chart.streams, s) - - def test_init_streams_dict(self): - streams_dict = [ - {'metric': 'my.metric', 'source': 'blah', 'composite': None}, - {'metric': 'other.metric', 'source': '*', 'composite': None} - ] - chart = Chart(self.conn, streams=streams_dict) - self.assertEqual(chart.streams[0].metric, streams_dict[0]['metric']) - self.assertEqual(chart.streams[0].source, streams_dict[0]['source']) - self.assertEqual(chart.streams[0].composite, streams_dict[0]['composite']) - self.assertEqual(chart.streams[1].metric, streams_dict[1]['metric']) - self.assertEqual(chart.streams[1].source, streams_dict[1]['source']) - self.assertEqual(chart.streams[1].composite, streams_dict[1]['composite']) - - def test_init_streams_list(self): - streams_list = [['my.metric', '*', None]] - chart = Chart(self.conn, streams=streams_list) - self.assertEqual(chart.streams[0].metric, streams_list[0][0]) - - def test_init_streams_group_functions(self): - streams_dict = [ - {'metric': 'my.metric', 'source': '*', - 'group_function': 'sum', 'summary_function': 'max'} - ] - chart = Chart(self.conn, streams=streams_dict) - stream = chart.streams[0] - self.assertEqual(stream.group_function, 'sum') - self.assertEqual(stream.summary_function, 'max') - - def test_init_min_max(self): - chart = Chart(self.conn, min=-42, max=100) - self.assertEqual(chart.min, -42) - self.assertEqual(chart.max, 100) - - def test_init_label(self): - chart = Chart(self.conn, label='I heart charts') - self.assertEqual(chart.label, 'I heart charts') - - def test_init_use_log_yaxis(self): - chart = Chart(self.conn, use_log_yaxis=True) - self.assertTrue(chart.use_log_yaxis) - - def test_save_chart(self): - chart = Chart(self.conn, 'test', space_id=self.space.id) - self.assertFalse(chart.persisted()) - self.assertIsNone(chart.id) - resp = chart.save() - self.assertIsInstance(resp, Chart) - self.assertTrue(chart.persisted()) - self.assertIsNotNone(chart.id) - self.assertEqual(chart.type, 'line') - - def test_save_persists_type(self): - # Ensure that type gets passed in the payload - for t in ['stacked', 'bignumber']: - chart = Chart(self.conn, space_id=self.space.id, type=t) - chart.save() - found = self.conn.get_chart(chart.id, self.space.id) - self.assertEqual(found.type, t) - - def test_save_persists_min_max(self): - chart = Chart(self.conn, space_id=self.space.id) - self.assertIsNone(chart.min) - self.assertIsNone(chart.max) - chart.min = 5 - chart.max = 30 - chart.save() - found = self.conn.get_chart(chart.id, self.space.id) - self.assertEqual(found.min, 5) - self.assertEqual(found.max, 30) - - def test_save_persists_label(self): - chart = Chart(self.conn, space_id=self.space.id) - self.assertIsNone(chart.label) - chart.label = 'my label' - chart.save() - found = self.conn.get_chart(chart.id, self.space.id) - self.assertEqual(found.label, 'my label') - - def test_save_persists_log_y_axis(self): - chart = Chart(self.conn, space_id=self.space.id) - self.assertIsNone(chart.use_log_yaxis) - chart.use_log_yaxis = True - chart.save() - found = self.conn.get_chart(chart.id, self.space.id) - self.assertTrue(found.use_log_yaxis) - - def test_save_persists_use_last_value(self): - chart = Chart(self.conn, space_id=self.space.id) - self.assertIsNone(chart.use_last_value) - chart.use_last_value = True - chart.save() - found = self.conn.get_chart(chart.id, self.space.id) - self.assertTrue(found.use_last_value) - - def test_save_persists_related_space(self): - chart = Chart(self.conn, space_id=self.space.id) - self.assertIsNone(chart.related_space) - chart.related_space = 1234 - chart.save() - found = self.conn.get_chart(chart.id, self.space.id) - self.assertTrue(found.related_space) - - def test_chart_is_not_persisted(self): - chart = Chart('not saved', self.space) - self.assertFalse(chart.persisted()) - - def test_chart_is_persisted_if_id_present(self): - chart = Chart(self.conn, 'test', id=42) - self.assertTrue(chart.persisted()) - chart = Chart(self.conn, 'test', id=None) - self.assertFalse(chart.persisted()) - - def test_get_space_from_chart(self): - chart = Chart(self.conn, space_id=self.space.id) - space = chart.space() - self.assertIsInstance(space, Space) - self.assertEqual(space.id, self.space.id) - - def test_new_stream_defaults(self): - chart = Chart(self.conn, 'test') - self.assertEqual(len(chart.streams), 0) - stream = chart.new_stream('my.metric') - self.assertIsInstance(stream, Stream) - self.assertEqual(stream.metric, 'my.metric') - self.assertEqual(stream.source, '*') - self.assertEqual(stream.composite, None) - # Appends to chart streams - self.assertEqual(len(chart.streams), 1) - self.assertEqual(chart.streams[0].metric, 'my.metric') - # Another way to do the same thing - stream = chart.new_stream(metric='my.metric') - self.assertEqual(stream.metric, 'my.metric') - - def test_new_stream_with_source(self): - chart = Chart(self.conn, 'test') - stream = chart.new_stream('my.metric', 'prod*') - self.assertEqual(stream.metric, 'my.metric') - self.assertEqual(stream.source, 'prod*') - self.assertEqual(stream.composite, None) - stream = chart.new_stream(metric='my.metric', source='prod*') - self.assertEqual(stream.metric, 'my.metric') - self.assertEqual(stream.source, 'prod*') - self.assertEqual(stream.composite, None) - - def test_new_stream_with_composite(self): - chart = Chart(self.conn) - composite_formula = 's("my.metric", "*")' - stream = chart.new_stream(composite=composite_formula) - self.assertIsNone(stream.metric) - self.assertIsNone(stream.source) - self.assertEqual(stream.composite, composite_formula) - - def test_get_payload(self): - chart = Chart(self.conn) - payload = chart.get_payload() - self.assertEqual(payload['name'], chart.name) - self.assertEqual(payload['type'], chart.type) - self.assertEqual(payload['streams'], chart.streams) - - def test_get_payload_bignumber(self): - streams = [{'metric': 'my.metric', 'source': '*'}] - chart = Chart(self.conn, type='bignumber', streams=streams, - use_last_value=False) - payload = chart.get_payload() - self.assertEqual(payload['name'], chart.name) - self.assertEqual(payload['type'], chart.type) - self.assertEqual(payload['streams'], streams) - self.assertEqual(payload['use_last_value'], chart.use_last_value) - - def test_streams_payload(self): - streams_payload = [ - {'metric': 'some.metric', 'source': None, 'composite': None}, - {'metric': None, 'source': None, 'composite': 's("other.metric", "sf", {function: "sum"})'} - ] - chart = Chart(self.conn, streams=streams_payload) - self.assertEqual(chart.streams_payload()[0]['metric'], streams_payload[0]['metric']) - - def test_get_payload_with_streams_dict(self): - streams_payload = [ - {'metric': 'some.metric', 'source': None, 'composite': None}, - {'metric': 'another.metric', 'source': None, 'composite': None} - ] - chart = Chart(self.conn, type='bignumber', space_id=42, streams=streams_payload) - chart_payload = chart.get_payload() - self.assertEqual(chart_payload['streams'][0]['metric'], streams_payload[0]['metric']) - self.assertEqual(chart_payload['streams'][1]['metric'], streams_payload[1]['metric']) - - def test_delete_chart(self): - chart = self.conn.create_chart('cpu', self.space) - chart.delete() - self.assertEqual(self.space.charts(), []) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_metrics.py b/tests/test_metrics.py index bd5232e..494569d 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -61,34 +61,13 @@ def test_list_metrics_adding_gauge(self): assert metrics[1].name == 'gauge_2' assert metrics[1].description == 'desc 2' - def test_list_metrics_adding_counter_metrics(self): - self.conn.submit('c1', 10, 'counter', description='counter desc 1') - self.conn.submit('c2', 20, 'counter', description='counter desc 2') - # Get all metrics - metrics = self.conn.list_metrics() - - assert len(metrics) == 2 - - assert isinstance(metrics[0], librato.metrics.Counter) - assert metrics[0].name == 'c1' - assert metrics[0].description == 'counter desc 1' - - assert isinstance(metrics[1], librato.metrics.Counter) - assert metrics[1].name == 'c2' - assert metrics[1].description == 'counter desc 2' - - def test_list_metrics_adding_one_counter_one_gauge(self): + def test_list_metrics_adding_one_gauge(self): self.conn.submit('gauge1', 10) - self.conn.submit('counter2', 20, type='counter', description="desc c2") # Get all metrics metrics = self.conn.list_metrics() assert isinstance(metrics[0], librato.metrics.Gauge) assert metrics[0].name == 'gauge1' - assert isinstance(metrics[1], librato.metrics.Counter) - assert metrics[1].name == 'counter2' - assert metrics[1].description == 'desc c2' - def test_deleting_a_gauge(self): self.conn.submit('test', 100) assert len(self.conn.list_metrics()) == 1 @@ -102,96 +81,30 @@ def test_deleting_a_batch_of_gauges(self): self.conn.delete(['test', 'test2']) assert len(self.conn.list_metrics()) == 0 - def test_deleting_a_counter(self): - self.conn.submit('test', 200, type='counter') - assert len(self.conn.list_metrics()) == 1 - self.conn.delete('test') - assert len(self.conn.list_metrics()) == 0 - def test_get_gauge_basic(self): - name, desc = '1', 'desc 1' - self.conn.submit(name, 10, description=desc) - gauge = self.conn.get(name) + name, desc, tags = 'my_metric', 'desc 1', {'city': 'austin'} + self.conn.submit(name, 10, description=desc, tags=tags) + gauge = self.conn.get_metric(name, duration=60) assert isinstance(gauge, librato.metrics.Gauge) assert gauge.name == name assert gauge.description == desc - assert len(gauge.measurements['unassigned']) == 1 - assert gauge.measurements['unassigned'][0]['value'] == 10 - - def test_get_counter_basic(self): - name, desc = 'counter1', 'count desc 1' - self.conn.submit(name, 20, type='counter', description=desc) - counter = self.conn.get(name) - assert isinstance(counter, librato.metrics.Counter) - assert counter.name == name - assert counter.description == desc - assert len(counter.measurements['unassigned']) == 1 - assert counter.measurements['unassigned'][0]['value'] == 20 - - def test_send_single_measurements_for_gauge_with_source(self): - name, desc, src = 'Test', 'A Test Gauge.', 'from_source' - self.conn.submit(name, 10, description=desc, source=src) - gauge = self.conn.get(name) - assert gauge.name == name - assert gauge.description == desc - assert len(gauge.measurements[src]) == 1 - assert gauge.measurements[src][0]['value'] == 10 - - def test_send_single_measurements_for_counter_with_source(self): - name, desc, src = 'Test', 'A Test Counter.', 'from_source' - self.conn.submit(name, 111, type='counter', description=desc, source=src) - counter = self.conn.get(name) - assert counter.name == name - assert counter.description == desc - assert len(counter.measurements[src]) == 1 - assert counter.measurements[src][0]['value'] == 111 - - def test_add_in_counter(self): - name, desc, src = 'Test', 'A Test Counter.', 'from_source' - self.conn.submit(name, 111, type='counter', description=desc, source=src) - counter = self.conn.get(name) - assert counter.name == name - assert counter.description == desc - assert len(counter.measurements[src]) == 1 - assert counter.measurements[src][0]['value'] == 111 - - counter.add(1, source=src) - - counter = self.conn.get(name) - assert counter.name == name - assert counter.description == desc - assert len(counter.measurements[src]) == 2 - assert counter.measurements[src][-1]['value'] == 1 - - def test_add_in_gauge(self): - name, desc, src = 'Test', 'A Test Gauge.', 'from_source' - self.conn.submit(name, 10, description=desc, source=src) - gauge = self.conn.get(name) - assert gauge.name == name - assert gauge.description == desc - assert len(gauge.measurements[src]) == 1 - assert gauge.measurements[src][0]['value'] == 10 - gauge.add(1, source=src) - - gauge = self.conn.get(name) - assert gauge.name == name - assert gauge.description == desc - assert len(gauge.measurements[src]) == 2 - assert gauge.measurements[src][-1]['value'] == 1 + data = self.conn.get(name, duration=60, tags=tags) + assert len(data['series']) == 1 + assert data['series'][0]['measurements'][0]['value'] == 10 - def test_md_submit(self): + def test_submit(self): mt1 = int(time.time()) - 5 tags = {'hostname': 'web-1'} - self.conn.submit_tagged('user_cpu', 20.2, time=mt1, tags=tags) + self.conn.submit('user_cpu', 20.2, time=mt1, tags=tags) - resp = self.conn.get_tagged('user_cpu', duration=60, tags_search="hostname=web-1") + resp = self.conn.get('user_cpu', duration=60, tags_search="hostname=web-1") assert len(resp['series']) == 1 assert resp['series'][0].get('tags', {}) == tags # Same query using tags param instead - resp = self.conn.get_tagged('user_cpu', duration=60, tags={'hostname': 'web-1'}) + resp = self.conn.get('user_cpu', duration=60, tags={'hostname': 'web-1'}) assert len(resp['series']) == 1 assert resp['series'][0].get('tags', {}) == tags @@ -206,11 +119,11 @@ def test_merge_tags(self): self.conn.set_tags({'company': 'Librato'}) tags = {'hostname': 'web-1'} - self.conn.submit_tagged('user_cpu', 20.2, time=mt1, tags=tags) + self.conn.submit('user_cpu', 20.2, time=mt1, tags=tags) # Ensure 'company' and 'hostname' tags made it through for tags_search in ["hostname=web-1", "company=Librato"]: - resp = self.conn.get_tagged('user_cpu', duration=60, tags_search=tags_search) + resp = self.conn.get('user_cpu', duration=60, tags_search=tags_search) assert len(resp['series']) == 1 @@ -220,22 +133,5 @@ def test_merge_tags(self): assert measurements[0]['time'] == mt1 assert measurements[0]['value'] == 20.2 - def test_submit_transparent_tagging(self): - mt1 = int(time.time()) - 5 - - tags = {'hostname': 'web-1'} - self.conn.submit('user_cpu', 20.2, time=mt1, tags=tags) - - resp = self.conn.get_tagged('user_cpu', duration=60, tags_search="hostname=web-1") - - assert len(resp['series']) == 1 - assert resp['series'][0].get('tags', {}) == tags - - measurements = resp['series'][0]['measurements'] - assert len(measurements) == 1 - - assert measurements[0]['time'] == mt1 - assert measurements[0]['value'] == 20.2 - if __name__ == '__main__': unittest.main() diff --git a/tests/test_queue.py b/tests/test_queue.py index cc43728..a93a4a1 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -30,11 +30,11 @@ def test_inherited_tags(self): assert conn.get_tags() == {'sky': 'blue'} q = conn.new_queue() - q.add_tagged('user_cpu', 10) + q.add('user_cpu', 10) q.submit() # Measurement must inherit 'sky' tag from connection - resp = self.conn.get_tagged('user_cpu', duration=60, tags_search="sky=blue") + resp = self.conn.get('user_cpu', duration=60, tags_search="sky=blue") assert len(resp['series']) == 1 assert resp['series'][0].get('tags', {}) == conn.get_tags() @@ -79,22 +79,13 @@ def test_set_tags(self): def test_single_measurement_gauge(self): q = self.q q.add('temperature', 22.1) - assert len(q.chunks) == 1 + assert len(q.tagged_chunks) == 1 assert q._num_measurements_in_current_chunk() == 1 def test_default_type_measurement(self): q = self.q q.add('temperature', 22.1) - assert len(q._current_chunk()['gauges']) == 1 - assert len(q._current_chunk()['counters']) == 0 - - def test_single_measurement_counter(self): - q = self.q - q.add('num_requests', 2000, type='counter') - assert len(q.chunks) == 1 - assert q._num_measurements_in_current_chunk() == 1 - assert len(q._current_chunk()['gauges']) == 0 - assert len(q._current_chunk()['counters']) == 1 + assert len(q._current_chunk()['measurements']) == 1 def test_num_metrics_in_queue(self): q = self.q @@ -104,7 +95,7 @@ def test_num_metrics_in_queue(self): assert q._num_measurements_in_queue() == 290 # Now ensure multiple chunks for _ in range(100): - q.add('num_requests', randint(100, 300), type='counter') + q.add('num_requests', randint(100, 300)) assert q._num_measurements_in_queue() == 390 def test_auto_submit_on_metric_count(self): @@ -123,111 +114,125 @@ def test_reach_chunk_limit(self): q = self.q for i in range(1, q.MAX_MEASUREMENTS_PER_CHUNK + 1): q.add('temperature', randint(20, 30)) - assert len(q.chunks) == 1 + assert len(q.tagged_chunks) == 1 assert q._num_measurements_in_current_chunk() == q.MAX_MEASUREMENTS_PER_CHUNK q.add('temperature', 40) # damn is pretty hot :) assert q._num_measurements_in_current_chunk() == 1 - assert len(q.chunks) == 2 + assert len(q.tagged_chunks) == 2 def test_submit_context_manager(self): + tags = {"host": "machine1"} try: with self.conn.new_queue() as q: - q.add('temperature', 32) + q.add('temperature', 32, tags=tags) raise ValueError except ValueError: - gauge = self.conn.get('temperature', resolution=1, count=2) - assert gauge.name == 'temperature' - assert gauge.description is None - assert len(gauge.measurements['unassigned']) == 1 + metric = self.conn.get_metric('temperature') + data = self.conn.get('temperature', resolution=1, count=2, duration=60, tags=tags) + assert metric.name == 'temperature' + assert metric.description is None + assert len(data['series'][0]['measurements']) == 1 def test_submit_one_measurement_batch_mode(self): + tags = {'city': 'barcelona'} q = self.q - q.add('temperature', 22.1) + q.add('temperature', 22.1, tags=tags) q.submit() metrics = self.conn.list_metrics() assert len(metrics) == 1 - gauge = self.conn.get('temperature', resolution=1, count=2) - assert gauge.name == 'temperature' - assert gauge.description is None - assert len(gauge.measurements['unassigned']) == 1 + metric = self.conn.get_metric('temperature', resolution=1, count=2) + assert metric.name == 'temperature' + assert metric.description is None + data = self.conn.get('temperature', resolution=1, count=2, duration=60, tags=tags) + assert len(data['series'][0]['measurements']) == 1 # Add another measurements for temperature - q.add('temperature', 23) + q.add('temperature', 23, tags=tags) q.submit() metrics = self.conn.list_metrics() assert len(metrics) == 1 - gauge = self.conn.get('temperature', resolution=1, count=2) - assert gauge.name == 'temperature' - assert gauge.description is None - assert len(gauge.measurements['unassigned']) == 2 - assert gauge.measurements['unassigned'][0]['value'] == 22.1 - assert gauge.measurements['unassigned'][1]['value'] == 23 + metric = self.conn.get_metric('temperature', resolution=1, count=2) + assert metric.name == 'temperature' + assert metric.description is None + data = self.conn.get('temperature', resolution=1, count=2, duration=60, tags=tags) + assert len(data['series'][0]['measurements']) == 2 + assert data['series'][0]['measurements'][0]['value'] == 22.1 + assert data['series'][0]['measurements'][1]['value'] == 23 def test_submit_tons_of_measurement_batch_mode(self): q = self.q metrics = self.conn.list_metrics() assert len(metrics) == 0 + tags = {'city': 'barcelona'} for t in range(1, q.MAX_MEASUREMENTS_PER_CHUNK + 1): - q.add('temperature', t) + q.add('temperature', t, tags=tags) q.submit() + metrics = self.conn.list_metrics() assert len(metrics) == 1 - gauge = self.conn.get('temperature', resolution=1, count=q.MAX_MEASUREMENTS_PER_CHUNK + 1) - assert gauge.name == 'temperature' - assert gauge.description is None + m = self.conn.get_metric('temperature', resolution=1, count=q.MAX_MEASUREMENTS_PER_CHUNK + 1) + assert m.name == 'temperature' + assert m.description is None + + data = self.conn.get('temperature', resolution=1, duration=60, tags=tags) + measurements = data['series'][0]['measurements'] for t in range(1, q.MAX_MEASUREMENTS_PER_CHUNK + 1): - assert gauge.measurements['unassigned'][t - 1]['value'] == t + assert measurements[t - 1]['value'] == t + tags = {'host': 'pollux'} for cl in range(1, q.MAX_MEASUREMENTS_PER_CHUNK + 1): - q.add('cpu_load', cl) + q.add('cpu_load', cl, tags=tags) q.submit() + metrics = self.conn.list_metrics() assert len(metrics) == 2 - gauge = self.conn.get('cpu_load', resolution=1, count=q.MAX_MEASUREMENTS_PER_CHUNK + 1) - assert gauge.name == 'cpu_load' - assert gauge.description is None + + m = self.conn.get_metric('cpu_load', resolution=1, count=q.MAX_MEASUREMENTS_PER_CHUNK + 1) + assert m.name == 'cpu_load' + assert m.description is None + + data = self.conn.get('cpu_load', resolution=1, duration=60, tags=tags) + measurements = data['series'][0]['measurements'] for t in range(1, q.MAX_MEASUREMENTS_PER_CHUNK + 1): - assert gauge.measurements['unassigned'][t - 1]['value'] == t + assert measurements[t - 1]['value'] == t def test_add_aggregator(self): q = self.q - metrics = self.conn.list_metrics() - a = Aggregator(self.conn, source='mysource', period=10) + tags = {'sky': 'blue'} + a = Aggregator(self.conn, tags=tags, measure_time=123) a.add('foo', 42) a.add('bar', 37) q.add_aggregator(a) - gauges = q.chunks[0]['gauges'] - names = [g['name'] for g in gauges] - - assert len(q.chunks) == 1 - + measurements = q.tagged_chunks[0]['measurements'] + names = [g['name'] for g in measurements] + assert len(q.tagged_chunks) == 1 assert 'foo' in names assert 'bar' in names - # All gauges should have the same source - assert gauges[0]['source'] == 'mysource' - assert gauges[1]['source'] == 'mysource' + assert measurements[0]['tags'] == tags + assert measurements[1]['tags'] == tags # All gauges should have the same measure_time - assert 'measure_time' in gauges[0] - assert 'measure_time' in gauges[1] + assert 'time' in measurements[0] + assert 'time' in measurements[1] - # Test that time was snapped to 10s - assert gauges[0]['measure_time'] % 10 == 0 + assert measurements[0]['time'] == 123 + assert measurements[1]['time'] == 123 - def test_md_submit(self): + def test_submit(self): q = self.q - q.set_tags({'hostname': 'web-1'}) + tags = {'hostname': 'web-1'} + q.set_tags(tags) mt1 = int(time.time()) - 5 - q.add_tagged('system_cpu', 3.2, time=mt1) + q.add('system_cpu', 3.2, time=mt1) assert q._num_measurements_in_queue() == 1 q.submit() - resp = self.conn.get_tagged('system_cpu', duration=60, tags_search="hostname=web-1") + resp = self.conn.get('system_cpu', duration=60, tags_search="hostname=web-1") assert len(resp['series']) == 1 assert resp['series'][0].get('tags', {}) == {'hostname': 'web-1'} @@ -238,17 +243,17 @@ def test_md_submit(self): assert measurements[0]['time'] == mt1 assert measurements[0]['value'] == 3.2 - def test_md_measurement_level_tag(self): + def test_measurement_level_tag(self): q = self.q q.set_tags({'hostname': 'web-1'}) mt1 = int(time.time()) - 5 - q.add_tagged('system_cpu', 33.22, time=mt1, tags={"user": "james"}) + q.add('system_cpu', 33.22, time=mt1, tags={"user": "james"}) q.submit() # Ensure both tags get submitted for tag_search in ["hostname=web-1", "user=james"]: - resp = self.conn.get_tagged('system_cpu', duration=60, tags_search=tag_search) + resp = self.conn.get('system_cpu', duration=60, tags_search=tag_search) assert len(resp['series']) == 1 @@ -263,14 +268,14 @@ def test_md_measurement_level_tag_supersedes(self): q.set_tags({'hostname': 'web-1'}) mt1 = int(time.time()) - 5 - q.add_tagged('system_cpu', 33.22, time=mt1, tags={"hostname": "web-2"}) + q.add('system_cpu', 33.22, time=mt1, tags={"hostname": "web-2"}) q.submit() # Ensure measurement-level tag takes precedence - resp = self.conn.get_tagged('system_cpu', duration=60, tags_search="hostname=web-1") + resp = self.conn.get('system_cpu', duration=60, tags_search="hostname=web-1") assert len(resp['series']) == 0 - resp = self.conn.get_tagged('system_cpu', duration=60, tags_search="hostname=web-2") + resp = self.conn.get('system_cpu', duration=60, tags_search="hostname=web-2") assert len(resp['series']) == 1 measurements = resp['series'][0]['measurements'] @@ -279,64 +284,23 @@ def test_md_measurement_level_tag_supersedes(self): assert measurements[0]['time'] == mt1 assert measurements[0]['value'] == 33.22 - def test_side_by_side(self): - # Ensure tagged and untagged measurements are handled independently - q = self.conn.new_queue(tags={'hostname': 'web-1'}) - - q.add('system_cpu', 10) - q.add_tagged('system_cpu', 20) - q.submit() - - resp = self.conn.get_tagged('system_cpu', duration=60, tags_search="hostname=web-1") - assert len(resp['series']) == 1 - - measurements = resp['series'][0]['measurements'] - assert len(measurements) == 2 - assert measurements[0]['value'] == 10 - assert measurements[1]['value'] == 20 - - def test_md_auto_submit_on_metric_count(self): + def test_auto_submit_on_metric_count_2(self): q = self.conn.new_queue(auto_submit_count=2) - q.add('untagged_cpu', 10) - q.add_tagged('tagged_cpu', 20, tags={'hostname': 'web-2'}) + q.add('tagged_cpu', 10, tags={'hostname': 'web-2'}) + q.add('tagged_cpu', 20, tags={'hostname': 'web-2'}) - assert q._num_measurements_in_queue() == 0 - - gauge = self.conn.get('untagged_cpu', duration=60) - assert len(gauge.measurements['unassigned']) == 1 - - resp = self.conn.get_tagged('tagged_cpu', duration=60, tags_search="hostname=web-2") - assert len(resp['series']) == 1 + resp = self.conn.get('tagged_cpu', duration=60, tags_search="hostname=web-2") + assert len(resp['series'][0]['measurements']) == 2 - def queue_send_as_md_when_queue_has_tags(self): + def queue_send_as_when_queue_has_tags(self): q = self.conn.new_queue(tags={'foo': 1}) q.add('a_metric', 10) assert q._num_measurements_in_queue() == 1 - resp = self.conn.get_tagged('a_metric', duration=60, tags_search="foo=1") + resp = self.conn.get('a_metric', duration=60, tags_search="foo=1") assert len(resp['series']) == 1 - def test_transparent_submit_md(self): - q = self.q - tags = {'hostname': 'web-1'} - - mt1 = int(time.time()) - 5 - q.add('system_cpu', 3.2, time=mt1, tags=tags) - assert q._num_measurements_in_queue() == 1 - q.submit() - - resp = self.conn.get_tagged('system_cpu', duration=60, tags_search="hostname=web-1") - - assert len(resp['series']) == 1 - assert resp['series'][0].get('tags', {}) == {'hostname': 'web-1'} - - measurements = resp['series'][0]['measurements'] - assert len(measurements) == 1 - - assert measurements[0]['time'] == mt1 - assert measurements[0]['value'] == 3.2 - if __name__ == '__main__': unittest.main() diff --git a/tests/test_spaces.py b/tests/test_spaces.py deleted file mode 100644 index 7bd9941..0000000 --- a/tests/test_spaces.py +++ /dev/null @@ -1,333 +0,0 @@ -import logging -import unittest -import librato -from librato import Space, Chart -from librato.streams import Stream -from mock_connection import MockConnect, server - -# logging.basicConfig(level=logging.DEBUG) -# Mock the server -librato.HTTPSConnection = MockConnect - - -class SpacesTest(unittest.TestCase): - def setUp(self): - self.conn = librato.connect('user_test', 'key_test') - server.clean() - - -class TestSpacesConnection(SpacesTest): - def test_list_spaces(self): - self.conn.create_space('My Space') - self.assertEqual(len(self.conn.list_spaces()), 1) - - def test_list_spaces_when_none(self): - spcs = self.conn.list_spaces() - self.assertEqual(len(spcs), 0) - - # Find a space by ID - def test_get_space(self): - space = self.conn.create_space('My Space') - found = self.conn.get_space(space.id) - self.assertEqual(found.id, space.id) - self.assertEqual(found.name, space.name) - - # Find a space by name - def test_find_space(self): - space = self.conn.create_space('My Space') - found = self.conn.find_space(space.name) - self.assertIsInstance(found, Space) - self.assertEqual(found.name, space.name) - - def test_create_space(self): - name = "My Space" - space = self.conn.create_space(name) - self.assertIsInstance(space, Space) - self.assertEqual(space.name, name) - - def test_rename_space(self): - name = 'My Space' - new_name = 'My Space 42' - space = self.conn.create_space(name) - space.rename(new_name) - found = self.conn.find_space(new_name) - self.assertEqual(found.name, new_name) - - def test_list_charts(self): - space_name = "My Space" - space = self.conn.create_space(space_name) - chart1 = self.conn.create_chart('CPU 1', space) - chart2 = self.conn.create_chart('CPU 2', space) - charts = space.charts() - for c in charts: - self.assertIsInstance(c, Chart) - self.assertIn(c.name, ['CPU 1', 'CPU 2']) - - def test_update_space(self): - space = self.conn.create_space('My Space') - self.conn.update_space(space, name='New Name') - - updated_spaces = self.conn.list_spaces() - self.assertEqual(len(updated_spaces), 1) - - updated = updated_spaces[0] - updated = self.conn.get_space(space.id) - self.assertEqual(updated.id, space.id) - self.assertEqual(updated.name, 'New Name') - - def test_delete_chart(self): - space = self.conn.create_space('My Space') - chart = self.conn.create_chart('My Chart', space) - self.conn.delete_chart(chart.id, space.id) - self.assertEqual(len(self.conn.list_charts_in_space(space)), 0) - - def test_delete_space(self): - space = self.conn.create_space('My Space') - self.conn.delete_space(space.id) - self.assertEqual(len(self.conn.list_spaces()), 0) - - -class TestSpaceModel(SpacesTest): - def setUp(self): - super(TestSpaceModel, self).setUp() - self.space = Space(self.conn, 'My Space', id=123) - - def test_connection(self): - self.assertEqual(Space(self.conn, 'My Space').connection, self.conn) - - def test_init_with_name(self): - self.assertEqual(Space(self.conn, 'My Space').name, 'My Space') - - def test_init_with_tags(self): - self.assertFalse(Space(self.conn, 'My Space').tags) - self.assertFalse(Space(self.conn, 'My Space', tags=False).tags) - self.assertTrue(Space(self.conn, 'My Space', tags=True).tags) - - def test_init_with_empty_name(self): - self.assertEqual(Space(self.conn, None).name, None) - - def test_init_with_id(self): - self.assertEqual(Space(self.conn, 'My Space', 123).id, 123) - - def test_charts_var(self): - self.assertEqual(self.space._charts, None) - - def test_init_chart_ids_empty(self): - self.assertEqual(self.space.chart_ids, []) - - def test_init_with_chart_payload(self): - space = Space(self.conn, 'My Space', chart_dicts=[{'id': 123}, {'id': 456}]) - self.assertEqual(space.chart_ids, [123, 456]) - - def test_space_is_not_persisted(self): - space = Space(self.conn, 'not saved') - self.assertFalse(space.persisted()) - - def test_space_is_persisted_if_id_present(self): - space = Space(self.conn, 'saved', id=42) - self.assertTrue(space.persisted()) - - # This only returns the name because that's all we can send to the Spaces API - def test_get_payload(self): - self.assertEqual(self.space.get_payload(), {'name': self.space.name}) - - def test_from_dict(self): - payload = {'id': 42, 'name': 'test', 'charts': [{'id': 123}, {'id': 456}]} - space = Space.from_dict(self.conn, payload) - self.assertIsInstance(space, Space) - self.assertEqual(space.id, 42) - self.assertEqual(space.name, 'test') - self.assertEqual(space.chart_ids, [123, 456]) - - def test_save_creates_space(self): - space = Space(self.conn, 'not saved') - self.assertFalse(space.persisted()) - resp = space.save() - self.assertIsInstance(resp, Space) - self.assertTrue(space.persisted()) - - def save_updates_space(self): - space = Space(self.conn, 'some name').save() - self.assertEqual(space.name, 'some name') - space.name = 'new name' - space.save() - self.assertEqual(self.conn.find_space('new_name').name, 'new name') - - def test_new_chart_name(self): - chart = self.space.new_chart('test') - self.assertIsInstance(chart, Chart) - self.assertEqual(chart.name, 'test') - - def test_new_chart_not_persisted(self): - # Doesn't save - self.assertFalse(self.space.new_chart('test').persisted()) - - def test_new_chart_type(self): - chart = self.space.new_chart('test') - self.assertEqual(chart.type, 'line') - chart = self.space.new_chart('test', type='stacked') - self.assertEqual(chart.type, 'stacked') - chart = self.space.new_chart('test', type='bignumber') - self.assertEqual(chart.type, 'bignumber') - - def test_new_chart_attrs(self): - chart = self.space.new_chart('test', - label='hello', - min=-5, - max=30, - use_log_yaxis=True, - use_last_value=True, - related_space=1234) - self.assertEqual(chart.label, 'hello') - self.assertEqual(chart.min, -5) - self.assertEqual(chart.max, 30) - self.assertTrue(chart.use_log_yaxis) - self.assertTrue(chart.use_last_value) - self.assertEqual(chart.related_space, 1234) - - def test_new_chart_bignumber(self): - chart = self.space.new_chart('test', type='bignumber', - use_last_value=False) - self.assertEqual(chart.type, 'bignumber') - self.assertFalse(chart.use_last_value) - - def test_add_chart_name(self): - space = self.conn.create_space('foo') - chart = space.add_chart('bar') - self.assertIsInstance(chart, Chart) - self.assertEqual(chart.name, 'bar') - - def test_add_chart_type(self): - space = self.conn.create_space('foo') - chart = space.add_chart('baz', type='stacked') - self.assertEqual(chart.type, 'stacked') - - def test_add_chart_persisted(self): - space = self.conn.create_space('foo') - chart = space.add_chart('bar') - # Does save - self.assertTrue(chart.persisted()) - - def test_add_chart_streams(self): - space = self.conn.create_space('foo') - streams = [ - {'metric': 'my.metric', 'source': 'foo'}, - {'metric': 'my.metric2', 'source': 'bar'} - ] - chart = space.add_chart('cpu', streams=streams) - self.assertEqual(chart.streams[0].metric, 'my.metric') - self.assertEqual(chart.streams[0].source, 'foo') - self.assertEqual(chart.streams[1].metric, 'my.metric2') - self.assertEqual(chart.streams[1].source, 'bar') - - def test_add_chart_bignumber_default(self): - space = self.conn.create_space('foo') - chart = space.add_chart('baz', type='bignumber') - self.assertEqual(chart.type, 'bignumber') - # Leave this up to the Librato API to default - self.assertIsNone(chart.use_last_value) - - def test_add_chart_bignumber_use_last_value(self): - space = self.conn.create_space('foo') - chart = space.add_chart('baz', type='bignumber', use_last_value=False) - self.assertFalse(chart.use_last_value) - chart = space.add_chart('baz', type='bignumber', use_last_value=True) - self.assertTrue(chart.use_last_value) - - def test_add_line_chart(self): - space = self.conn.create_space('foo') - streams = [{'metric': 'my.metric', 'source': 'my.source'}] - chart = space.add_line_chart('cpu', streams=streams) - self.assertEqual([chart.name, chart.type], ['cpu', 'line']) - self.assertEqual(len(chart.streams), 1) - - def test_add_single_line_chart_default(self): - space = self.conn.create_space('foo') - chart = space.add_single_line_chart('cpu', 'my.cpu.metric') - self.assertEqual(chart.type, 'line') - self.assertEqual(chart.name, 'cpu') - self.assertEqual(len(chart.streams), 1) - self.assertEqual(chart.streams[0].metric, 'my.cpu.metric') - self.assertEqual(chart.streams[0].source, '*') - - def test_add_single_line_chart_source(self): - space = self.conn.create_space('foo') - chart = space.add_single_line_chart('cpu', 'my.cpu.metric', 'prod*') - self.assertEqual(chart.streams[0].source, 'prod*') - - def test_add_single_line_chart_group_functions(self): - space = self.conn.create_space('foo') - chart = space.add_single_line_chart('cpu', 'my.cpu.metric', '*', 'min', 'max') - stream = chart.streams[0] - self.assertEqual(stream.group_function, 'min') - self.assertEqual(stream.summary_function, 'max') - - def test_add_stacked_chart(self): - space = self.conn.create_space('foo') - streams = [{'metric': 'my.metric', 'source': 'my.source'}] - chart = space.add_stacked_chart('cpu', streams=streams) - self.assertEqual(chart.type, 'stacked') - self.assertEqual([chart.name, chart.type], ['cpu', 'stacked']) - self.assertEqual(len(chart.streams), 1) - - def test_add_single_stacked_chart(self): - space = self.conn.create_space('foo') - chart = space.add_single_stacked_chart('cpu', 'my.cpu.metric', '*') - self.assertEqual(chart.type, 'stacked') - self.assertEqual(chart.name, 'cpu') - self.assertEqual(len(chart.streams), 1) - self.assertEqual(chart.streams[0].metric, 'my.cpu.metric') - self.assertEqual(chart.streams[0].source, '*') - - def test_add_bignumber_chart_default(self): - space = self.conn.create_space('foo') - chart = space.add_bignumber_chart('cpu', 'my.metric') - self.assertEqual(chart.type, 'bignumber') - self.assertEqual(chart.name, 'cpu') - self.assertTrue(chart.use_last_value) - stream = chart.streams[0] - self.assertEqual(stream.metric, 'my.metric') - self.assertEqual(stream.source, '*') - self.assertEqual(stream.summary_function, 'average') - - def test_add_bignumber_chart_source(self): - space = self.conn.create_space('foo') - chart = space.add_bignumber_chart('cpu', 'my.metric', 'foo') - self.assertEqual(chart.streams[0].source, 'foo') - - def test_add_bignumber_chart_summary_function(self): - space = self.conn.create_space('foo') - chart = space.add_bignumber_chart('cpu', 'my.metric', - summary_function='min') - self.assertEqual(chart.streams[0].summary_function, 'min') - - def test_add_bignumber_chart_group_function(self): - space = self.conn.create_space('foo') - chart = space.add_bignumber_chart('cpu', 'my.metric', - group_function='max') - self.assertEqual(chart.streams[0].group_function, 'max') - - def test_add_bignumber_chart_use_last_value(self): - space = self.conn.create_space('foo') - # True shows the most recent value, False reduces over time - # Default to True - chart = space.add_bignumber_chart('cpu', 'my.metric') - self.assertTrue(chart.use_last_value) - chart = space.add_bignumber_chart('cpu', 'my.metric', use_last_value=True) - self.assertTrue(chart.use_last_value) - chart = space.add_bignumber_chart('cpu', 'my.metric', use_last_value=False) - self.assertFalse(chart.use_last_value) - - def test_delete_space(self): - space = self.conn.create_space('Delete Me') - # Ensure we can find it - self.assertEqual(self.conn.find_space(space.name).name, space.name) - resp = space.delete() - # Returns None - self.assertIsNone(resp) - # Ensure it was deleted - self.assertIsNone(self.conn.find_space(space.name)) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_streams.py b/tests/test_streams.py deleted file mode 100644 index 64a177d..0000000 --- a/tests/test_streams.py +++ /dev/null @@ -1,128 +0,0 @@ -import logging -import unittest -import librato -from librato.streams import Stream -from mock_connection import MockConnect, server - -# logging.basicConfig(level=logging.DEBUG) -# Mock the server -librato.HTTPSConnection = MockConnect - - -class TestStreamModel(unittest.TestCase): - def setUp(self): - self.conn = librato.connect('user_test', 'key_test') - server.clean() - - def test_init_metric(self): - self.assertEqual(Stream('my.metric').metric, 'my.metric') - self.assertEqual(Stream(metric='my.metric').metric, 'my.metric') - - def test_init_source(self): - self.assertEqual(Stream('my.metric', 'my.source').source, 'my.source') - self.assertEqual(Stream(source='my.source').source, 'my.source') - - def test_init_composite(self): - composite_formula = 's("my.metric", "*")' - self.assertEqual(Stream(composite=composite_formula).composite, composite_formula) - - def test_source_defaults_to_all(self): - self.assertEqual(Stream('my.metric').source, '*') - - def test_composite_defaults_to_none(self): - self.assertIsNone(Stream('my.metric').composite) - - def test_init_group_function(self): - self.assertIsNone(Stream('my.metric').group_function) - self.assertEqual(Stream(group_function='max').group_function, 'max') - - def test_init_summary_function(self): - self.assertIsNone(Stream('my.metric').summary_function) - self.assertEqual(Stream(summary_function='min').summary_function, 'min') - - def test_init_transform_function(self): - self.assertIsNone(Stream('my.metric').transform_function) - self.assertEqual(Stream(transform_function='x/p*60').transform_function, 'x/p*60') - - # For composites ONLY - def test_init_downsample_function(self): - self.assertIsNone(Stream('my.metric').downsample_function) - self.assertEqual(Stream(downsample_function='sum').downsample_function, 'sum') - - def test_init_period(self): - self.assertIsNone(Stream('my.metric').period) - self.assertEqual(Stream(period=60).period, 60) - - # Not very useful but part of the API - def test_init_type(self): - self.assertIsNone(Stream().type) - self.assertEqual(Stream(type='gauge').type, 'gauge') - - def test_init_split_axis(self): - self.assertIsNone(Stream().split_axis) - self.assertTrue(Stream(split_axis=True).split_axis) - self.assertFalse(Stream(split_axis=False).split_axis) - - def test_init_min_max(self): - self.assertIsNone(Stream().min) - self.assertIsNone(Stream().max) - self.assertEqual(Stream(min=-2).min, -2) - self.assertEqual(Stream(max=42).max, 42) - - def test_init_units(self): - self.assertIsNone(Stream().units_short) - self.assertIsNone(Stream().units_long) - self.assertEqual(Stream(units_short='req/s').units_short, 'req/s') - self.assertEqual(Stream(units_long='requests per second').units_long, 'requests per second') - - def test_init_color(self): - self.assertIsNone(Stream().color) - self.assertEqual(Stream(color='#f00').color, '#f00') - - def test_init_gap_detection(self): - self.assertIsNone(Stream().gap_detection) - self.assertTrue(Stream(gap_detection=True).gap_detection) - self.assertFalse(Stream(gap_detection=False).gap_detection) - - # Adding this to avoid exceptions raised due to unknown Stream attributes - def test_init_with_extra_attributes(self): - attrs = {"color": "#f00", "something": "foo"} - s = Stream(**attrs) - # color is a known attribute - self.assertEqual(s.color, '#f00') - self.assertEqual(s.something, 'foo') - - def test_get_payload(self): - self.assertEqual(Stream(metric='my.metric').get_payload(), - {'metric': 'my.metric', 'source': '*'}) - - def test_payload_all_attributes(self): - s = Stream(metric='my.metric', source='*', name='my display name', - type='gauge', id=1234, - group_function='min', summary_function='max', - transform_function='x/p', downsample_function='min', - period=60, split_axis=False, - min=0, max=42, - units_short='req/s', units_long='requests per second') - payload = { - 'metric': 'my.metric', - 'source': '*', - 'name': 'my display name', - 'type': 'gauge', - 'id': 1234, - 'group_function': 'min', - 'summary_function': 'max', - 'transform_function': 'x/p', - 'downsample_function': 'min', - 'period': 60, - 'split_axis': False, - 'min': 0, - 'max': 42, - 'units_short': 'req/s', - 'units_long': 'requests per second' - } - self.assertEqual(s.get_payload(), payload) - - -if __name__ == '__main__': - unittest.main()