Skip to content
Snippets Groups Projects
Commit 344e849a authored by Lorenz Boguhn's avatar Lorenz Boguhn
Browse files

Add tupp slo-checker

parent 9b6258c7
No related branches found
No related tags found
1 merge request!299Add Time Until Peak Processed SLO-Checker
FROM python:3.8
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
WORKDIR /code/app
ENV HOST 0.0.0.0
ENV PORT 80
CMD ["sh", "-c", "uvicorn main:app --host $HOST --port $PORT"]
# Time Until Peak Processed SLO Evaluator
## Execution
For development:
```sh
uvicorn main:app --reload
```
For testing in `app` directory:
```sh
python3 test.py
```
## Build the docker image:
```sh
docker build . -t tupp
```
Run the Docker image:
```sh
docker run -p 80:80 tupp
```
## 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 web server provides a REST API with the following route:
* /
* Method: POST
* Body:
* results
* metric-metadata
* values
* metadata
* warmup
* beforeWindowLength
* ignoreDuring
* afterWindowLength
* error
* ratio
The body of the request must be a JSON string that satisfies the following conditions:
### description
* results:
* metric-metadata:
* Labels of this metric. This 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**: (Unsigned integer) Specifies the warm-up time in seconds that are ignored for evaluating the SLO.
* **beforeWindowLength**: (Unsigned integer) Specifies amount of measurements used for the first window. This window is evaluated by the lag trend metric and afterwards the average of the window is calculated.
* **ignoreDuring**: (Unsigned integer) Specifies the amount of ignored measurements before searching for a window with a lower average.
* **afterWindowLength**: (Unsigned integer) Specifies the size of the windows that are used to calculate the averages.
* **error**: (Ratio) Specifies how much the after windows are allowed to deviate from the initial average lag.
* **ratio**: (Ratio) Specifies the threshold for the *lag trend* SLO evaluation.
\ No newline at end of file
from fastapi import FastAPI,Request
import logging
import os
import json
import sys
import re
import pandas as pd
import numpy as np
from numpy.lib.stride_tricks import sliding_window_view
from sklearn.linear_model import LinearRegression
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)
logger.setLevel(logging.INFO)
measurementInterval = 5
# calculates if there is a window with a mean after the peak <= the mean of the first period
def average_reached(data, beforeWindowSize, ignoreDuring, afterWindowLength, error):
timeArray = data[data.columns[0]].to_numpy()
lagArray = data[data.columns[1]].to_numpy()
beforePeak = lagArray[:beforeWindowSize]
afterPeak = lagArray[beforeWindowSize + ignoreDuring:]
logger.info(f'Measurements before: {len(beforePeak)}')
logger.info(f'Measurements after: {len(afterPeak)}')
# Average of the first window
averageBefore = np.mean(beforePeak)
averageBeforeError = averageBefore*(1 + error)
logger.info(f'Average with error {averageBeforeError}')
if afterPeak.size < afterWindowLength:
raise Exception("The after window measurement is smaller than the configured window length")
# get an array of sliding windows
windows = sliding_window_view(afterPeak, afterWindowLength)
logger.info(f'Search in {windows.shape[0]} windows')
# Searches for all indexes at which the window average is smaler than the average of the first window
smaller, = np.where(windows.mean(axis=1) <= averageBeforeError)
# if there is no window found, return false
if smaller.size == 0:
logger.info("No window found!")
return False
else:
index = (smaller[0]) + ignoreDuring
logger.info(f'Window found!' + f' Index: {index}')
logger.info(f'Window: {windows[smaller[0]]}')
logger.info(f'Peak processed after roughly: {index * measurementInterval} seconds')
logger.info(f'From the start: {(index+beforeWindowSize) * measurementInterval} seconds')
logger.info(f'Window start: {timeArray[index+beforeWindowSize]}')
return True
def lagTrend(data,ratio):
X = data.iloc[:, 0].values.reshape(-1, 1) # values converts it into a numpy array
Y = data.iloc[:, 1].values.reshape(-1, 1) # -1 means that calculate the dimension of rows, but have 1 column
linear_regressor = LinearRegression() # create object for the class
linear_regressor.fit(X, Y) # perform linear regression
Y_pred = linear_regressor.predict(X) # make predictions
trend_slope = linear_regressor.coef_[0][0]
logger.info("Computed lag trend slope is '%s'", trend_slope)
return trend_slope <= ratio
@app.post("/",response_model=bool)
async def check_slo(request: Request):
data = json.loads(await request.body())
logger.info('Received request with data: %s', data)
# Get parameters from request
warmup = int(data['metadata']['warmup'])
beforeWindowSize = int(data['metadata']['beforeWindowLength'])
ignoreDuring = int(data['metadata']['ignoreDuring'])
afterWindowLength = int(data['metadata']['afterWindowLength'])
error = float(data['metadata']['error'])
ratio = float(data['metadata']['ratio'])
# extract dataframe from the request
prom_results = [r[0]["values"] for r in data["results"]]
df = pd.DataFrame.from_dict(prom_results[0])
df.columns = ['timestamp', 'value'] # name columns
logger.info(f'First row: {df.head(1)}')
logger.info(f'Dataframe shape: {df.shape}')
# Calculate the mesurementInterval
starttime = df['timestamp'].iloc[0]
row2 = df['timestamp'].iloc[1]
measurementInterval = int(row2 - starttime)
logger.info(f'Measurement interval: {measurementInterval}')
# cut the warmup period
filtered = df.loc[df['timestamp'] >= df['timestamp'][0] + warmup,].copy()
filtered['value'] = filtered['value'].astype(float)
# check first lagtrend
if(lagTrend(filtered.head(beforeWindowSize), ratio)):
logger.info("Lag trend for first part is ok")
# check average afterwards
return average_reached(filtered,
beforeWindowSize, ignoreDuring, afterWindowLength, error)
else:
logger.info("Lag trend to large for first window")
return False
logger.info("SLO evaluator is online")
\ No newline at end of file
import unittest
from main import app
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-fail.json') as json_file:
data = json.load(json_file)
response = self.client.post("/", json=data)
print(f'respose: {response}')
self.assertEqual(response.json(), False)
def test_2_rep(self):
with open('../resources/test-2-rep-success.json') as json_file:
data = json.load(json_file)
response = self.client.post("/", json=data)
print(f'respose: {response}')
self.assertEqual(response.json(), True)
if __name__ == '__main__':
unittest.main()
\ No newline at end of file
requests
fastapi>=0.68.0,<0.69.0
uvicorn>=0.15.0,<0.16.0
numpy>=1.22.4
#pydantic>=1.8.0,<2.0.0
scikit-learn==0.22.2
pandas==1.0.3
{
"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,
"3970.0000000000005"
]
]
}
]
],
"metadata": {
"warmup": "60",
"beforeWindowLength": "48",
"ignoreDuring": 5,
"afterWindowLength": 10,
"error": 0.1,
"ratio": 0.1
}
}
\ No newline at end of file
{
"results": [
[
{
"metric": {
"consumergroup": "theodolite-uc1-application-0.0.1"
},
"values": [
[
1659012042.578,
"42222"
],
[
1659012047.578,
"174052"
],
[
1659012052.578,
"594895"
],
[
1659012057.578,
"905226"
],
[
1659012062.578,
"788174"
],
[
1659012067.578,
"597812"
],
[
1659012072.578,
"445247"
],
[
1659012077.578,
"230365"
],
[
1659012082.578,
"96352"
],
[
1659012087.578,
"11577"
],
[
1659012092.578,
"6705"
],
[
1659012097.578,
"7265"
],
[
1659012102.578,
"4077"
],
[
1659012107.578,
"3281"
],
[
1659012112.578,
"467"
],
[
1659012117.578,
"3431"
],
[
1659012122.578,
"1251"
],
[
1659012127.578,
"13310"
],
[
1659012132.578,
"2365"
],
[
1659012137.578,
"4378"
],
[
1659012142.578,
"2681"
],
[
1659012147.578,
"3924"
],
[
1659012152.578,
"1701"
],
[
1659012157.578,
"323"
],
[
1659012162.578,
"2772"
],
[
1659012167.578,
"2610"
],
[
1659012172.578,
"1120"
],
[
1659012177.578,
"3545"
],
[
1659012182.578,
"1326"
],
[
1659012187.578,
"2790"
],
[
1659012192.578,
"1757"
],
[
1659012197.578,
"4591"
],
[
1659012202.578,
"733"
],
[
1659012207.578,
"2248"
],
[
1659012212.578,
"2204"
],
[
1659012217.578,
"2630"
],
[
1659012222.578,
"1093"
],
[
1659012227.578,
"4766"
],
[
1659012232.578,
"2219"
],
[
1659012237.578,
"3511"
],
[
1659012242.578,
"1618"
],
[
1659012247.578,
"1205"
],
[
1659012252.578,
"2611"
],
[
1659012257.578,
"2518"
],
[
1659012262.578,
"1819"
],
[
1659012267.578,
"1692"
],
[
1659012272.578,
"1937"
],
[
1659012277.578,
"2677"
],
[
1659012282.578,
"1999"
],
[
1659012287.578,
"4382"
],
[
1659012292.578,
"2039"
],
[
1659012297.578,
"2186"
],
[
1659012302.578,
"1604"
],
[
1659012307.578,
"9087"
],
[
1659012312.578,
"407"
],
[
1659012317.578,
"539"
],
[
1659012322.578,
"738"
],
[
1659012327.578,
"2654"
],
[
1659012332.578,
"2251"
],
[
1659012337.578,
"1593"
],
[
1659012342.578,
"1737"
],
[
1659012347.578,
"3856"
],
[
1659012352.578,
"1772"
],
[
1659012357.578,
"3599"
],
[
1659012362.578,
"3876"
],
[
1659012367.578,
"3242"
],
[
1659012372.578,
"612"
],
[
1659012377.578,
"3645"
],
[
1659012382.578,
"776"
],
[
1659012387.578,
"2343"
],
[
1659012392.578,
"2253"
],
[
1659012397.578,
"2856"
],
[
1659012402.578,
"1976"
],
[
1659012407.578,
"4079"
],
[
1659012412.578,
"561"
],
[
1659012417.578,
"2981"
],
[
1659012422.578,
"3499"
],
[
1659012427.578,
"2745"
],
[
1659012432.578,
"121"
],
[
1659012437.578,
"2132"
],
[
1659012442.578,
"95"
],
[
1659012447.578,
"1950"
],
[
1659012452.578,
"1941"
],
[
1659012457.578,
"886"
]
]
}
]
],
"metadata": {
"warmup": 60,
"beforeWindowLength": 60,
"ignoreDuring": 0,
"afterWindowLength": 10,
"error": 10.0,
"ratio": 3000.0
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment