Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

refactor(api): Refactor some API functions and classes #53

Merged
merged 21 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ enhances flexibility and automation in monitoring and alerting workflows.
### Prerequisites

The following prerequisites are required to get up and running with this tool:
- The Prometheus server's rules directory must be shared and accessible
- The Prometheus lifecycle API must be enabled to allow requesting the /reload API
- Prometheus server's rules directory and configuration file (prometheus.yml) must be shared and accessible
- Prometheus lifecycle API must be enabled to allow requesting the /reload API

### Quick Start

Expand Down
Binary file modified docs/images/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ email-validator==2.0.0
APScheduler==3.10.4
pytimeparse2==1.7.1
jsonschema==4.17.3
starlette==0.35.1
requests==2.28.2
pydantic==1.10.7
fastapi==0.109.0
Expand Down
36 changes: 18 additions & 18 deletions src/api/v1/endpoints/configs.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from src.core.configs import partial_update, rename_global_keyword
from fastapi import APIRouter, Response, Request, Body, status
from src.core.prometheus import PrometheusAPIClient
from src.utils.validations import validate_schema
from fastapi.responses import PlainTextResponse
from src.core.export import validate_request
from src.models.config import UpdateConfig
from src.core import configs as cfg
from src.models.rule import Rule
from src.utils.log import logger
from typing import Annotated
import yaml

router = APIRouter()
prometheus = Rule()
prom = PrometheusAPIClient()


