122 lines
4.2 KiB
Python
122 lines
4.2 KiB
Python
from pydantic import field_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
from .timeutil import validate_time_zone
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_prefix="MONLET_", env_file=".env", extra="ignore")
|
|
|
|
auth_token: str
|
|
database_url: str = "postgresql+asyncpg://monlet:monlet@127.0.0.1:5432/monlet"
|
|
log_level: str = "INFO"
|
|
body_limit_bytes: int = 1_048_576
|
|
output_max_bytes: int = 8192
|
|
max_batch_events: int = 200
|
|
stale_after_sec: int = 20
|
|
dead_after_sec: int = 30
|
|
detector_tick_sec: int = 5
|
|
host: str = "0.0.0.0"
|
|
port: int = 8000
|
|
enable_detector: bool = True
|
|
enable_notifier_worker: bool = True
|
|
events_retention_months: int = 36
|
|
events_future_partitions: int = 3
|
|
event_dedup_retention_days: int = 30
|
|
event_dedup_prune_batch_size: int = 5000
|
|
max_pending_agents: int = 10_000
|
|
pending_agent_ttl_days: int = 7
|
|
pending_agent_prune_batch_size: int = 1000
|
|
partition_maintenance_interval_sec: int = 3600
|
|
outbox_retention_max_rows: int = 50_000
|
|
# PH-012: bounded prune per maintenance tick to avoid one huge DELETE.
|
|
outbox_prune_batch_size: int = 5000
|
|
# PH-016: explicit DB pool sizing for production tuning.
|
|
db_pool_size: int = 10
|
|
db_max_overflow: int = 10
|
|
db_pool_timeout_sec: float = 30.0
|
|
db_pool_recycle_sec: int = 1800
|
|
notifier_tick_sec: int = 5
|
|
notifier_batch_size: int = 20
|
|
notifier_max_attempts: int = 8
|
|
notifier_lease_sec: int = 300
|
|
notifier_http_timeout_sec: float = 10.0
|
|
notification_time_zone: str = "UTC"
|
|
|
|
# PH-020: notifier output policy. include controls whether output text is
|
|
# sent at all; max_bytes truncates it before delivery (after redaction).
|
|
notifier_include_output: bool = True
|
|
notifier_max_output_bytes: int = 1024
|
|
|
|
notifier_debug_enabled: bool = True
|
|
notifier_telegram_enabled: bool = False
|
|
notifier_telegram_token: str = ""
|
|
notifier_telegram_chat_id: str = ""
|
|
notifier_webhook_enabled: bool = False
|
|
notifier_webhook_url: str = ""
|
|
notifier_webhook_token: str = ""
|
|
notifier_alertmanager_enabled: bool = False
|
|
notifier_alertmanager_url: str = ""
|
|
|
|
@staticmethod
|
|
def _validate_token_list(value: str, env_name: str) -> str:
|
|
if not value or value == "changeme":
|
|
raise ValueError(f"{env_name} must be set to a non-default value")
|
|
tokens = [t for t in value.replace(",", " ").split() if t]
|
|
if not tokens:
|
|
raise ValueError(f"{env_name} must contain at least one token")
|
|
if any(t == "changeme" for t in tokens):
|
|
raise ValueError(f"{env_name} must be set to a non-default value")
|
|
return value
|
|
|
|
@field_validator("auth_token")
|
|
@classmethod
|
|
def _validate_auth_token(cls, value: str) -> str:
|
|
return cls._validate_token_list(value, "MONLET_AUTH_TOKEN")
|
|
|
|
@property
|
|
def auth_tokens(self) -> list[str]:
|
|
"""Active Bearer tokens (>=1). Multiple tokens enable overlap rotation."""
|
|
return [t for t in self.auth_token.replace(",", " ").split() if t]
|
|
|
|
@field_validator("notification_time_zone")
|
|
@classmethod
|
|
def _validate_notification_time_zone(cls, value: str) -> str:
|
|
return validate_time_zone(value)
|
|
|
|
@property
|
|
def enabled_notifiers(self) -> list[str]:
|
|
out: list[str] = []
|
|
if self.notifier_debug_enabled:
|
|
out.append("debug")
|
|
if (
|
|
self.notifier_telegram_enabled
|
|
and self.notifier_telegram_token
|
|
and self.notifier_telegram_chat_id
|
|
):
|
|
out.append("telegram")
|
|
if self.notifier_webhook_enabled and self.notifier_webhook_url:
|
|
out.append("webhook")
|
|
if self.notifier_alertmanager_enabled and self.notifier_alertmanager_url:
|
|
out.append("alertmanager")
|
|
return out
|
|
|
|
@property
|
|
def sync_database_url(self) -> str:
|
|
return self.database_url.replace("+asyncpg", "+psycopg")
|
|
|
|
|
|
_settings: Settings | None = None
|
|
|
|
|
|
def get_settings() -> Settings:
|
|
global _settings
|
|
if _settings is None:
|
|
_settings = Settings()
|
|
return _settings
|
|
|
|
|
|
def reset_settings_cache() -> None:
|
|
global _settings
|
|
_settings = None
|