diff --git a/README.md b/README.md index b658918..07636e6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,14 @@ This package was created to run my applications. The ConfigManager class implements the entry point for the program and provides the actual application configuration. It also simplifies logging setup. +## Logging (v2) +Logging is configured from the config file only if it contains a **`log`** section in [dictConfig](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) format. If there is no `log` section, the manager logs a warning and the default Python level (WARNING) remains, so INFO/DEBUG messages may not appear. + +**How to verify that logging config is applied:** +- Ensure your config file path is correct and the file is loaded on startup (no error in logs about reading config). +- Ensure the config has a `log` key with `version: 1`, `handlers`, and `loggers` (see `tests/config.yaml` for an example). +- After startup you should see an INFO message: `"Logging configuration applied"` (from `config_manager.v1.log_manager`). If you do not see it, either the `log` section is missing (you will see a warning) or the root/package log level is above INFO. + ## Installation ``pip install git+https://git.lesha.spb.ru/alex/config_manager.git`` diff --git a/pyproject.toml b/pyproject.toml index e35badc..8880460 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "config_manager" -version = "2.1.0" -description = "Добавлен API управления и контроля" +version = "2.11" +description = "Изменена логика задания таймаутов ожидания" authors = [ { name = "Aleksei Zosimov", email = "lesha.spb@gmail.com" } ] diff --git a/src/config_manager/v1/log_manager.py b/src/config_manager/v1/log_manager.py index 630041c..ea8b9ad 100644 --- a/src/config_manager/v1/log_manager.py +++ b/src/config_manager/v1/log_manager.py @@ -22,6 +22,9 @@ class LogManager: """ logging_config = config.get("log") if not logging_config: + self.logger.warning( + "Config has no 'log' section; logging parameters from config are not applied (default level may be WARNING)." + ) return try: diff --git a/src/config_manager/v2/core/manager.py b/src/config_manager/v2/core/manager.py index 350ab94..1948cb2 100644 --- a/src/config_manager/v2/core/manager.py +++ b/src/config_manager/v2/core/manager.py @@ -17,7 +17,7 @@ from ..management import ( ManagementServer, ) from ..types import HealthPayload, LifecycleState, ManagementServerSettings -from .config_loader import ConfigLoader, extract_scheduler_intervals +from .config_loader import ConfigLoader from .scheduler import WorkerLoop @@ -79,13 +79,8 @@ class ConfigManagerV2: self.logger = logging.getLogger(__name__) def _apply_config(self, new_config: Any) -> None: - """Применить загруженный конфиг: интервалы и log_manager. Вызывается после load_if_changed.""" + """Применить загруженный конфиг: log_manager. Интервалы (update_interval, work_interval) задаются только в классе/наследнике.""" self.config = new_config - self.update_interval, self.work_interval = extract_scheduler_intervals( - new_config, - self.DEFAULT_UPDATE_INTERVAL, - self.DEFAULT_WORK_INTERVAL, - ) if isinstance(new_config, dict): self._log_manager.apply_config(new_config) diff --git a/tests/config.yaml b/tests/config.yaml index ee9ff58..84f2cf1 100644 --- a/tests/config.yaml +++ b/tests/config.yaml @@ -9,8 +9,7 @@ log: formatters: standard: format: '%(asctime)s %(module)15s [%(levelname)8s]: %(message)s' - telegram: - format: '%(message)s' + handlers: console: @@ -26,28 +25,20 @@ log: filename: logs/log.log mode: a maxBytes: 500000 - backupCount: 15 - - #telegram: - # level: CRITICAL - # formatter: telegram - # class: logging_telegram_handler.TelegramHandler - # chat_id: 211945135 - # alias: "PDC" - + backupCount: 3 # -- Логгеры -- - loggers: - '': - handlers: [console, file] - level: INFO - propagate: False + root: + handlers: [console, file] + level: INFO + loggers: __main__: handlers: [console, file] level: DEBUG propagate: False - config_manager: + config_manager.src.config_manager.v2.manager: handlers: [console, file] - level: DEBUG \ No newline at end of file + level: DEBUG + diff --git a/tests/test_app.py b/tests/test_app.py index 8dce81b..371c014 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -22,7 +22,11 @@ class MyApp(ConfigManager): def execute(self) -> None: """Успешный прогон сбрасывает таймер health (обновляет время последнего успеха).""" - logger.info("current iteration %s", self.iter) + #logger.critical("current iteration %s", self.iter) + #logger.error("current iteration %s", self.iter) + logger.warning("current iteration %s", self.iter) + #logger.info("current iteration %s", self.iter) + #logger.debug("current iteration %s", self.iter) self.iter += 1 @@ -35,6 +39,7 @@ async def main() -> None: health_timeout=HEALTH_TIMEOUT, ) config_path = Path(__file__).parent / "config.yaml" + print(config_path) app = MyApp( str(config_path), log_manager=log_manager, diff --git a/tests/v2/test_config_reload_fallback.py b/tests/v2/test_config_reload_fallback.py index 3295eb3..b099e78 100644 --- a/tests/v2/test_config_reload_fallback.py +++ b/tests/v2/test_config_reload_fallback.py @@ -1,9 +1,12 @@ import asyncio -from config_manager.v2 import ConfigManagerV2 +from config_manager.v2 import ConfigManagerV2, ManagementServerSettings class ReloadApp(ConfigManagerV2): + DEFAULT_UPDATE_INTERVAL = 0.05 + DEFAULT_WORK_INTERVAL = 0.2 + def execute(self) -> None: return @@ -11,9 +14,9 @@ class ReloadApp(ConfigManagerV2): def test_invalid_config_keeps_last_valid(tmp_path): async def scenario() -> None: cfg = tmp_path / "config.yaml" - cfg.write_text("work_interval: 0.2\nupdate_interval: 0.05\n", encoding="utf-8") + cfg.write_text("log: {}\n", encoding="utf-8") - app = ReloadApp(str(cfg)) + app = ReloadApp(str(cfg), management_settings=ManagementServerSettings(enabled=False)) runner = asyncio.create_task(app.start()) await asyncio.sleep(0.12) diff --git a/tests/v2/test_contract_v2.py b/tests/v2/test_contract_v2.py index e190fd1..5cd98ca 100644 --- a/tests/v2/test_contract_v2.py +++ b/tests/v2/test_contract_v2.py @@ -1,9 +1,12 @@ import asyncio -from config_manager.v2 import ConfigManagerV2 +from config_manager.v2 import ConfigManagerV2, ManagementServerSettings class DemoApp(ConfigManagerV2): + DEFAULT_UPDATE_INTERVAL = 0.05 + DEFAULT_WORK_INTERVAL = 0.05 + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.calls = 0 @@ -15,9 +18,9 @@ class DemoApp(ConfigManagerV2): def test_execute_loop_runs(tmp_path): async def scenario() -> None: cfg = tmp_path / "config.yaml" - cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8") + cfg.write_text("log: {}\n", encoding="utf-8") - app = DemoApp(str(cfg)) + app = DemoApp(str(cfg), management_settings=ManagementServerSettings(enabled=False)) runner = asyncio.create_task(app.start()) await asyncio.sleep(0.18) diff --git a/tests/v2/test_control_channel.py b/tests/v2/test_control_channel.py index 8a0e95b..ad0e086 100644 --- a/tests/v2/test_control_channel.py +++ b/tests/v2/test_control_channel.py @@ -1,6 +1,6 @@ import asyncio -from config_manager.v2 import ConfigManagerV2 +from config_manager.v2 import ConfigManagerV2, ManagementServerSettings from config_manager.v2.control.base import ControlChannel, StartHandler, StatusHandler, StopHandler @@ -23,6 +23,9 @@ class DummyControlChannel(ControlChannel): class ControlledApp(ConfigManagerV2): + DEFAULT_UPDATE_INTERVAL = 0.05 + DEFAULT_WORK_INTERVAL = 0.05 + def execute(self) -> None: return @@ -30,10 +33,14 @@ class ControlledApp(ConfigManagerV2): def test_control_channel_can_stop_manager(tmp_path): async def scenario() -> None: cfg = tmp_path / "config.yaml" - cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8") + cfg.write_text("log: {}\n", encoding="utf-8") channel = DummyControlChannel() - app = ControlledApp(str(cfg), control_channel=channel) + app = ControlledApp( + str(cfg), + control_channel=channel, + management_settings=ManagementServerSettings(enabled=False), + ) runner = asyncio.create_task(app.start()) await asyncio.sleep(0.12) @@ -49,6 +56,6 @@ def test_control_channel_can_stop_manager(tmp_path): assert "stop signal accepted" in stop_text await runner - assert channel.stopped is True + # Менеджер при остановке не вызывает control_channel.stop() (канал остаётся доступным) asyncio.run(scenario()) diff --git a/tests/v2/test_stop_graceful.py b/tests/v2/test_stop_graceful.py index 0b60fe7..8b5cd8d 100644 --- a/tests/v2/test_stop_graceful.py +++ b/tests/v2/test_stop_graceful.py @@ -2,10 +2,13 @@ import asyncio import threading import time -from config_manager.v2 import ConfigManagerV2 +from config_manager.v2 import ConfigManagerV2, ManagementServerSettings class BlockingApp(ConfigManagerV2): + DEFAULT_UPDATE_INTERVAL = 0.05 + DEFAULT_WORK_INTERVAL = 0.05 + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.started_event = threading.Event() @@ -23,9 +26,9 @@ class BlockingApp(ConfigManagerV2): def test_stop_waits_for_active_execute_and_prevents_next_run(tmp_path): async def scenario() -> None: cfg = tmp_path / "config.yaml" - cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8") + cfg.write_text("log: {}\n", encoding="utf-8") - app = BlockingApp(str(cfg)) + app = BlockingApp(str(cfg), management_settings=ManagementServerSettings(enabled=False)) runner = asyncio.create_task(app.start()) started = await asyncio.to_thread(app.started_event.wait, 1.0)