""" Tests for DECNET application logging system. Covers: - get_logger() factory and _ComponentFilter injection - Rfc5424Formatter component-aware APP-NAME field - Log level gating via DECNET_DEVELOPER - Component tags for all five microservice layers """ from __future__ import annotations import logging import os import re import pytest from decnet.logging import _ComponentFilter, get_logger # RFC 5424 parser: 1 TIMESTAMP HOSTNAME APP-NAME PROCID MSGID SD MSG _RFC5424_RE = re.compile( r"^<(\d+)>1 " # PRI r"\S+ " # TIMESTAMP r"\S+ " # HOSTNAME r"(\S+) " # APP-NAME ← what we care about r"\S+ " # PROCID r"(\S+) " # MSGID r"(.+)$", # SD + MSG ) def _format_record(logger: logging.Logger, level: int, msg: str) -> str: """Emit a log record through the root handler and return the formatted string.""" from decnet.config import Rfc5424Formatter formatter = Rfc5424Formatter() record = logger.makeRecord( logger.name, level, "", 0, msg, (), None ) # Run all filters attached to the logger so decnet_component gets injected for f in logger.filters: f.filter(record) return formatter.format(record) class TestGetLogger: def test_returns_logger(self): log = get_logger("cli") assert isinstance(log, logging.Logger) def test_logger_name(self): log = get_logger("engine") assert log.name == "decnet.engine" def test_filter_attached(self): log = get_logger("api") assert any(isinstance(f, _ComponentFilter) for f in log.filters) def test_idempotent_filter(self): log = get_logger("mutator") get_logger("mutator") # second call component_filters = [f for f in log.filters if isinstance(f, _ComponentFilter)] assert len(component_filters) == 1 @pytest.mark.parametrize("component", ["cli", "engine", "api", "mutator", "collector"]) def test_all_components_registered(self, component): log = get_logger(component) assert any(isinstance(f, _ComponentFilter) for f in log.filters) class TestComponentFilter: def test_injects_attribute(self): f = _ComponentFilter("engine") record = logging.LogRecord("test", logging.INFO, "", 0, "msg", (), None) assert f.filter(record) is True assert record.decnet_component == "engine" # type: ignore[attr-defined] def test_always_passes(self): f = _ComponentFilter("collector") record = logging.LogRecord("test", logging.DEBUG, "", 0, "msg", (), None) assert f.filter(record) is True class TestRfc5424FormatterComponentAware: @pytest.mark.parametrize("component", ["cli", "engine", "api", "mutator", "collector"]) def test_app_name_is_component(self, component): log = get_logger(component) line = _format_record(log, logging.INFO, "test message") m = _RFC5424_RE.match(line) assert m is not None, f"Not RFC 5424: {line!r}" assert m.group(2) == component, f"Expected APP-NAME={component!r}, got {m.group(2)!r}" def test_fallback_app_name_without_component(self): """Untagged loggers (no _ComponentFilter) fall back to 'decnet'.""" from decnet.config import Rfc5424Formatter formatter = Rfc5424Formatter() record = logging.LogRecord("some.module", logging.INFO, "", 0, "hello", (), None) line = formatter.format(record) m = _RFC5424_RE.match(line) assert m is not None assert m.group(2) == "decnet" def test_msgid_is_logger_name(self): log = get_logger("engine") line = _format_record(log, logging.INFO, "deploying") m = _RFC5424_RE.match(line) assert m is not None assert m.group(3) == "decnet.engine" class TestLogLevelGating: def test_configure_logging_normal_mode_sets_info(self): """_configure_logging(dev=False) must set root to INFO.""" from decnet.config import _configure_logging, Rfc5424Formatter root = logging.getLogger() original_level = root.level original_handlers = root.handlers[:] # Remove any existing RFC5424 handlers so idempotency check doesn't skip root.handlers = [ h for h in root.handlers if not (isinstance(h, logging.StreamHandler) and isinstance(h.formatter, Rfc5424Formatter)) ] try: _configure_logging(dev=False) assert root.level == logging.INFO finally: root.setLevel(original_level) root.handlers = original_handlers def test_configure_logging_dev_mode_sets_debug(self): """_configure_logging(dev=True) must set root to DEBUG.""" from decnet.config import _configure_logging, Rfc5424Formatter root = logging.getLogger() original_level = root.level original_handlers = root.handlers[:] root.handlers = [ h for h in root.handlers if not (isinstance(h, logging.StreamHandler) and isinstance(h.formatter, Rfc5424Formatter)) ] try: _configure_logging(dev=True) assert root.level == logging.DEBUG finally: root.setLevel(original_level) root.handlers = original_handlers def test_debug_enabled_in_developer_mode(self, monkeypatch): """Programmatically setting DEBUG on root allows debug records through.""" root = logging.getLogger() original_level = root.level root.setLevel(logging.DEBUG) try: assert root.isEnabledFor(logging.DEBUG) finally: root.setLevel(original_level)