diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 66fb891898a1c57f8d814394a698a17bb7935164..9a6ccb8e17f9b01e751e0631f7a9fd2488f0b56b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -474,6 +474,22 @@ test-slo-checker-dropped-records-kstreams: - when: manual allow_failure: true +test-slo-checker-generic: + stage: test + needs: [] + image: python:3.7-slim + before_script: + - cd slo-checker/generic + script: + - pip install -r requirements.txt + - cd app + - python -m unittest + rules: + - changes: + - slo-checker/generic/**/* + - when: manual + allow_failure: true + deploy-slo-checker-lag-trend: stage: deploy extends: @@ -510,6 +526,24 @@ deploy-slo-checker-dropped-records-kstreams: when: manual allow_failure: true +deploy-slo-checker-generic: + stage: deploy + extends: + - .kaniko-push + needs: + - test-slo-checker-generic + before_script: + - cd slo-checker/generic + variables: + IMAGE_NAME: theodolite-slo-checker-generic + rules: + - changes: + - slo-checker/generic/**/* + if: "$CR_HOST && $CR_ORG && $CR_USER && $CR_PW" + - if: "$CR_HOST && $CR_ORG && $CR_USER && $CR_PW" + when: manual + allow_failure: true + # Theodolite Random Scheduler diff --git a/slo-checker/generic/Dockerfile b/slo-checker/generic/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..032b8153a6989ca04631ba553289dacb3620a38d --- /dev/null +++ b/slo-checker/generic/Dockerfile @@ -0,0 +1,6 @@ +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY ./app /app \ No newline at end of file diff --git a/slo-checker/generic/README.md b/slo-checker/generic/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1a1358a06dc4165c678bca8745dd40473a7c5880 --- /dev/null +++ b/slo-checker/generic/README.md @@ -0,0 +1,89 @@ +# Generic SLO Evaluator + +## Execution + +For development: + +```sh +uvicorn main:app --reload +``` + +## Build the docker image: + +```sh +docker build . -t theodolite-evaluator +``` + +Run the Docker image: + +```sh +docker run -p 80:80 theodolite-evaluator +``` + +## Configuration + +You can set the `HOST` and the `PORT` (and a lot of more parameters) via environment variables. Default is `0.0.0.0:80`. +For more information see the [Gunicorn/FastAPI Docker docs](https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker#advanced-usage). + +## API Documentation + +The running webserver provides a REST API with the following route: + +* / + * Method: POST + * Body: + * results + * metric-metadata + * values + * metadata + * warmup + * queryAggregation + * repetitionAggregation + * operator + * threshold + +The body of the request must be a JSON string that satisfies the following conditions: + +* **dropped records**: This property is based on the [Range Vector type](https://www.prometheus.io/docs/prometheus/latest/querying/api/#range-vectors) from Prometheus and must have the following JSON *structure*: + + ```json + { + "results": [ + [ + { + "metric": { + "<label-name>": "<label-value>" + }, + "values": [ + [ + <unix_timestamp>, // 1.634624989695E9 + "<sample_value>" // integer + ] + ] + } + ] + ], + "metadata": { + "warmup": 60, + "queryAggregation": "max", + "repetitionAggregation": "median", + "operator": "lt", + "threshold": 2000000 + } + } + ``` + +### description + +* results: + * metric-metadata: + * Labels of this metric. The `generic` slo checker does not use labels in the calculation of the service level objective. + * results + * The `<unix_timestamp>` provided as the first element of each element in the "values" array must be the timestamp of the measurement value in seconds (with optional decimal precision) + * The `<sample_value>` must be the measurement value as string. +* metadata: For the calculation of the service level objective require metadata. + * **warmup**: Specifies the warmup time in seconds that are ignored for evaluating the SLO. + * **queryAggregation**: Specifies the function used to aggregate a query. + * **repetitionAggregation**: Specifies the function used to aggregate a the results of multiple query aggregations. + * **operator**: Specifies how the result should be checked agains a threshold. Possible values are `lt`, `lte`, `gt` and `gte`. + * **threshold**: Must be an unsigned integer that specifies the threshold for the SLO evaluation. diff --git a/slo-checker/generic/app/main.py b/slo-checker/generic/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..e55c478c5df5c7e8ff7d26289cd99f9a82b725fc --- /dev/null +++ b/slo-checker/generic/app/main.py @@ -0,0 +1,73 @@ +from fastapi import FastAPI,Request +import logging +import os +import json +import sys +import re +import pandas as pd + + +app = FastAPI() + +logging.basicConfig(stream=sys.stdout, + format="%(asctime)s %(levelname)s %(name)s: %(message)s") +logger = logging.getLogger("API") + + +if os.getenv('LOG_LEVEL') == 'INFO': + logger.setLevel(logging.INFO) +elif os.getenv('LOG_LEVEL') == 'WARNING': + logger.setLevel(logging.WARNING) +elif os.getenv('LOG_LEVEL') == 'DEBUG': + logger.setLevel(logging.DEBUG) + + +def get_aggr_func(func_string: str): + if func_string in ['mean', 'median', 'mode', 'sum', 'count', 'max', 'min', 'std', 'var', 'skew', 'kurt']: + return func_string + elif re.search(r'^p\d\d?(\.\d+)?$', func_string): # matches strings like 'p99', 'p99.99', 'p1', 'p0.001' + def percentile(x): + return x.quantile(float(func_string[1:]) / 100) + percentile.__name__ = func_string + return percentile + else: + raise ValueError('Invalid function string.') + +def aggr_query(values: dict, warmup: int, aggr_func): + df = pd.DataFrame.from_dict(values) + df.columns = ['timestamp', 'value'] + filtered = df[df['timestamp'] >= (df['timestamp'][0] + warmup)] + filtered['value'] = filtered['value'].astype(int) + return filtered['value'].aggregate(aggr_func) + +def check_result(result, operator: str, threshold): + if operator == 'lt': + return result < threshold + if operator == 'lte': + return result <= threshold + if operator == 'gt': + return result > threshold + if operator == 'gte': + return result >= threshold + else: + raise ValueError('Invalid operator string.') + + + +@app.post("/",response_model=bool) +async def check_slo(request: Request): + data = json.loads(await request.body()) + warmup = int(data['metadata']['warmup']) + query_aggregation = get_aggr_func(data['metadata']['queryAggregation']) + rep_aggregation = get_aggr_func(data['metadata']['repetitionAggregation']) + operator = data['metadata']['operator'] + threshold = int(data['metadata']['threshold']) + + for r in data["results"]: + aggr_query(r[0]["values"], warmup, query_aggregation) + + query_results = [aggr_query(r[0]["values"], warmup, query_aggregation) for r in data["results"]] + result = pd.DataFrame(query_results).aggregate(rep_aggregation).at[0] + return check_result(result, operator, threshold) + +logger.info("SLO evaluator is online") \ No newline at end of file diff --git a/slo-checker/generic/app/test.py b/slo-checker/generic/app/test.py new file mode 100644 index 0000000000000000000000000000000000000000..2609225ddc9e6e96cdcd01db197cebbdd6501102 --- /dev/null +++ b/slo-checker/generic/app/test.py @@ -0,0 +1,56 @@ +import unittest +from main import app, get_aggr_func, check_result +import json +from fastapi.testclient import TestClient + +class TestSloEvaluation(unittest.TestCase): + client = TestClient(app) + + def test_1_rep(self): + with open('../resources/test-1-rep-success.json') as json_file: + data = json.load(json_file) + response = self.client.post("/", json=data) + self.assertEqual(response.json(), True) + + def test_get_aggr_func_mean(self): + self.assertEqual(get_aggr_func('median'), 'median') + + def test_get_aggr_func_p99(self): + self.assertTrue(callable(get_aggr_func('p99'))) + + def test_get_aggr_func_p99_9(self): + self.assertTrue(callable(get_aggr_func('p99.9'))) + + def test_get_aggr_func_p99_99(self): + self.assertTrue(callable(get_aggr_func('p99.99'))) + + def test_get_aggr_func_p0_1(self): + self.assertTrue(callable(get_aggr_func('p0.1'))) + + def test_get_aggr_func_p99_(self): + self.assertRaises(ValueError, get_aggr_func, 'p99.') + + def test_get_aggr_func_p99_(self): + self.assertRaises(ValueError, get_aggr_func, 'q99') + + def test_get_aggr_func_p99_(self): + self.assertRaises(ValueError, get_aggr_func, 'mux') + + def test_check_result_lt(self): + self.assertEqual(check_result(100, 'lt', 200), True) + + def test_check_result_lte(self): + self.assertEqual(check_result(200, 'lte', 200), True) + + def test_check_result_gt(self): + self.assertEqual(check_result(100, 'gt', 200), False) + + def test_check_result_gte(self): + self.assertEqual(check_result(300, 'gte', 200), True) + + def test_check_result_invalid(self): + self.assertRaises(ValueError, check_result, 100, 'xyz', 200) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/slo-checker/generic/requirements.txt b/slo-checker/generic/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..87972ab01a276cbb63033e214e1ad53d38b5c8d8 --- /dev/null +++ b/slo-checker/generic/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.65.2 +pandas==1.0.3 +uvicorn +requests diff --git a/slo-checker/generic/resources/test-1-rep-success.json b/slo-checker/generic/resources/test-1-rep-success.json new file mode 100644 index 0000000000000000000000000000000000000000..b70f461cf620d8eee8c4d9d93feb46db7498626f --- /dev/null +++ b/slo-checker/generic/resources/test-1-rep-success.json @@ -0,0 +1,276 @@ +{ + "results": [ + [ + { + "metric": { + "job": "titan-ccp-aggregation" + }, + "values": [ + [ + 1.634624674695E9, + "0" + ], + [ + 1.634624679695E9, + "0" + ], + [ + 1.634624684695E9, + "0" + ], + [ + 1.634624689695E9, + "0" + ], + [ + 1.634624694695E9, + "0" + ], + [ + 1.634624699695E9, + "0" + ], + [ + 1.634624704695E9, + "0" + ], + [ + 1.634624709695E9, + "0" + ], + [ + 1.634624714695E9, + "0" + ], + [ + 1.634624719695E9, + "0" + ], + [ + 1.634624724695E9, + "0" + ], + [ + 1.634624729695E9, + "0" + ], + [ + 1.634624734695E9, + "0" + ], + [ + 1.634624739695E9, + "0" + ], + [ + 1.634624744695E9, + "1" + ], + [ + 1.634624749695E9, + "3" + ], + [ + 1.634624754695E9, + "4" + ], + [ + 1.634624759695E9, + "4" + ], + [ + 1.634624764695E9, + "4" + ], + [ + 1.634624769695E9, + "4" + ], + [ + 1.634624774695E9, + "4" + ], + [ + 1.634624779695E9, + "4" + ], + [ + 1.634624784695E9, + "4" + ], + [ + 1.634624789695E9, + "4" + ], + [ + 1.634624794695E9, + "4" + ], + [ + 1.634624799695E9, + "4" + ], + [ + 1.634624804695E9, + "176" + ], + [ + 1.634624809695E9, + "176" + ], + [ + 1.634624814695E9, + "176" + ], + [ + 1.634624819695E9, + "176" + ], + [ + 1.634624824695E9, + "176" + ], + [ + 1.634624829695E9, + "159524" + ], + [ + 1.634624834695E9, + "209870" + ], + [ + 1.634624839695E9, + "278597" + ], + [ + 1.634624844695E9, + "460761" + ], + [ + 1.634624849695E9, + "460761" + ], + [ + 1.634624854695E9, + "460761" + ], + [ + 1.634624859695E9, + "460761" + ], + [ + 1.634624864695E9, + "460761" + ], + [ + 1.634624869695E9, + "606893" + ], + [ + 1.634624874695E9, + "653534" + ], + [ + 1.634624879695E9, + "755796" + ], + [ + 1.634624884695E9, + "919317" + ], + [ + 1.634624889695E9, + "919317" + ], + [ + 1.634624894695E9, + "955926" + ], + [ + 1.634624899695E9, + "955926" + ], + [ + 1.634624904695E9, + "955926" + ], + [ + 1.634624909695E9, + "955926" + ], + [ + 1.634624914695E9, + "955926" + ], + [ + 1.634624919695E9, + "1036530" + ], + [ + 1.634624924695E9, + "1078477" + ], + [ + 1.634624929695E9, + "1194775" + ], + [ + 1.634624934695E9, + "1347755" + ], + [ + 1.634624939695E9, + "1352151" + ], + [ + 1.634624944695E9, + "1360428" + ], + [ + 1.634624949695E9, + "1360428" + ], + [ + 1.634624954695E9, + "1360428" + ], + [ + 1.634624959695E9, + "1360428" + ], + [ + 1.634624964695E9, + "1360428" + ], + [ + 1.634624969695E9, + "1525685" + ], + [ + 1.634624974695E9, + "1689296" + ], + [ + 1.634624979695E9, + "1771358" + ], + [ + 1.634624984695E9, + "1854284" + ], + [ + 1.634624989695E9, + "1854284" + ] + ] + } + ] + ], + "metadata": { + "warmup": 60, + "queryAggregation": "max", + "repetitionAggregation": "median", + "operator": "lt", + "threshold": 2000000 + } +} \ No newline at end of file diff --git a/theodolite-benchmarks/beam-commons/build.gradle b/theodolite-benchmarks/beam-commons/build.gradle index 66ec44ad715d64458584e71fdd4f49abb9c458f4..a809f6bc4b97d8d62b807243eddecda8a5de5032 100644 --- a/theodolite-benchmarks/beam-commons/build.gradle +++ b/theodolite-benchmarks/beam-commons/build.gradle @@ -3,7 +3,7 @@ plugins { } repositories { - jcenter() + mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.beam.gradle b/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.beam.gradle index 09e36d52171699c61b212b1f64c827933679b6fa..41d1ae4f2bdfa358aca3fca2b91ea2b57e4c3405 100644 --- a/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.beam.gradle +++ b/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.beam.gradle @@ -9,7 +9,7 @@ plugins { tasks.distZip.enabled = false repositories { - jcenter() + mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.flink.gradle b/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.flink.gradle index 26a827b6049d09e422d48609590614f383f6cae8..f5e93dd88d2234f8a9b0d6fea880f47d652dccfa 100644 --- a/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.flink.gradle +++ b/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.flink.gradle @@ -25,7 +25,7 @@ ext { } repositories { - jcenter() + mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.kstreams.gradle b/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.kstreams.gradle index eece7b835ae9d6f39283ea371ce8b0b8194cdaa0..da2d42176ac0ddc9a157f843e3268b37ac4397e2 100644 --- a/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.kstreams.gradle +++ b/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.kstreams.gradle @@ -9,7 +9,7 @@ plugins { tasks.distZip.enabled = false repositories { - jcenter() + mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.load-generator.gradle b/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.load-generator.gradle index c6c2b6057cf35c32faa4d67b6ea6dba9e5c13beb..fb4fd89d1fe8a6d625a3ba7b459e9b0961befdbc 100644 --- a/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.load-generator.gradle +++ b/theodolite-benchmarks/buildSrc/src/main/groovy/theodolite.load-generator.gradle @@ -9,7 +9,7 @@ plugins { tasks.distZip.enabled = false repositories { - jcenter() + mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/theodolite-benchmarks/flink-commons/build.gradle b/theodolite-benchmarks/flink-commons/build.gradle index edd48c914b8c909ff196bb98e9bbc8b9d99865b9..a3a4a35752006bb10e15ff508ce0b37f70adc57d 100644 --- a/theodolite-benchmarks/flink-commons/build.gradle +++ b/theodolite-benchmarks/flink-commons/build.gradle @@ -8,7 +8,7 @@ ext { } repositories { - jcenter() + mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/theodolite-benchmarks/kstreams-commons/build.gradle b/theodolite-benchmarks/kstreams-commons/build.gradle index c5a880acd4377056cc0b0f06b33a2d74c9f87c4e..7683ffe39314ec375eda0ed4e139d618d44a7328 100644 --- a/theodolite-benchmarks/kstreams-commons/build.gradle +++ b/theodolite-benchmarks/kstreams-commons/build.gradle @@ -3,7 +3,7 @@ plugins { } repositories { - jcenter() + mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/theodolite-benchmarks/load-generator-commons/build.gradle b/theodolite-benchmarks/load-generator-commons/build.gradle index 118f3e648f829a3eafe719ddf660d35ac8563574..f2aa10b079f4be80d19d9ac5d822b7bdab0b6d78 100644 --- a/theodolite-benchmarks/load-generator-commons/build.gradle +++ b/theodolite-benchmarks/load-generator-commons/build.gradle @@ -3,7 +3,7 @@ plugins { } repositories { - jcenter() + mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt b/theodolite/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt index 6064772889ba50ac87fb5f5cf71ef564f6cc2732..be3e48be406b631e03ca2fd32909a442b592f259 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt @@ -46,7 +46,7 @@ class AnalysisExecutor( fetcher.fetchMetric( start = interval.first, end = interval.second, - query = SloConfigHandler.getQueryString(sloType = slo.sloType) + query = SloConfigHandler.getQueryString(slo = slo) ) } @@ -67,7 +67,7 @@ class AnalysisExecutor( return sloChecker.evaluate(prometheusData) } catch (e: Exception) { - throw EvaluationFailedException("Evaluation failed for resource '${res.get()}' and load '${load.get()} ", e) + throw EvaluationFailedException("Evaluation failed for resource '${res.get()}' and load '${load.get()}", e) } } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt b/theodolite/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt index 92b98c30229f9b724048020193eb88024c0c90f2..7fb5417e200f64b0db74a8bebe69a751c5d484b8 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/ExternalSloChecker.kt @@ -8,13 +8,11 @@ import java.net.ConnectException /** * [SloChecker] that uses an external source for the concrete evaluation. * @param externalSlopeURL The url under which the external evaluation can be reached. - * @param threshold threshold that should not be exceeded to evaluate to true. - * @param warmup time that is not taken into consideration for the evaluation. + * @param metadata metadata passed to the external SLO checker. */ class ExternalSloChecker( private val externalSlopeURL: String, - private val threshold: Int, - private val warmup: Int + private val metadata: Map<String, Any> ) : SloChecker { private val RETRIES = 2 @@ -27,29 +25,25 @@ class ExternalSloChecker( * Will try to reach the external service until success or [RETRIES] times. * Each request will timeout after [TIMEOUT]. * - * @param start point of the experiment. - * @param end point of the experiment. * @param fetchedData that should be evaluated - * @return true if the experiment was successful(the threshold was not exceeded. + * @return true if the experiment was successful (the threshold was not exceeded). * @throws ConnectException if the external service could not be reached. */ override fun evaluate(fetchedData: List<PrometheusResponse>): Boolean { var counter = 0 - val data = SloJson.Builder() - .results(fetchedData.map { it.data?.result }) - .addMetadata("threshold", threshold) - .addMetadata( "warmup", warmup) - .build() - .toJson() + val data = SloJson( + results = fetchedData.map { it.data?.result ?: listOf() }, + metadata = metadata + ).toJson() while (counter < RETRIES) { val result = post(externalSlopeURL, data = data, timeout = TIMEOUT) if (result.statusCode != 200) { counter++ - logger.error { "Could not reach external SLO checker" } + logger.error { "Could not reach external SLO checker." } } else { val booleanResult = result.text.toBoolean() - logger.info { "SLO checker result is: $booleanResult" } + logger.info { "SLO checker result is: $booleanResult." } return booleanResult } } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloChecker.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloChecker.kt index af70fa5dca3f0556d38791ed96c2af30b9a44a68..82f903f5be868731d58ebefd6279d5d438bd5eab 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloChecker.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloChecker.kt @@ -11,7 +11,7 @@ interface SloChecker { * Evaluates [fetchedData] and returns if the experiments were successful. * * @param fetchedData from Prometheus that will be evaluated. - * @return true if experiments were successful. Otherwise false. + * @return true if experiments were successful. Otherwise, false. */ fun evaluate(fetchedData: List<PrometheusResponse>): Boolean } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt index 714af6075a030e2e49140afd491cb06cb10801da..c2514469925bcfc20c15377e93963df04a3b91f6 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloCheckerFactory.kt @@ -43,15 +43,33 @@ class SloCheckerFactory { properties: MutableMap<String, String>, load: LoadDimension ): SloChecker { - return when (sloType.lowercase()) { - SloTypes.LAG_TREND.value, SloTypes.DROPPED_RECORDS.value -> ExternalSloChecker( + return when (SloTypes.from(sloType)) { + SloTypes.GENERIC -> ExternalSloChecker( externalSlopeURL = properties["externalSloUrl"] ?: throw IllegalArgumentException("externalSloUrl expected"), - threshold = properties["threshold"]?.toInt() ?: throw IllegalArgumentException("threshold expected"), - warmup = properties["warmup"]?.toInt() ?: throw IllegalArgumentException("warmup expected") + // TODO validate property contents + metadata = mapOf( + "warmup" to (properties["warmup"]?.toInt() ?: throw IllegalArgumentException("warmup expected")), + "queryAggregation" to (properties["warmup"]?.toInt() + ?: throw IllegalArgumentException("queryAggregation expected")), + "repetitionAggregation" to (properties["warmup"]?.toInt() + ?: throw IllegalArgumentException("repetitionAggregation expected")), + "operator" to (properties["warmup"]?.toInt() + ?: throw IllegalArgumentException("operator expected")), + "threshold" to (properties["threshold"]?.toInt() + ?: throw IllegalArgumentException("threshold expected")) + ) ) - - SloTypes.LAG_TREND_RATIO.value, SloTypes.DROPPED_RECORDS_RATIO.value -> { + SloTypes.LAG_TREND, SloTypes.DROPPED_RECORDS -> ExternalSloChecker( + externalSlopeURL = properties["externalSloUrl"] + ?: throw IllegalArgumentException("externalSloUrl expected"), + metadata = mapOf( + "warmup" to (properties["warmup"]?.toInt() ?: throw IllegalArgumentException("warmup expected")), + "threshold" to (properties["threshold"]?.toInt() + ?: throw IllegalArgumentException("threshold expected")) + ) + ) + SloTypes.LAG_TREND_RATIO, SloTypes.DROPPED_RECORDS_RATIO -> { val thresholdRatio = properties["ratio"]?.toDouble() ?: throw IllegalArgumentException("ratio for threshold expected") @@ -64,11 +82,13 @@ class SloCheckerFactory { ExternalSloChecker( externalSlopeURL = properties["externalSloUrl"] ?: throw IllegalArgumentException("externalSloUrl expected"), - threshold = threshold, - warmup = properties["warmup"]?.toInt() ?: throw IllegalArgumentException("warmup expected") + metadata = mapOf( + "warmup" to (properties["warmup"]?.toInt() + ?: throw IllegalArgumentException("warmup expected")), + "threshold" to threshold + ) ) } - else -> throw IllegalArgumentException("Slotype $sloType not found.") } } } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloConfigHandler.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloConfigHandler.kt index 8d54dd88d9d9d5fe6e58246f37c5e44724218b5f..425a4f3b0634d53f8b1d5c4b8abdba9ca81c3f2b 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloConfigHandler.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloConfigHandler.kt @@ -1,5 +1,6 @@ package theodolite.evaluation +import theodolite.benchmark.BenchmarkExecution import theodolite.util.InvalidPatcherConfigurationException import javax.enterprise.context.ApplicationScoped @@ -9,11 +10,12 @@ private const val DROPPED_RECORDS_QUERY = "sum by(job) (kafka_streams_stream_tas @ApplicationScoped class SloConfigHandler { companion object { - fun getQueryString(sloType: String): String { - return when (sloType.lowercase()) { + fun getQueryString(slo: BenchmarkExecution.Slo): String { + return when (slo.sloType.toLowerCase()) { + SloTypes.GENERIC.value -> slo.properties["promQLQuery"] ?: throw IllegalArgumentException("promQLQuery expected") SloTypes.LAG_TREND.value, SloTypes.LAG_TREND_RATIO.value -> CONSUMER_LAG_QUERY SloTypes.DROPPED_RECORDS.value, SloTypes.DROPPED_RECORDS_RATIO.value -> DROPPED_RECORDS_QUERY - else -> throw InvalidPatcherConfigurationException("Could not find Prometheus query string for slo type $sloType") + else -> throw InvalidPatcherConfigurationException("Could not find Prometheus query string for slo type $slo.sloType") } } } diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloJson.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloJson.kt index fc9fe17b255dbb5ae68881538d8d2a50a191edb1..205389276f2c1adef6cba6c745baf99744c8d2dd 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloJson.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloJson.kt @@ -3,61 +3,17 @@ package theodolite.evaluation import com.google.gson.Gson import theodolite.util.PromResult -class SloJson private constructor( - val results: List<List<PromResult>?>? = null, - var metadata: MutableMap<String, Any>? = null +class SloJson constructor( + val results: List<List<PromResult>>, + var metadata: Map<String, Any> ) { - data class Builder( - var results:List<List<PromResult>?>? = null, - var metadata: MutableMap<String, Any>? = null - ) { - - /** - * Set the results - * - * @param results list of prometheus results - */ - fun results(results: List<List<PromResult>?>) = apply { this.results = results } - - /** - * Add metadata as key value pairs - * - * @param key key of the metadata to be added - * @param value value of the metadata to be added - */ - fun addMetadata(key: String, value: String) = apply { - if (this.metadata.isNullOrEmpty()) { - this.metadata = mutableMapOf(key to value) - } else { - this.metadata!![key] = value - } - } - - /** - * Add metadata as key value pairs - * - * @param key key of the metadata to be added - * @param value value of the metadata to be added - */ - fun addMetadata(key: String, value: Int) = apply { - if (this.metadata.isNullOrEmpty()) { - this.metadata = mutableMapOf(key to value) - } else { - this.metadata!![key] = value - } - } - - fun build() = SloJson( - results = results, - metadata = metadata + fun toJson(): String { + return Gson().toJson( + mapOf( + "results" to this.results, + "metadata" to this.metadata + ) ) } - - fun toJson(): String { - return Gson().toJson(mapOf( - "results" to this.results, - "metadata" to this.metadata - )) - } } \ No newline at end of file diff --git a/theodolite/src/main/kotlin/theodolite/evaluation/SloTypes.kt b/theodolite/src/main/kotlin/theodolite/evaluation/SloTypes.kt index ac9de35861b0bd9c012bfb0b8cfcb2e1aa5aed68..812b50de779d2f3abfd5788b8aee145edc959e6c 100644 --- a/theodolite/src/main/kotlin/theodolite/evaluation/SloTypes.kt +++ b/theodolite/src/main/kotlin/theodolite/evaluation/SloTypes.kt @@ -1,10 +1,14 @@ package theodolite.evaluation enum class SloTypes(val value: String) { + GENERIC("generic"), LAG_TREND("lag trend"), LAG_TREND_RATIO("lag trend ratio"), DROPPED_RECORDS("dropped records"), - DROPPED_RECORDS_RATIO("dropped records ratio") - + DROPPED_RECORDS_RATIO("dropped records ratio"); + companion object { + fun from(type: String): SloTypes = + values().find { it.value == type } ?: throw IllegalArgumentException("Requested SLO does not exist") + } } \ No newline at end of file