feat: add configurable logging level for backend and Mini App

- Introduced a new `LOG_LEVEL` configuration option in the `.env.example` file to allow users to set the logging level (DEBUG, INFO, WARNING, ERROR).
- Updated the `Settings` class to include the `log_level` attribute, normalizing its value to ensure valid logging levels are used.
- Modified the logging setup in `run.py` to utilize the configured log level, enhancing flexibility in log management.
- Enhanced the Mini App to include the logging level in the JavaScript configuration, allowing for consistent logging behavior across the application.
- Added a new `logger.js` module for frontend logging, implementing level-based filtering and console delegation.
- Included unit tests for the new logger functionality to ensure proper behavior and level handling.
This commit is contained in:
2026-03-02 23:15:22 +03:00
parent 67ba9826c7
commit 43386b15fa
16 changed files with 226 additions and 15 deletions

View File

@@ -93,7 +93,11 @@ class NoCacheStaticMiddleware:
headers[idx] = (b"vary", existing + b", " + vary_val)
else:
headers.append((b"vary", vary_val))
message = {"type": "http.response.start", "status": message["status"], "headers": headers}
message = {
"type": "http.response.start",
"status": message["status"],
"headers": headers,
}
await send(message)
await self.app(scope, receive, send_wrapper)
@@ -104,13 +108,16 @@ app.add_middleware(NoCacheStaticMiddleware)
@app.get(
"/app/config.js",
summary="Mini App config (language)",
description="Returns JS that sets window.__DT_LANG from DEFAULT_LANGUAGE. Loaded before main.js.",
summary="Mini App config (language, log level)",
description=(
"Returns JS that sets window.__DT_LANG and window.__DT_LOG_LEVEL. Loaded before main.js."
),
)
def app_config_js() -> Response:
"""Return JS assigning window.__DT_LANG for the webapp. No caching."""
"""Return JS assigning window.__DT_LANG and window.__DT_LOG_LEVEL for the webapp. No caching."""
lang = config.DEFAULT_LANGUAGE
body = f'window.__DT_LANG = "{lang}";'
log_level = config.LOG_LEVEL_STR.lower()
body = f'window.__DT_LANG = "{lang}";\nwindow.__DT_LOG_LEVEL = "{log_level}";'
return Response(
content=body,
media_type="application/javascript; charset=utf-8",

View File

@@ -20,6 +20,7 @@ from duty_teller.utils.dates import DateRangeValidationError, validate_date_rang
log = logging.getLogger(__name__)
def _lang_from_accept_language(header: str | None) -> str:
"""Return the application language: always config.DEFAULT_LANGUAGE.

View File

@@ -4,6 +4,7 @@ BOT_TOKEN is not validated on import; call require_bot_token() in the entry poin
when running the bot.
"""
import logging
import os
import re
from dataclasses import dataclass
@@ -46,6 +47,14 @@ def _parse_phone_list(raw: str) -> set[str]:
return result
def _normalize_log_level(raw: str) -> str:
"""Return a valid log level name (DEBUG, INFO, WARNING, ERROR); default INFO."""
level = (raw or "").strip().upper()
if level in ("DEBUG", "INFO", "WARNING", "ERROR"):
return level
return "INFO"
@dataclass(frozen=True)
class Settings:
"""Injectable settings built from environment. Used in tests or when env is overridden."""
@@ -67,6 +76,7 @@ class Settings:
duty_display_tz: str
default_language: str
duty_pin_notify: bool
log_level: str
@classmethod
def from_env(cls) -> "Settings":
@@ -118,6 +128,7 @@ class Settings:
default_language=normalize_lang(os.getenv("DEFAULT_LANGUAGE", "en")),
duty_pin_notify=os.getenv("DUTY_PIN_NOTIFY", "1").strip().lower()
not in ("0", "false", "no"),
log_level=_normalize_log_level(os.getenv("LOG_LEVEL", "INFO")),
)
@@ -141,6 +152,8 @@ EXTERNAL_CALENDAR_ICS_URL = _settings.external_calendar_ics_url
DUTY_DISPLAY_TZ = _settings.duty_display_tz
DEFAULT_LANGUAGE = _settings.default_language
DUTY_PIN_NOTIFY = _settings.duty_pin_notify
LOG_LEVEL = getattr(logging, _settings.log_level.upper(), logging.INFO)
LOG_LEVEL_STR = _settings.log_level
def is_admin(username: str) -> bool:

View File

@@ -24,7 +24,7 @@ async def _resolve_bot_username(application) -> None:
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO,
level=config.LOG_LEVEL,
)
logger = logging.getLogger(__name__)