diff --git a/redash/query_runner/prometheus.py b/redash/query_runner/prometheus.py index b48c5269c..c7bbbc143 100644 --- a/redash/query_runner/prometheus.py +++ b/redash/query_runner/prometheus.py @@ -1,10 +1,59 @@ import requests -import datetime +from datetime import datetime +from urlparse import parse_qs from redash.query_runner import BaseQueryRunner, register, TYPE_DATETIME, TYPE_STRING from redash.utils import json_dumps +def get_instant_rows(metrics_data): + rows = [] + + for metric in metrics_data: + row_data = metric['metric'] + + timestamp, value = metric['value'] + date_time = datetime.fromtimestamp(timestamp) + + row_data.update({"timestamp": date_time, "value": value}) + rows.append(row_data) + return rows + + +def get_range_rows(metrics_data): + rows = [] + + for metric in metrics_data: + ts_values = metric['values'] + metric_labels = metric['metric'] + + for values in ts_values: + row_data = metric_labels.copy() + + timestamp, value = values + date_time = datetime.fromtimestamp(timestamp) + + row_data.update({'timestamp': date_time, 'value': value}) + rows.append(row_data) + return rows + + class Prometheus(BaseQueryRunner): + def __init__(self, configuration): + self.columns = [ + { + 'friendly_name': 'timestamp', + 'type': TYPE_DATETIME, + 'name': 'timestamp' + }, + { + 'friendly_name': 'value', + 'type': TYPE_STRING, + 'name': 'value' + }, + ] + + super(Prometheus, self).__init__(configuration) + @classmethod def configuration_schema(cls): return { @@ -40,52 +89,56 @@ class Prometheus(BaseQueryRunner): return schema.values() def run_query(self, query, user): + """ + query syntax, it is the actual url query string. + check the prometheus http API for the detail of supported query string. + + https://prometheus.io/docs/prometheus/latest/querying/api/ + + example: instant query + query=http_requests_total + + example: range query + query=http_requests_total&start=2018-01-20T00:00:00.000Z&end=2018-01-25T00:00:00.000Z&step=60s + """ + base_url = self.configuration["url"] try: error = None query = query.strip() + # for backward compatibility + query = 'query={}'.format(query) if not query.startswith('query=') else query - local_query = '/api/v1/query' - url = base_url + local_query - payload = {'query': query} - response = requests.get(url, params=payload) + payload = parse_qs(query) + query_type = 'query_range' if 'step' in payload.keys() else 'query' + api_endpoint = base_url + '/api/v1/{}'.format(query_type) + + response = requests.get(api_endpoint, params=payload) response.raise_for_status() - raw_data = response.json()['data']['result'] - columns = [ - { - 'friendly_name': 'timestamp', - 'type': TYPE_DATETIME, - 'name': 'timestamp' - }, - { - 'friendly_name': 'value', - 'type': TYPE_STRING, - 'name': 'value' - }, - ] - columns_name = raw_data[0]['metric'].keys() - for column_name in columns_name: - columns.append({ - 'friendly_name': column_name, - 'type': TYPE_STRING, - 'name': column_name - }) - rows = [] - for row in raw_data: - h = {} - for r in row['metric']: - h[r] = row['metric'][r] - h['value'] = row['value'][1] - h['timestamp'] = datetime.datetime.fromtimestamp(row['value'][0]) - rows.append(h) + metrics = response.json()['data']['result'] + + if len(metrics) == 0: + return None, 'query result is empty.' + + metric_labels = metrics[0]['metric'].keys() + + for label_name in metric_labels: + self.columns.append({ + 'friendly_name': label_name, + 'type': TYPE_STRING, + 'name': label_name + }) + + rows = get_range_rows(metrics) if query_type == 'query_range' else get_instant_rows(metrics) json_data = json_dumps( { 'rows': rows, - 'columns': columns + 'columns': self.columns } ) + except requests.RequestException as e: return None, str(e) except KeyboardInterrupt: diff --git a/redash/query_runner/python.py b/redash/query_runner/python.py index eb5ea33e8..f8e69f96c 100644 --- a/redash/query_runner/python.py +++ b/redash/query_runner/python.py @@ -55,8 +55,8 @@ class Python(BaseQueryRunner): 'type': 'string', 'title': 'Modules to import prior to running the script' }, - 'additionalModulesPaths' : { - 'type' : 'string' + 'additionalModulesPaths': { + 'type': 'string' } }, } diff --git a/tests/query_runner/test_prometheus.py b/tests/query_runner/test_prometheus.py new file mode 100644 index 000000000..32cec3fe9 --- /dev/null +++ b/tests/query_runner/test_prometheus.py @@ -0,0 +1,98 @@ +import datetime +import json +from unittest import TestCase +from redash.query_runner.prometheus import get_instant_rows, get_range_rows + + +class TestPrometheus(TestCase): + def setUp(self): + self.instant_query_result = [ + { + "metric": { + "name": "example_metric_name", + "foo_bar": "foo", + }, + "value": [1516937400.781, "7400_foo"] + }, + { + "metric": { + "name": "example_metric_name", + "foo_bar": "bar", + }, + "value": [1516937400.781, "7400_bar"] + } + ] + + self.range_query_result = [ + { + "metric": { + "name": "example_metric_name", + "foo_bar": "foo", + }, + "values": [ + [1516937400.781, "7400_foo"], + [1516938000.781, "8000_foo"], + ] + }, + { + "metric": { + "name": "example_metric_name", + "foo_bar": "bar", + }, + "values": [ + [1516937400.781, "7400_bar"], + [1516938000.781, "8000_bar"], + ] + } + ] + + def test_get_instant_rows(self): + instant_rows = [ + { + "name": "example_metric_name", + "foo_bar": "foo", + "timestamp": datetime.datetime.fromtimestamp(1516937400.781), + "value": "7400_foo" + }, + { + "name": "example_metric_name", + "foo_bar": "bar", + "timestamp": datetime.datetime.fromtimestamp(1516937400.781), + "value": "7400_bar" + }, + ] + + rows = get_instant_rows(self.instant_query_result) + self.assertEqual(instant_rows, rows) + + def test_get_range_rows(self): + + range_rows = [ + { + "name": "example_metric_name", + "foo_bar": "foo", + "timestamp": datetime.datetime.fromtimestamp(1516937400.781), + "value": "7400_foo" + }, + { + "name": "example_metric_name", + "foo_bar": "foo", + "timestamp": datetime.datetime.fromtimestamp(1516938000.781), + "value": "8000_foo" + }, + { + "name": "example_metric_name", + "foo_bar": "bar", + "timestamp": datetime.datetime.fromtimestamp(1516937400.781), + "value": "7400_bar" + }, + { + "name": "example_metric_name", + "foo_bar": "bar", + "timestamp": datetime.datetime.fromtimestamp(1516938000.781), + "value": "8000_bar" + }, + ] + + rows = get_range_rows(self.range_query_result) + self.assertEqual(range_rows, rows)