From ce855a97de14fc64ce2f78aa2c6e2b41041978e0 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Fri, 15 Sep 2023 19:34:04 +0200 Subject: [PATCH] Data source query improvements: Adjust software tests --- grafana_client/elements/datasource.py | 18 +- grafana_client/knowledge.py | 7 +- test/elements/test_datasource_base.py | 46 +++- test/elements/test_datasource_fixtures.py | 3 + test/elements/test_datasource_health.py | 279 +++++++++++++++------- 5 files changed, 259 insertions(+), 94 deletions(-) diff --git a/grafana_client/elements/datasource.py b/grafana_client/elements/datasource.py index bc09363..043ba34 100644 --- a/grafana_client/elements/datasource.py +++ b/grafana_client/elements/datasource.py @@ -16,6 +16,7 @@ logger = logging.getLogger(__name__) VERSION_9 = LooseVersion("9") +VERSION_8 = LooseVersion("8") VERSION_7 = LooseVersion("7") VERBOSE = False @@ -394,6 +395,7 @@ def smartquery( request["data"]["to"], request["data"]["step"], ) + # For all others, use the generic data source communication endpoint. elif access_type in ["server", "proxy"]: url = "/ds/query" @@ -438,6 +440,7 @@ def health_check(self, datasource: Union[DatasourceIdentifier, Dict]) -> Datasou expression = get_healthcheck_expression(datasource_type, datasource_dialect) start = time.time() + message = "Unknown error" try: response = self.smartquery(datasource, expression) response_display = response @@ -487,15 +490,16 @@ def health_check(self, datasource: Union[DatasourceIdentifier, Dict]) -> Datasou message = f"Invalid response. {reason}" elif datasource_type == "loki": - if self.api.version: - if LooseVersion(self.api.version) == VERSION_7: - if "status" in response and response["status"] == "success": - message = "Success" - success = True - elif "results" in response and "test" in response["results"]: + if self.api.version and VERSION_7 <= LooseVersion(self.api.version) < VERSION_8: + if "status" in response and response["status"] == "success": message = "Success" success = True - else: + else: + message = response.get("message", "Unknown error") + elif "results" in response and "test" in response["results"]: + message = "Success" + success = True + elif "message" in response: message = response["message"] # With OpenTSDB, a 200 OK response with empty body is just fine. diff --git a/grafana_client/knowledge.py b/grafana_client/knowledge.py index d64a2ae..0a24582 100644 --- a/grafana_client/knowledge.py +++ b/grafana_client/knowledge.py @@ -115,7 +115,7 @@ def datasource_factory(datasource: DatasourceModel) -> DatasourceModel: return datasource -def query_factory(datasource, model: Optional[dict]) -> Union[Dict, str]: +def query_factory(datasource, model: Optional[dict] = None, expression: Optional[str] = None) -> Union[Dict, str]: """ Create payload suitable for running a query against a Grafana data source. @@ -124,6 +124,11 @@ def query_factory(datasource, model: Optional[dict]) -> Union[Dict, str]: TODO: Complete the list for all popular databases. """ + + model = model or {} + if "query" not in model and expression: + model["query"] = expression + request = { "method": "POST", "data": None, diff --git a/test/elements/test_datasource_base.py b/test/elements/test_datasource_base.py index 814bfde..8c69bd6 100644 --- a/test/elements/test_datasource_base.py +++ b/test/elements/test_datasource_base.py @@ -1,5 +1,6 @@ import unittest from test.elements.test_datasource_fixtures import ( + DATAFRAME_RESPONSE_HEALTH_ELASTICSEARCH_VALID, DATAFRAME_RESPONSE_HEALTH_PROMETHEUS, ELASTICSEARCH_DATASOURCE, INFLUXDB1_DATASOURCE, @@ -271,7 +272,29 @@ def test_series(self, m): self.assertEqual(len(result["data"]["result"][0]["values"]), 6) @requests_mock.Mocker() - def test_query_with_datasource_prometheus(self, m): + def test_query_with_datasource_prometheus_grafana7(self, m): + # Mock the version inquiry request, because `smartquery` needs + # it, as Prometheus responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "unknown", "database": "ok", "version": "7.0.1"}, + ) + m.post( + "http://localhost/api/ds/query", + json=DATAFRAME_RESPONSE_HEALTH_PROMETHEUS, + ) + datasource = PROMETHEUS_DATASOURCE.copy() + response = self.grafana.datasource.smartquery(datasource, "1+1") + self.assertEqual(response, DATAFRAME_RESPONSE_HEALTH_PROMETHEUS) + + @requests_mock.Mocker() + def test_query_with_datasource_prometheus_grafana9(self, m): + # Mock the version inquiry request, because `smartquery` needs + # it, as Prometheus responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "14e988bd22", "database": "ok", "version": "9.0.1"}, + ) m.post( "http://localhost/api/ds/query", json=DATAFRAME_RESPONSE_HEALTH_PROMETHEUS, @@ -298,12 +321,22 @@ def test_query_with_datasource_elasticsearch(self, m): "http://localhost/api/datasources/proxy/44/bazqux/_mapping", json={}, ) + m.post( + "http://localhost/api/ds/query", + json=DATAFRAME_RESPONSE_HEALTH_ELASTICSEARCH_VALID, + ) datasource = ELASTICSEARCH_DATASOURCE.copy() _ = self.grafana.datasource.smartquery(datasource, "url:///datasources/proxy/44/bazqux/_mapping") - # TODO: No response payload yet. + # TODO: Response payload not reflected and validated yet. @requests_mock.Mocker() def test_query_with_datasource_identifier(self, m): + # Mock the version inquiry request, because `smartquery` needs + # it, as Prometheus responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "14e988bd22", "database": "ok", "version": "9.0.1"}, + ) m.get( "http://localhost/api/datasources/uid/h8KkCLt7z", json=PROMETHEUS_DATASOURCE, @@ -315,7 +348,14 @@ def test_query_with_datasource_identifier(self, m): response = self.grafana.datasource.smartquery(DatasourceIdentifier(uid="h8KkCLt7z"), "1+1") self.assertEqual(response, DATAFRAME_RESPONSE_HEALTH_PROMETHEUS) - def test_query_unknown_access_type_failure(self): + @requests_mock.Mocker() + def test_query_unknown_access_type_failure(self, m): + # Mock the version inquiry request, because `smartquery` needs + # it, as Prometheus responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "14e988bd22", "database": "ok", "version": "9.0.1"}, + ) datasource = PROMETHEUS_DATASOURCE.copy() datasource["access"] = "__UNKNOWN__" self.assertRaises(NotImplementedError, lambda: self.grafana.datasource.smartquery(datasource, expression="1+1")) diff --git a/test/elements/test_datasource_fixtures.py b/test/elements/test_datasource_fixtures.py index a718083..330151b 100644 --- a/test/elements/test_datasource_fixtures.py +++ b/test/elements/test_datasource_fixtures.py @@ -205,6 +205,9 @@ "results": {"test": {"frames": [{"schema": {"meta": {"executedQueryString": "SELECT 1"}}}]}} } +# TODO: Reflect a real Grafana response for Elasticsearch here. +DATAFRAME_RESPONSE_HEALTH_ELASTICSEARCH_VALID = {"bazqux": {}} + DATAFRAME_RESPONSE_HEALTH_PROMETHEUS = { "results": { "test": { diff --git a/test/elements/test_datasource_health.py b/test/elements/test_datasource_health.py index 575bef9..140b184 100644 --- a/test/elements/test_datasource_health.py +++ b/test/elements/test_datasource_health.py @@ -24,6 +24,7 @@ ) import requests_mock +from parameterized import parameterized from grafana_client import GrafanaApi from grafana_client.client import GrafanaClientError, GrafanaServerError @@ -51,8 +52,8 @@ def test_health_check_elasticsearch_success(self, m): "http://localhost/api/datasources/uid/34inf2sdc", json=ELASTICSEARCH_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/44/bazqux/_mapping", + m.post( + "http://localhost/api/ds/query", json={"bazqux": {"mappings": {"properties": "something"}}}, ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="34inf2sdc")) @@ -77,8 +78,8 @@ def test_health_check_elasticsearch_empty_response_failure(self, m): "http://localhost/api/datasources/uid/34inf2sdc", json=ELASTICSEARCH_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/44/bazqux/_mapping", + m.post( + "http://localhost/api/ds/query", json={}, ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="34inf2sdc")) @@ -103,8 +104,8 @@ def test_health_check_elasticsearch_incomplete_response_failure(self, m): "http://localhost/api/datasources/uid/34inf2sdc", json=ELASTICSEARCH_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/44/bazqux/_mapping", + m.post( + "http://localhost/api/ds/query", json={"bazqux": {}}, ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="34inf2sdc")) @@ -129,8 +130,8 @@ def test_health_check_elasticsearch_error_response_failure(self, m): "http://localhost/api/datasources/uid/34inf2sdc", json=ELASTICSEARCH_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/44/bazqux/_mapping", + m.post( + "http://localhost/api/ds/query", json={"error": "This failed!", "status": 400}, status_code=400, ) @@ -156,8 +157,8 @@ def test_health_check_elasticsearch_error_response_with_root_cause_failure(self, "http://localhost/api/datasources/uid/34inf2sdc", json=ELASTICSEARCH_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/44/bazqux/_mapping", + m.post( + "http://localhost/api/ds/query", json={"error": {"root_cause": [{"type": "foo", "reason": "bar"}]}, "status": 400}, status_code=400, ) @@ -273,8 +274,8 @@ def test_health_check_jaeger_success(self, m): "http://localhost/api/datasources/uid/DbtFe237k", json=JAEGER_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/53/api/services", + m.post( + "http://localhost/api/ds/query", json={"data": ["jaeger-query"], "total": 1, "limit": 0, "offset": 0, "errors": None}, ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="DbtFe237k")) @@ -299,8 +300,8 @@ def test_health_check_jaeger_error_response_failure(self, m): "http://localhost/api/datasources/uid/DbtFe237k", json=JAEGER_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/53/api/services", + m.post( + "http://localhost/api/ds/query", json={ "data": ["jaeger-query"], "total": 1, @@ -326,13 +327,19 @@ def test_health_check_jaeger_error_response_failure(self, m): ) @requests_mock.Mocker() - def test_health_check_loki_success(self, m): + def test_health_check_loki_success_grafana7(self, m): + # Mock the version inquiry request, because `smartquery` needs + # it, as Loki responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "unknown", "database": "ok", "version": "7.0.1"}, + ) m.get( "http://localhost/api/datasources/uid/vCyglaq7z", json=LOKI_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/54/resources/labels", + m.post( + "http://localhost/api/ds/query", json={"status": "success", "data": ["__name__"]}, ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="vCyglaq7z")) @@ -352,13 +359,83 @@ def test_health_check_loki_success(self, m): ) @requests_mock.Mocker() - def test_health_check_loki_error_response_failure(self, m): + def test_health_check_loki_success_grafana9(self, m): + # Mock the version inquiry request, because `smartquery` needs + # it, as Loki responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "14e988bd22", "database": "ok", "version": "9.0.1"}, + ) m.get( "http://localhost/api/datasources/uid/vCyglaq7z", json=LOKI_DATASOURCE, ) + m.post( + "http://localhost/api/ds/query", + json={"results": {"test": True}}, + ) + response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="vCyglaq7z")) + response.duration = None + response.response = None + self.assertEqual( + response, + DatasourceHealthResponse( + uid="vCyglaq7z", + type="loki", + success=True, + status="OK", + message="Success", + duration=None, + response=None, + ), + ) + + @requests_mock.Mocker() + def test_health_check_loki_error_response_failure_grafana7(self, m): + # Mock the version inquiry request, because `smartquery` needs + # it, as Loki responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "unknown", "database": "ok", "version": "7.0.1"}, + ) + m.get( + "http://localhost/api/datasources/uid/vCyglaq7z", + json=LOKI_DATASOURCE, + ) + m.post( + "http://localhost/api/ds/query", + json={"message": "Failed to call resource", "traceID": "00000000000000000000000000000000"}, + ) + response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="vCyglaq7z")) + response.duration = None + response.response = None + self.assertEqual( + response, + DatasourceHealthResponse( + uid="vCyglaq7z", + type="loki", + success=False, + status="ERROR", + message="Failed to call resource", + duration=None, + response=None, + ), + ) + + @requests_mock.Mocker() + def test_health_check_loki_error_response_failure_grafana9(self, m): + # Mock the version inquiry request, because `smartquery` needs + # it, as Loki responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "14e988bd22", "database": "ok", "version": "9.0.1"}, + ) m.get( - "http://localhost/api/datasources/54/resources/labels", + "http://localhost/api/datasources/uid/vCyglaq7z", + json=LOKI_DATASOURCE, + ) + m.post( + "http://localhost/api/ds/query", json={"message": "Failed to call resource", "traceID": "00000000000000000000000000000000"}, ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="vCyglaq7z")) @@ -435,8 +512,8 @@ def test_health_check_opentsdb(self, m): "http://localhost/api/datasources/uid/hkuk5h3nk", json=OPENTSDB_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/51/api/suggest", + m.post( + "http://localhost/api/ds/query", json="", ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="hkuk5h3nk")) @@ -481,8 +558,15 @@ def test_health_check_postgres(self, m): ), ) - @requests_mock.Mocker() - def test_health_check_prometheus_healthy_success(self, m): + @parameterized.expand(["7.0.1", "9.0.1"]) + def test_health_check_prometheus_healthy_success(self, grafana_version): + m = requests_mock.Mocker() + # Mock the version inquiry request, because `smartquery` needs + # it, as Prometheus responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "unknown", "database": "ok", "version": grafana_version}, + ) m.get( "http://localhost/api/datasources/uid/h8KkCLt7z", json=PROMETHEUS_DATASOURCE, @@ -491,24 +575,32 @@ def test_health_check_prometheus_healthy_success(self, m): "http://localhost/api/ds/query", json=DATAFRAME_RESPONSE_HEALTH_PROMETHEUS, ) - response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="h8KkCLt7z")) - response.duration = None - response.response = None - self.assertEqual( - response, - DatasourceHealthResponse( - uid="h8KkCLt7z", - type="prometheus", - success=True, - status="OK", - message="Expr: 1+1\nStep: 15s", - duration=None, - response=None, - ), - ) + with m: + response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="h8KkCLt7z")) + response.duration = None + response.response = None + self.assertEqual( + response, + DatasourceHealthResponse( + uid="h8KkCLt7z", + type="prometheus", + success=True, + status="OK", + message="Expr: 1+1\nStep: 15s", + duration=None, + response=None, + ), + ) - @requests_mock.Mocker() - def test_health_check_prometheus_empty_dataframe_success(self, m): + @parameterized.expand(["7.0.1", "9.0.1"]) + def test_health_check_prometheus_empty_dataframe_success(self, grafana_version): + m = requests_mock.Mocker() + # Mock the version inquiry request, because `smartquery` needs + # it, as Prometheus responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "unknown", "database": "ok", "version": grafana_version}, + ) m.get( "http://localhost/api/datasources/uid/h8KkCLt7z", json=PROMETHEUS_DATASOURCE, @@ -517,24 +609,32 @@ def test_health_check_prometheus_empty_dataframe_success(self, m): "http://localhost/api/ds/query", json=DATAFRAME_RESPONSE_EMPTY, ) - response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="h8KkCLt7z")) - response.duration = None - response.response = None - self.assertEqual( - response, - DatasourceHealthResponse( - uid="h8KkCLt7z", - type="prometheus", - success=True, - status="OK", - message="Success", - duration=None, - response=None, - ), - ) + with m: + response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="h8KkCLt7z")) + response.duration = None + response.response = None + self.assertEqual( + response, + DatasourceHealthResponse( + uid="h8KkCLt7z", + type="prometheus", + success=True, + status="OK", + message="Success", + duration=None, + response=None, + ), + ) - @requests_mock.Mocker() - def test_health_check_prometheus_invalid_dataframe_failure(self, m): + @parameterized.expand(["7.0.1", "9.0.1"]) + def test_health_check_prometheus_invalid_dataframe_failure(self, grafana_version): + m = requests_mock.Mocker() + # Mock the version inquiry request, because `smartquery` needs + # it, as Prometheus responses differ between versions. + m.get( + "http://localhost/api/health", + json={"commit": "unknown", "database": "ok", "version": grafana_version}, + ) m.get( "http://localhost/api/datasources/uid/h8KkCLt7z", json=PROMETHEUS_DATASOURCE, @@ -543,22 +643,23 @@ def test_health_check_prometheus_invalid_dataframe_failure(self, m): "http://localhost/api/ds/query", json=DATAFRAME_RESPONSE_INVALID, ) - response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="h8KkCLt7z")) - response.duration = None - response.response = None - self.assertEqual( - response, - DatasourceHealthResponse( - uid="h8KkCLt7z", - type="prometheus", - success=False, - status="ERROR", - message="FATAL: Unable to decode result from dictionary-type response. " - "TypeError: DataFrame response detected, but 'frames' is not a list", - duration=None, - response=None, - ), - ) + with m: + response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="h8KkCLt7z")) + response.duration = None + response.response = None + self.assertEqual( + response, + DatasourceHealthResponse( + uid="h8KkCLt7z", + type="prometheus", + success=False, + status="ERROR", + message="FATAL: Unable to decode result from dictionary-type response. " + "TypeError: DataFrame response detected, but 'frames' is not a list", + duration=None, + response=None, + ), + ) @requests_mock.Mocker() def test_health_check_simplejson(self, m): @@ -566,8 +667,8 @@ def test_health_check_simplejson(self, m): "http://localhost/api/datasources/uid/rw783ds3e", json=SIMPLE_JSON_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/47", + m.post( + "http://localhost/api/ds/query", json={"results": {"test": {"error": "Resource not found: /metadata"}}}, status_code=404, ) @@ -593,8 +694,8 @@ def test_health_check_simpod_json(self, m): "http://localhost/api/datasources/uid/oie238af3", json=SIMPOD_JSON_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/49", + m.post( + "http://localhost/api/ds/query", json={"results": [{"statement_id": "ID", "series": []}]}, ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="oie238af3")) @@ -619,6 +720,10 @@ def test_health_check_sunandmoon_success(self, m): "http://localhost/api/datasources/uid/239fasva4", json=SUNANDMOON_DATASOURCE, ) + m.post( + "http://localhost/api/ds/query", + json=SUNANDMOON_DATASOURCE, + ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="239fasva4")) response.duration = None response.response = None @@ -641,6 +746,10 @@ def test_health_check_sunandmoon_incomplete_failure(self, m): "http://localhost/api/datasources/uid/239fasva4", json=SUNANDMOON_DATASOURCE_INCOMPLETE, ) + m.post( + "http://localhost/api/ds/query", + json=SUNANDMOON_DATASOURCE_INCOMPLETE, + ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="239fasva4")) response.duration = None response.response = None @@ -663,8 +772,8 @@ def test_health_check_tempo_success(self, m): "http://localhost/api/datasources/uid/aTk86s3nk", json=TEMPO_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/55/api/echo", + m.post( + "http://localhost/api/ds/query", headers={"Content-Type": "text/plain"}, text="echo", ) @@ -690,8 +799,8 @@ def test_health_check_tempo_error_response_failure(self, m): "http://localhost/api/datasources/uid/aTk86s3nk", json=TEMPO_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/55/api/echo", + m.post( + "http://localhost/api/ds/query", status_code=502, ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="aTk86s3nk")) @@ -716,6 +825,10 @@ def test_health_check_testdata(self, m): "http://localhost/api/datasources/uid/439fngqr2", json=TESTDATA_DATASOURCE, ) + m.post( + "http://localhost/api/ds/query", + json={"id": "test", "uid": "test"}, + ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="439fngqr2")) response.duration = None response.response = None @@ -738,8 +851,8 @@ def test_health_check_zipkin_success(self, m): "http://localhost/api/datasources/uid/3sXIv8q7k", json=ZIPKIN_DATASOURCE, ) - m.get( - "http://localhost/api/datasources/proxy/57/api/v2/services", + m.post( + "http://localhost/api/ds/query", json=[], ) response = self.grafana.datasource.health_check(DatasourceIdentifier(uid="3sXIv8q7k"))