13074f56cb
Unit Tests / test (push) Successful in 12m34s
Root causes fixed:
- Dead LOG_LEVEL globals() lookup pinned root logger at INFO regardless of
PIC_LOG_LEVEL env or config; replaced with _resolve_root_log_level() +
apply_root_log_level() which sets both root logger and all attached handlers
at startup and on runtime re-apply.
- set_service_level() only set the named 'pic.<service>' logger; bare module
loggers (e.g. 'caddy_manager') were never reached, so per-service log files
stayed 0 bytes. Fixed via _SERVICE_MODULE_LOGGERS map covering all managers.
- Log viewer GET /api/logs had no level filter; added ?level= query param.
- Per-service log levels lived in an out-of-band config/api/log_levels.json
side-file with no validation; migrated into ConfigManager under a new
'logging' section ({python:{root,services}, containers:{caddy,coredns,
wireguard,mailserver,api}}) with get/set helpers, invalid-level rejection,
and one-time migration from the old file on first load.
New capabilities:
- Container log levels: Caddy (injects global log { level X } + hot reload),
CoreDNS (DEBUG enables log plugin, else errors-only), WireGuard/mailserver
via pending_restart path.
- PUT /api/logs/verbosity accepts {python, containers} dict; returns per-entry
applied:hot|pending_restart status.
- Webui Logs page gains two-section Verbosity tab (Python services + Container
services) with needs-restart badges.
- managers.py wires per-service loggers before manager instantiation and
re-applies persisted levels from ConfigManager; legacy log_levels.json read
removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for the `logging` section of cell_config (ConfigManager):
|
|
- default schema present after first load
|
|
- round-trip persistence of python + container levels
|
|
- migration from the legacy config/api/log_levels.json side-file
|
|
- invalid-level rejection
|
|
- inclusion in the backed-up cell_config
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import tempfile
|
|
import shutil
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
api_dir = Path(__file__).parent.parent / 'api'
|
|
sys.path.insert(0, str(api_dir))
|
|
|
|
from config_manager import ConfigManager
|
|
|
|
|
|
def _make_cm(tmp):
|
|
config_file = os.path.join(tmp, 'cell_config.json')
|
|
data_dir = os.path.join(tmp, 'data')
|
|
os.makedirs(data_dir, exist_ok=True)
|
|
return ConfigManager(config_file, data_dir)
|
|
|
|
|
|
class TestLoggingSchema(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
|
|
def test_default_logging_config_present(self):
|
|
cm = _make_cm(self.tmp)
|
|
cfg = cm.get_logging_config()
|
|
self.assertEqual(cfg['python']['root'], 'INFO')
|
|
self.assertEqual(cfg['python']['services']['network'], 'INFO')
|
|
self.assertEqual(cfg['containers']['caddy'], 'INFO')
|
|
self.assertEqual(cfg['containers']['coredns'], 'INFO')
|
|
|
|
def test_set_and_get_python_level_round_trip(self):
|
|
cm = _make_cm(self.tmp)
|
|
cm.set_python_log_level('network', 'DEBUG')
|
|
cm.set_python_log_level('root', 'WARNING')
|
|
# Re-load from disk to prove persistence.
|
|
cm2 = _make_cm(self.tmp)
|
|
cfg = cm2.get_logging_config()
|
|
self.assertEqual(cfg['python']['services']['network'], 'DEBUG')
|
|
self.assertEqual(cfg['python']['root'], 'WARNING')
|
|
|
|
def test_set_and_get_container_level_round_trip(self):
|
|
cm = _make_cm(self.tmp)
|
|
cm.set_container_log_level('coredns', 'DEBUG')
|
|
cm2 = _make_cm(self.tmp)
|
|
self.assertEqual(cm2.get_logging_config()['containers']['coredns'], 'DEBUG')
|
|
|
|
def test_invalid_python_level_rejected(self):
|
|
cm = _make_cm(self.tmp)
|
|
with self.assertRaises(ValueError):
|
|
cm.set_python_log_level('network', 'LOUD')
|
|
|
|
def test_invalid_container_level_rejected(self):
|
|
cm = _make_cm(self.tmp)
|
|
with self.assertRaises(ValueError):
|
|
cm.set_container_log_level('caddy', 'chatty')
|
|
|
|
def test_migration_from_legacy_log_levels_json(self):
|
|
# Legacy side-file lived at config/api/log_levels.json (next to cell_config).
|
|
api_cfg_dir = os.path.join(self.tmp, 'api')
|
|
os.makedirs(api_cfg_dir, exist_ok=True)
|
|
with open(os.path.join(api_cfg_dir, 'log_levels.json'), 'w') as f:
|
|
json.dump({'network': 'DEBUG', 'email': 'WARNING', 'bogus': 'INFO'}, f)
|
|
cm = _make_cm(self.tmp)
|
|
cfg = cm.get_logging_config()
|
|
self.assertEqual(cfg['python']['services']['network'], 'DEBUG')
|
|
self.assertEqual(cfg['python']['services']['email'], 'WARNING')
|
|
# Unknown service names from the legacy file are ignored.
|
|
self.assertNotIn('bogus', cfg['python']['services'])
|
|
|
|
def test_logging_section_is_part_of_persisted_config(self):
|
|
"""The logging section lives in cell_config (already in the backup set)."""
|
|
cm = _make_cm(self.tmp)
|
|
cm.set_python_log_level('vault', 'ERROR')
|
|
with open(cm.config_file) as f:
|
|
on_disk = json.load(f)
|
|
self.assertIn('logging', on_disk)
|
|
self.assertEqual(on_disk['logging']['python']['services']['vault'], 'ERROR')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|