Files
monlet/server/monlet_server/settings.py

123 lines
4.2 KiB
Python

from pydantic import Field, 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_concurrency: int = Field(default=4, ge=1)
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