from __future__ import annotations from functools import lru_cache from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) app_name: str = "otel-bi-backend" app_env: str = "dev" log_level: str = "INFO" api_host: str = "0.0.0.0" api_port: int = 8000 cors_origins: str = "http://localhost:5173" # Go analytics service analytics_service_url: str = "http://localhost:8080" # PostgreSQL — write store for derived data postgres_host: str = "localhost" postgres_port: int = 5432 postgres_database: str = "otel_bi" postgres_username: str = "otel_bi" postgres_password: str = "otel_bi" postgres_sslmode: str = "prefer" postgres_connection_string: str | None = None # Frontend OIDC JWT validation require_frontend_auth: bool = True frontend_jwt_issuer_url: str = "" frontend_jwt_audience: str = "" frontend_jwt_jwks_url: str | None = None frontend_jwt_algorithm: str = "RS256" frontend_required_scopes: str = "" frontend_clock_skew_seconds: int = Field(default=30, ge=0, le=300) # Frontend OIDC client config (served via /api/config) frontend_oidc_client_id: str = "" frontend_oidc_scope: str = "openid profile email" # OpenTelemetry otel_service_name: str = "otel-bi-backend" otel_service_namespace: str = "final-thesis" otel_collector_endpoint: str = "http://localhost:4318" otel_export_timeout_ms: int = 10000 # Browser-reachable OTLP endpoint — served to the SPA via GET /api/config. # Distinct from otel_collector_endpoint, which is the backend's own # in-cluster collector address. frontend_otel_collector_endpoint: str = "/otel" frontend_otel_service_name: str = "otel-bi-frontend" frontend_otel_service_namespace: str = "final-thesis" frontend_deployment_environment: str = "production" # Report output — points at the K8s CSI / SMB mountpoint in production report_output_dir: str = "/tmp/otel-bi-reports" # Analytics defaults (forwarded to Go service as query params) forecast_horizon_days: int = Field(default=30, ge=7, le=180) default_history_days: int = Field(default=365, ge=30, le=1460) ranking_default_top_n: int = Field(default=10, ge=3, le=100) storage_default_limit: int = Field(default=50, ge=10, le=500) @property def cors_origins_list(self) -> list[str]: return [o.strip() for o in self.cors_origins.split(",") if o.strip()] @property def frontend_required_scopes_list(self) -> list[str]: return [s.strip() for s in self.frontend_required_scopes.split(" ") if s.strip()] @property def postgres_connection_url(self) -> str: if self.postgres_connection_string: return self.postgres_connection_string from urllib.parse import quote_plus user = quote_plus(self.postgres_username) password = quote_plus(self.postgres_password) return ( f"postgresql+psycopg://{user}:{password}@{self.postgres_host}:{self.postgres_port}" f"/{self.postgres_database}?sslmode={self.postgres_sslmode}" ) @lru_cache def get_settings() -> Settings: return Settings() settings = get_settings()