@router.get("/configs",
Expand Down Expand Up @@ -84,7 +84,7 @@ async def get_config(
request: Request,
response: (Response or PlainTextResponse)
):
cfg_status, response.status_code, cfg_dict = cfg.get_prometheus_config()
cfg_status, response.status_code, cfg_dict = prom.get_config()
if cfg_status:
if request.headers.get("content-type") == "application/yaml":
data_yaml = yaml.dump(
Expand Down Expand Up @@ -190,17 +190,17 @@ async def update_config(
],
):
user_data = data.dict(exclude_unset=True)
cfg.rename_global_keyword(user_data)
rename_global_keyword(user_data=user_data)
validation_status, response.status_code, sts, msg = \
validate_request("configs.json", user_data)
validate_schema("configs.json", user_data)
if validation_status:
data_yaml = yaml.dump(
user_data,
Dumper=yaml.SafeDumper,
sort_keys=False)
config_update_status, msg = cfg.update_prometheus_yml(data=data_yaml)
sort_keys=True)
config_update_status, msg = prom.update_config(data=data_yaml)
if config_update_status:
response.status_code, sts, msg = prometheus.reload()
response.status_code, sts, msg = prom.reload()
msg = "Configuration updated successfully" if sts == "success" else msg
else:
response.status_code, sts = 500, "error"
Expand Down Expand Up @@ -261,7 +261,7 @@ async def update_config(
}
}
)
async def partial_update(
async def partial_updates(
request: Request,
response: Response,
data: Annotated[
Expand Down Expand Up @@ -292,21 +292,21 @@ async def partial_update(
],
):
user_data = data.dict(exclude_unset=True)
cfg.rename_global_keyword(user_data)
rename_global_keyword(user_data)
validation_status, response.status_code, sts, msg = \
validate_request("configs.json", user_data)
validate_schema("configs.json", user_data)
if validation_status:
cfg_status, response.status_code, cfg_dict = cfg.get_prometheus_config()
cfg_status, response.status_code, cfg_dict = prom.get_config()
if cfg_status:
cfg.partial_update(user_data, cfg_dict)
partial_update(user_data, cfg_dict)
data_yaml = yaml.dump(
user_data,
Dumper=yaml.SafeDumper,
sort_keys=False)
config_update_status, msg = cfg.update_prometheus_yml(
sort_keys=True)
config_update_status, msg = prom.update_config(
data=data_yaml)
if config_update_status:
response.status_code, sts, msg = prometheus.reload()
response.status_code, sts, msg = prom.reload()
msg = "Configuration updated successfully" if sts == "success" else msg
else:
response.status_code, sts = 500, "error"
Expand Down
3 changes: 2 additions & 1 deletion src/api/v1/endpoints/export.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fastapi import APIRouter, Response, Request, Body, status
from src.utils.validations import validate_schema
from starlette.background import BackgroundTask
from fastapi.responses import FileResponse
from src.models.export import ExportData
Expand Down Expand Up @@ -73,7 +74,7 @@ async def export(
file, file_format = None, format.lower()
custom_fields, timestamp_format = data.get(
"replace_fields"), data.get("timestamp_format")
validation_status, response.status_code, sts, msg = exp.validate_request(
validation_status, response.status_code, sts, msg = validate_schema(
"export.json", data)
if validation_status:
range_query = True if all([start, end, step]) else False
Expand Down
5 changes: 3 additions & 2 deletions src/api/v1/endpoints/policies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from src.models.policy import MetricsLifecyclePolicyCreate, MetricsLifecyclePolicyUpdate
from fastapi import APIRouter, Response, Request, Body, status
from apscheduler.triggers.date import DateTrigger
from src.utils.validations import validate_schema
from src.utils.scheduler import schedule
from src.core import policies as mlp
from src.utils.log import logger
Expand Down Expand Up @@ -185,7 +186,7 @@ async def create(
data = policy.dict()
validation_status, response.status_code, sts, msg = mlp.validate_prom_admin_api()
if validation_status:
validation_status, response.status_code, sts, msg = mlp.validate_policy(
validation_status, response.status_code, sts, msg = validate_schema(
"policies_create.json", data)
if validation_status:
validation_status, response.status_code, sts, msg, val = mlp.validate_duration(
Expand Down Expand Up @@ -281,7 +282,7 @@ async def update(
data = policy.dict(exclude_unset=True)
validation_status, response.status_code, sts, msg = mlp.validate_prom_admin_api()
if validation_status:
validation_status, response.status_code, sts, msg = mlp.validate_policy(
validation_status, response.status_code, sts, msg = validate_schema(
"policies_update.json", data)
if validation_status:
validation_status, response.status_code, sts, msg, val = mlp.validate_duration(
Expand Down
120 changes: 33 additions & 87 deletions src/api/v1/endpoints/rules.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,15 @@
from fastapi import APIRouter, Response, Request, Body, status
from src.core.prometheus import PrometheusAPIClient
from src.utils.arguments import arg_parser
from string import ascii_lowercase
from src.models.rule import Rule
from src.utils.log import logger
from typing import Annotated
from random import choices
from shutil import copy
import time
import os

router = APIRouter()


def create_prometheus_rule(
rule: Rule,
request: Request,
response: Response,
file: str) -> dict:
"""
A common function for the /rules API
is used in the POST and PUT routes.
"""

while True:
validation_status, sts, msg = rule.validate_rule()
if not validation_status:
response.status_code = status.HTTP_400_BAD_REQUEST
break
create_rule_status, sts, msg = rule.create_rule(file)
if not create_rule_status:
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
break
time.sleep(0.1)
response.status_code, sts, msg = rule.reload()
if response.status_code != 200:
rule.delete_rule(file)
break
msg = "The rule was created successfully"
response.status_code = status.HTTP_201_CREATED
break

logger.info(
msg=msg,
extra={
"status": response.status_code,
"method": request.method,
"request_path": f"{request.url.path}{'?' + request.url.query if request.url.query else ''}"})

resp = {"status": sts, "message": msg}
if request.method == "POST":
resp.update({"file": file})
return resp
prom = PrometheusAPIClient()
rule_path = arg_parser().get('rule.path')


@router.post("/rules",
Expand Down Expand Up @@ -112,15 +71,14 @@ async def create(
]
):
r = Rule(data=rule.data)
file_prefix = f"{arg_parser().get('file.prefix')}-" if arg_parser().get('file.prefix') else ""
file_suffix = arg_parser().get('file.extension')

while True:
file = f"{file_prefix}{''.join(choices(ascii_lowercase, k=15))}{file_suffix}"
if os.path.exists(f"{Rule._rule_path}/{file}"):
continue
break
return create_prometheus_rule(r, request, response, file)
response.status_code, resp = prom.create_rule(r)
logger.info(
msg=resp["message"],
extra={
"status": response.status_code,
"method": request.method,
"request_path": f"{request.url.path}{'?' + request.url.query if request.url.query else ''}"})
return resp


@router.put("/rules/{file}",
Expand Down Expand Up @@ -197,27 +155,32 @@ async def update(
):
r = Rule(data=rule.data)

if file and os.path.exists(f"{Rule._rule_path}/{file}"):
if file and os.path.exists(f"{rule_path}/{file}"):
if recreate.lower() == "true":
orig_file, temp_file = f"{Rule._rule_path}/{file}", f"{Rule._rule_path}/{file}.temp"
orig_file, temp_file = f"{rule_path}/{file}", f"{rule_path}/{file}.temp"
copy(orig_file, temp_file)
resp = create_prometheus_rule(r, request, response, file)
response.status_code, resp = prom.create_rule(r, file)
if resp.get("status") == "success":
os.remove(temp_file)
return resp
os.rename(temp_file, orig_file)
return resp
else:
os.rename(temp_file, orig_file)
else:
response.status_code = status.HTTP_409_CONFLICT
resp = {
"status": "error",
"message": "The requested file already exists.",
"file": file}
else:
response.status_code, resp = prom.create_rule(r, file)

response.status_code = status.HTTP_409_CONFLICT
msg = "The requested file already exists."
logger.info(
msg=msg,
extra={
"status": response.status_code,
"method": request.method,
"request_path": f"{request.url.path}{'?' + request.url.query if request.url.query else ''}"})
return {"status": "error", "message": msg}
return create_prometheus_rule(r, request, response, file)
logger.info(
msg=resp["message"],
extra={
"status": response.status_code,
"method": request.method,
"request_path": f"{request.url.path}{'?' + request.url.query if request.url.query else ''}"})
del resp["file"]
return resp


@router.delete("/rules/{file}",
Expand Down Expand Up @@ -258,24 +221,7 @@ async def update(
}
)
async def delete(file, request: Request, response: Response):
r = Rule()

while True:
if not os.path.exists(f"{Rule._rule_path}/{file}"):
response.status_code, sts, msg = status.HTTP_404_NOT_FOUND, "error", "File not found"
break
delete_rule_status, sts, msg = r.delete_rule(file)
if not delete_rule_status:
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
break
reload_status, sts, msg = r.reload()
if reload_status != 200:
response.status_code = reload_status
break
msg = "The rule was deleted successfully"
response.status_code = status.HTTP_204_NO_CONTENT
break

response.status_code, sts, msg = prom.delete_rule(file)
logger.info(
msg=msg,
extra={
Expand Down
56 changes: 5 additions & 51 deletions src/core/configs.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,4 @@
from src.utils.arguments import arg_parser
from src.utils.log import logger
import requests
import yaml

prom_addr = arg_parser().get("prom.addr")
prom_config_file = arg_parser().get("config.file")


def get_prometheus_config(prometheus_addr=prom_addr) -> tuple[bool, int, dict]:
"""
This function returns current configuration
of Prometheus as a dictionary object
"""
try:
r = requests.request(method="GET",
url=f"{prometheus_addr}/api/v1/status/config")
except BaseException as e:
return False, 500, {"status": "error",
"error": f"Failed to connect to Prometheus. {e}"}
else:
if r.status_code == 200:
data_raw = r.json().get("data")
data = yaml.load(data_raw.get("yaml"), Loader=yaml.SafeLoader)
return True, r.status_code, data
return False, r.status_code, {"status": "error", "error": r.reason}


def update_prometheus_yml(data: str) -> tuple[bool, str]:
"""
This function updates Prometheus
configuration file (prometheus.yml)
"""
try:
with open(prom_config_file, "w") as f:
f.write(data)
except BaseException as e:
logger.error(
f"Failed to update Prometheus configuration file. {e}")
return False, str(e)
else:
logger.debug(
f"Successfully updated Prometheus configuration file: {prom_config_file}")
return True, "success"


def partial_update(user_data: dict, data: dict):
def partial_update(data: dict, user_data: dict):
"""
This function updates objects depending on their types
"""
Expand All @@ -57,12 +11,12 @@ def partial_update(user_data: dict, data: dict):
data[k] = user_data[k]


def rename_global_keyword(data: dict) -> None:
def rename_global_keyword(user_data) -> None:
""""
This function replaces the key 'global_' with 'global'
from the user's input, since Python does not allow a
key with the name 'global'
"""
if "global_" in data:
data["global"] = data["global_"]
del data["global_"]
if "global_" in user_data:
user_data["global"] = user_data["global_"]
del user_data["global_"]
Loading