Reworking and unifying log levels.

This commit is contained in:
Roland 2025-12-16 19:20:04 -07:00
parent 628b9f3096
commit 42f5607281
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
6 changed files with 89 additions and 18 deletions

View file

@ -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"]

View file

@ -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)

View file

@ -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

65
wrolpi/log_levels.py Normal file
View file

@ -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

View file

@ -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)

View file

@ -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', ''))