Skip to content

Commit 77125de

Browse files
committed
Migrate config to pydantic-settings
1 parent acf90e7 commit 77125de

6 files changed

Lines changed: 51 additions & 92 deletions

File tree

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
loki_api_url=http://localhost:3100/
2+
loki_jobs=["default/bot"]
3+
4+
discord_webhook_url=some_discord_webhook
5+
6+
service_interval_minutes=5
7+
service_tokens=[{"token":"ERROR","color":"#ff5f5f"},{"token":"WARN","color":"#ffe24d"},{"token":"INFO"}]

olli/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
from loguru import logger
77

88
from olli.alert import run
9-
from olli.config import CONFIG
9+
from olli.config import SERVICE_CONFIG
1010

1111

1212
@logger.catch
1313
def start() -> None:
1414
"""Start the Olli process."""
1515
logger.info("Starting Olli")
16-
schedule.every(CONFIG.olli.interval_minutes).minutes.do(run)
16+
schedule.every(SERVICE_CONFIG.interval_minutes).minutes.do(run)
1717

1818
run()
1919

olli/alert.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,27 @@
44

55
from olli import webhook
66
from olli.api import LokiHTTPClient
7-
from olli.config import CONFIG, TokenConfig
7+
from olli.config import SERVICE_CONFIG, TokenConfig
88
from olli.structures import TokenMatch
99

1010
api_client = LokiHTTPClient()
1111

1212

1313
def get_match(token: TokenConfig) -> TokenMatch:
1414
"""Search the configured service logs for a given token."""
15+
logger.debug(f"Searching for token {token.token}")
1516
try:
16-
logger.debug(f"Searching for token {token.token}")
1717
svc_logs = api_client.get_token_logs(token)
1818
except httpx.ConnectError:
1919
logger.error("Could not connect to Loki")
2020
return webhook.send_olli_error("Loki refused to connect.")
2121
except httpx.HTTPStatusError as e:
2222
logger.error(f"Loki returned error status code: {e.response.status_code}")
23-
return webhook.send_olli_error(
24-
f"Loki returned a bad status code: `{e.response.status_code}`"
25-
)
23+
return webhook.send_olli_error(f"Loki returned a bad status code: `{e.response.status_code}`")
2624

2725
if svc_logs["status"] != "success":
2826
logger.error(f"Received an error response from Loki for token {token.token}")
29-
return webhook.send_olli_error(
30-
"Loki returned an error, check your service names are correct."
31-
)
27+
return webhook.send_olli_error("Loki returned an error, check your service names are correct.")
3228

3329
match = TokenMatch(token, {})
3430

@@ -57,7 +53,7 @@ def run() -> None:
5753
logger.info("Running Olli search")
5854
matches = []
5955

60-
for token in CONFIG.olli.tokens:
56+
for token in SERVICE_CONFIG.tokens:
6157
if match := get_match(token):
6258
matches.append(match)
6359

olli/api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import httpx
66

7-
from olli.config import CONFIG, TokenConfig
7+
from olli.config import LOKI_CONFIG, SERVICE_CONFIG, TokenConfig
88

99

1010
class LokiHTTPClient:
@@ -13,7 +13,7 @@ class LokiHTTPClient:
1313
@staticmethod
1414
def route(path: str) -> str:
1515
"""Generate the Loki API route for a given path."""
16-
return CONFIG.loki.api_url + "/loki/api/v1/" + path
16+
return LOKI_CONFIG.api_url + "/loki/api/v1/" + path
1717

1818
def get_token_logs(self, token: TokenConfig) -> dict[str, Any]:
1919
"""
@@ -22,18 +22,18 @@ def get_token_logs(self, token: TokenConfig) -> dict[str, Any]:
2222
The term is searched case-insensitively in the logs for the interval
2323
configured in the config.toml file.
2424
"""
25-
td = datetime.timedelta(minutes=CONFIG.olli.interval_minutes)
25+
td = datetime.timedelta(minutes=SERVICE_CONFIG.interval_minutes)
2626
start_time = datetime.datetime.now() - td
2727
start_ts = start_time.timestamp() * 1_000_000_000
2828

29-
job_regex = "|".join(CONFIG.loki.jobs)
29+
job_regex = "|".join(LOKI_CONFIG.jobs)
3030

3131
case_filter = '(?i)' if not token.case_sensitive else ''
3232

3333
resp = httpx.get(self.route("query_range"), params={
3434
"query": f'{{job=~"({job_regex})"}} |~ "{case_filter}{token.token}"',
3535
"start": f"{start_ts:0.0f}",
36-
"limit": CONFIG.loki.max_logs
36+
"limit": LOKI_CONFIG.max_logs,
3737
})
3838

3939
resp.raise_for_status()

olli/config.py

Lines changed: 27 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,51 @@
11
"""Configuration loading and validation."""
2-
import os
3-
from pathlib import Path
2+
43
from typing import Optional
54

6-
import tomllib
7-
from dotenv import load_dotenv
85
from loguru import logger
9-
from pydantic import BaseModel, validator
10-
11-
load_dotenv()
6+
from pydantic import BaseModel
7+
from pydantic_settings import BaseSettings
128

139

14-
# All the locations where we might find a config file
15-
CONFIG_PATHS = [
16-
"./olli.toml",
17-
"/config/olli.toml",
18-
"/olli/config.toml",
19-
"/etc/olli/config.toml"
20-
]
10+
class EnvConfig(
11+
BaseSettings,
12+
env_file=".env",
13+
env_file_encoding="utf-8",
14+
env_nested_delimiter="__",
15+
extra="ignore",
16+
):
17+
"""Our default configuration for models that should load from .env files."""
2118

