diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 77f0ddfb..663d5fad 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -18,4 +18,5 @@ COPY wrolpi /opt/wrolpi/wrolpi COPY modules /opt/wrolpi/modules ENTRYPOINT [ "python3", "-OO", "/opt/wrolpi/main.py"] -CMD ["-v", "api", "--host", "0.0.0.0"] +# Log level controlled by LOG_LEVEL env var or wrolpi.yaml config (default: info) +CMD ["api", "--host", "0.0.0.0"] diff --git a/wrolpi/common.py b/wrolpi/common.py index 207ccdde..296e50eb 100644 --- a/wrolpi/common.py +++ b/wrolpi/common.py @@ -43,6 +43,7 @@ from sqlalchemy.orm import Session from wrolpi.dates import now, from_timestamp from wrolpi.errors import WROLModeEnabled, NativeOnly, LogLevelError, InvalidConfig, \ ValidationError +from wrolpi.log_levels import TRACE_LEVEL, name_to_int as log_level_name_to_int, int_to_name as log_level_int_to_name from wrolpi.vars import PYTEST, DOCKERIZED, CONFIG_DIR, MEDIA_DIRECTORY, DEFAULT_HTTP_HEADERS @@ -176,8 +177,7 @@ class TraceLogger(logging.Logger): logger: TraceLogger = logging.getLogger() # noqa Not really a TraceLogger, but I wanted IDE suggestions. logging.config.dictConfig(LOGGING_CONFIG) -# Add extra verbose debug level (-vvv) -TRACE_LEVEL = logging.DEBUG - 5 +# Add extra verbose debug level (-vvv). TRACE_LEVEL imported from log_levels. if not hasattr(logging, 'TRACE'): add_logging_level('TRACE', TRACE_LEVEL) @@ -705,6 +705,7 @@ class WROLPiConfigValidator: hotspot_password: str = None hotspot_ssid: str = None ignore_outdated_zims: bool = None + log_level: str = None map_destination: str = None nav_color: str = None throttle_on_startup: bool = None @@ -769,6 +770,7 @@ class WROLPiConfig(ConfigFile): check_for_upgrades=True, ignore_outdated_zims=False, ignored_directories=['config', 'tags'], + log_level='info', map_destination='map', nav_color='violet', throttle_on_startup=False, @@ -877,6 +879,14 @@ class WROLPiConfig(ConfigFile): def ignore_outdated_zims(self, value: bool): self.update({'ignore_outdated_zims': value}) + @property + def log_level(self) -> str: + return self._config.get('log_level', 'info') + + @log_level.setter + def log_level(self, value: str): + self.update({'log_level': value}) + @property def check_for_upgrades(self) -> bool: return self._config.get('check_for_upgrades', True) diff --git a/wrolpi/conftest.py b/wrolpi/conftest.py index 4bcb9ea5..cf60a2b3 100644 --- a/wrolpi/conftest.py +++ b/wrolpi/conftest.py @@ -100,13 +100,9 @@ def test_session() -> Session: @pytest.fixture(autouse=True) def test_debug_logger(request): - """The tests use debug logging by default. Use pytest's verbosity level if set.""" - level = logging.INFO - verbose = request.config.option.verbose - if verbose == 2: - # -vv - level = logging.DEBUG - elif verbose == 3: + """Tests default to DEBUG logging. Use pytest -vvv for TRACE level.""" + level = logging.DEBUG + if request.config.option.verbose >= 3: # -vvv level = TRACE_LEVEL diff --git a/wrolpi/log_levels.py b/wrolpi/log_levels.py new file mode 100644 index 00000000..05d1870e --- /dev/null +++ b/wrolpi/log_levels.py @@ -0,0 +1,65 @@ +""" +Log level utilities. + +IMPORTANT: Keep this module minimal with NO imports from wrolpi to avoid circular imports. +This module is imported very early by vars.py. +""" +import logging +import os +from pathlib import Path + +TRACE_LEVEL = 5 + +LOG_LEVELS = { + 'critical': logging.CRITICAL, + 'warning': logging.WARNING, + 'info': logging.INFO, + 'debug': logging.DEBUG, + 'trace': TRACE_LEVEL, +} + + +def name_to_int(name: str) -> int: + """Convert log level name to integer. Defaults to INFO for invalid names.""" + return LOG_LEVELS.get(str(name).lower(), logging.INFO) + + +def int_to_name(level: int) -> str: + """Convert log level integer to name. Defaults to 'info' for invalid levels.""" + for name, value in LOG_LEVELS.items(): + if value == level: + return name + return 'info' + + +def get_log_level_from_env_or_config() -> int: + """ + Get log level from environment variable or config file. + + Priority: + 1. LOG_LEVEL environment variable + 2. log_level in wrolpi.yaml config file + 3. Default to INFO + + NOTE: CLI flags are handled separately by main.py argparse and take highest priority. + This function is called during vars.py import (very early), before argparse runs. + """ + # 1. Environment variable (highest priority at this stage) + if env_level := os.environ.get('LOG_LEVEL'): + return name_to_int(env_level) + + # 2. Config file + try: + import yaml + media_dir = Path(os.environ.get('MEDIA_DIRECTORY', '/media/wrolpi')) + config_path = media_dir / 'config/wrolpi.yaml' + if config_path.is_file(): + with open(config_path) as f: + config = yaml.safe_load(f) + if config and 'log_level' in config: + return name_to_int(config['log_level']) + except Exception: + pass # Config doesn't exist or is invalid, use default + + # 3. Default + return logging.INFO diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py index 808eda23..34a1690b 100644 --- a/wrolpi/root_api.py +++ b/wrolpi/root_api.py @@ -24,7 +24,7 @@ from wrolpi.api_utils import json_response, api_app from wrolpi.collections.api import collection_bp from wrolpi.common import logger, get_wrolpi_config, wrol_mode_enabled, get_media_directory, \ wrol_mode_check, native_only, disable_wrol_mode, enable_wrol_mode, get_global_statistics, url_strip_host, \ - set_global_log_level, get_relative_to_media_directory, search_other_estimates + set_global_log_level, get_relative_to_media_directory, search_other_estimates, log_level_int_to_name from wrolpi.config_api import config_bp from wrolpi.dates import now from wrolpi.downloader import download_manager @@ -232,6 +232,8 @@ async def update_settings(_: Request, body: schema.SettingsRequest): log_level = new_config.pop('log_level', None) if isinstance(log_level, int): set_global_log_level(log_level) + # Persist log level to config + wrolpi_config.log_level = log_level_int_to_name(log_level) old_password = wrolpi_config.hotspot_password wrolpi_config.update(new_config) diff --git a/wrolpi/vars.py b/wrolpi/vars.py index a9b8e95e..2818e71b 100644 --- a/wrolpi/vars.py +++ b/wrolpi/vars.py @@ -1,4 +1,3 @@ -import logging import multiprocessing import os import pathlib @@ -45,12 +44,10 @@ def list_var(name: str, default='') -> List[str]: return [i for i in os.environ.get(name, default).split(',')] -LOG_LEVEL_INT = logging.INFO -if '-vv' in sys.argv: - LOG_LEVEL_INT = logging.DEBUG -elif '-vvv' in sys.argv: - # SQLAlchemy will increase logging. - LOG_LEVEL_INT = logging.DEBUG - 5 +from wrolpi.log_levels import get_log_level_from_env_or_config + +# Get initial log level from ENV or config. CLI flags are handled by main.py argparse. +LOG_LEVEL_INT = get_log_level_from_env_or_config() # Special environment variable set in the docker/api/Dockerfile. DOCKERIZED = truthy_arg(os.environ.get('DOCKER', ''))