2219

23-
class TokenConfig(BaseModel):
24-
"""Class representing a token config entry."""
25-
26-
token: str
27-
color: Optional[str] = "#7289DA"
28-
case_sensitive: Optional[bool] = False
29-
30-
31-
class LokiConfig(BaseModel):
20+
class _LokiConfig(EnvConfig, env_prefix="loki_"):
3221
"""Loki specific configuration."""
3322

3423
api_url: str
3524
jobs: list[str]
3625
max_logs: Optional[int] = 5_000
3726

3827

39-
class DiscordConfig(BaseModel):
28+
LOKI_CONFIG = _LokiConfig()
29+
30+
31+
class _DiscordConfig(EnvConfig, env_prefix="discord_"):
4032
"""Configuration for Discord alerting."""
4133

4234
webhook_url: Optional[str]
4335

44-
@validator("webhook_url", always=True)
45-
def env_provided_webhook(cls, value: Optional[str]) -> str:
46-
"""
47-
If no webhook is specified in the config, try fetch from environment.
4836

49-
If not found in the environment either then raise a validation error.
50-
"""
51-
if value:
52-
return value
37+
DISCORD_CONFIG = _DiscordConfig()
5338

54-
if webhook := os.environ.get("WEBHOOK_URL"):
55-
return webhook
5639

57-
raise ValueError(
58-
"Must specify webhook_url under [discord] or WEBHOOK_URL env var"
59-
)
40+
class TokenConfig(BaseModel):
41+
"""Class representing a token config entry."""
6042

43+
token: str
44+
color: Optional[str] = "#7289DA"
45+
case_sensitive: Optional[bool] = False
6146

62-
class ServiceConfig(BaseModel):
47+
48+
class _ServiceConfig(EnvConfig, env_prefix="service_"):
6349
"""Configuration of the Olli status."""
6450

6551
interval_minutes: int
@@ -81,40 +67,10 @@ def warn_above_ten(cls, value: list[TokenConfig]) -> list[TokenConfig]:
8167
This is because we cannot handle more than 10 token triggers at once until we
8268
batch triggers into groups of 10 to distribute to the webhook.
8369
"""
84-
if len(value) > 10:
85-
logger.warning(
86-
"More than 10 token triggers in one period cannot be handled, be careful."
87-
)
70+
if len(value) > 10: # noqa: PLR2004
71+
logger.warning("More than 10 token triggers in one period cannot be handled, be careful.")
8872

8973
return value
9074

9175

92-
class OlliConfig(BaseModel):
93-
"""Class representing root Olli config."""
94-
95-
loki: LokiConfig
96-
olli: ServiceConfig
97-
discord: DiscordConfig = DiscordConfig()
98-
99-
100-
def get_config() -> OlliConfig:
101-
"""Open the config file, parse the TOML and convert to Pydantic objects."""
102-
logger.info("Searching for config file")
103-
104-
path = None
105-
106-
for file_path in CONFIG_PATHS:
107-
if (config_file := Path(file_path)).exists():
108-
path = config_file
109-
logger.info(f"Found config at {file_path}")
110-
break
111-
112-
if not path:
113-
logger.critical("Could not find a config file. Please refer to the documentation.")
114-
raise SystemExit(1)
115-
116-
with open(config_file, "rb") as conf_file:
117-
return OlliConfig(**tomllib.load(conf_file))
118-
119-
120-
CONFIG = get_config()
76+
SERVICE_CONFIG = _ServiceConfig()

olli/webhook.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import httpx
66
from loguru import logger
77

8-
from olli.config import CONFIG
8+
from olli.config import DISCORD_CONFIG, SERVICE_CONFIG
99
from olli.structures import TokenMatch
1010

1111

@@ -26,12 +26,12 @@ def send_with_backoff(url: str, json: dict[str, Any], n: int = 5) -> None:
2626
def send_olli_error(error: str) -> None:
2727
"""Send an error embed containing the passed error message to Discord."""
2828
logger.info("Sending error payload to Discord")
29-
send_with_backoff(CONFIG.discord.webhook_url, {
29+
send_with_backoff(DISCORD_CONFIG.webhook_url, {
3030
"embeds": [{
3131
"title": "Olli Error",
3232
"color": 0xff5f5f,
3333
"description": f"Olli encountered an error: {error}",
34-
"timestamp": datetime.utcnow().isoformat()
34+
"timestamp": datetime.utcnow().isoformat(),
3535
}]
3636
})
3737

@@ -56,7 +56,7 @@ def send_token_matches(matches: list[TokenMatch]) -> None:
5656
"name": "Olli"
5757
},
5858
"footer": {
59-
"text": f"Last {CONFIG.olli.interval_minutes} minutes"
59+
"text": f"Last {SERVICE_CONFIG.interval_minutes} minutes",
6060
},
6161
"timestamp": datetime.utcnow().isoformat(),
6262
"fields": []
@@ -73,7 +73,7 @@ def send_token_matches(matches: list[TokenMatch]) -> None:
7373

7474
if len(embeds) > 0:
7575
logger.info("Sending alerts payload to Discord")
76-
send_with_backoff(CONFIG.discord.webhook_url, {
76+
send_with_backoff(DISCORD_CONFIG.webhook_url, {
7777
"embeds": embeds
7878
})
7979
else:

0 commit comments

Comments
 (0)