chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
import structlog
|
||||
|
||||
from opensquilla.gateway.boot import _setup_file_logging
|
||||
from opensquilla.gateway.config import GatewayConfig
|
||||
from opensquilla.observability.cli_logging import configure_cli_structlog
|
||||
|
||||
|
||||
def _remove_debug_handlers() -> None:
|
||||
root = logging.getLogger()
|
||||
for handler in list(root.handlers):
|
||||
if getattr(handler, "_opensquilla_debug_file_handler", False):
|
||||
root.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
|
||||
def _remove_console_handlers() -> None:
|
||||
root = logging.getLogger()
|
||||
for handler in list(root.handlers):
|
||||
if getattr(handler, "_opensquilla_console_log_handler", False):
|
||||
root.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
|
||||
def test_setup_file_logging_uses_rotation_without_forcing_root_debug(tmp_path, monkeypatch) -> None:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
root = logging.getLogger()
|
||||
opensquilla_logger = logging.getLogger("opensquilla")
|
||||
original_root_level = root.level
|
||||
original_opensquilla_level = opensquilla_logger.level
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_LEVEL", "INFO")
|
||||
|
||||
try:
|
||||
_setup_file_logging(
|
||||
GatewayConfig(
|
||||
log_level="DEBUG",
|
||||
log_file_max_bytes=4096,
|
||||
log_file_backup_count=2,
|
||||
)
|
||||
)
|
||||
|
||||
handlers = [
|
||||
handler
|
||||
for handler in root.handlers
|
||||
if getattr(handler, "_opensquilla_debug_file_handler", False)
|
||||
]
|
||||
assert len(handlers) == 1
|
||||
handler = handlers[0]
|
||||
assert isinstance(handler, RotatingFileHandler)
|
||||
assert handler.level == logging.INFO
|
||||
assert handler.maxBytes == 4096
|
||||
assert handler.backupCount == 2
|
||||
assert getattr(handler, "baseFilename").endswith("debug.log")
|
||||
assert root.level == original_root_level
|
||||
assert opensquilla_logger.level == logging.INFO
|
||||
finally:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
root.setLevel(original_root_level)
|
||||
opensquilla_logger.setLevel(original_opensquilla_level)
|
||||
|
||||
|
||||
def test_setup_file_logging_can_be_disabled(tmp_path, monkeypatch) -> None:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
opensquilla_logger = logging.getLogger("opensquilla")
|
||||
original_opensquilla_level = opensquilla_logger.level
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(tmp_path))
|
||||
monkeypatch.delenv("OPENSQUILLA_LOG_FILE_ENABLED", raising=False)
|
||||
monkeypatch.delenv("OPENSQUILLA_LOG_LEVEL", raising=False)
|
||||
|
||||
try:
|
||||
_setup_file_logging(GatewayConfig(log_file_enabled=False))
|
||||
assert not (tmp_path / "debug.log").exists()
|
||||
|
||||
_setup_file_logging(GatewayConfig(log_level="INFO"))
|
||||
assert opensquilla_logger.level == logging.INFO
|
||||
assert (tmp_path / "debug.log").exists()
|
||||
_setup_file_logging(GatewayConfig(log_file_enabled=False))
|
||||
|
||||
handlers = [
|
||||
handler
|
||||
for handler in logging.getLogger().handlers
|
||||
if getattr(handler, "_opensquilla_debug_file_handler", False)
|
||||
]
|
||||
assert handlers == []
|
||||
# Disabling file logging must not silence the bridged console output:
|
||||
# the disabled path still pins the "opensquilla" logger to the
|
||||
# configured level (default DEBUG) rather than restoring the pre-setup
|
||||
# level, so INFO/DEBUG keep flowing to the console handler.
|
||||
assert opensquilla_logger.level == logging.DEBUG
|
||||
finally:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
opensquilla_logger.setLevel(original_opensquilla_level)
|
||||
|
||||
|
||||
def test_disabled_file_logging_still_bridges_info_to_console(tmp_path, monkeypatch) -> None:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
old_config = structlog.get_config()
|
||||
opensquilla_logger = logging.getLogger("opensquilla")
|
||||
original_level = opensquilla_logger.level
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(tmp_path))
|
||||
monkeypatch.delenv("OPENSQUILLA_LOG_FILE_ENABLED", raising=False)
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_LEVEL", "INFO")
|
||||
try:
|
||||
structlog.reset_defaults()
|
||||
_setup_file_logging(GatewayConfig(log_file_enabled=False))
|
||||
assert not (tmp_path / "debug.log").exists()
|
||||
|
||||
console_handlers = [
|
||||
handler
|
||||
for handler in logging.getLogger().handlers
|
||||
if getattr(handler, "_opensquilla_console_log_handler", False)
|
||||
]
|
||||
assert len(console_handlers) == 1
|
||||
stream = io.StringIO()
|
||||
console_handlers[0].setStream(stream)
|
||||
|
||||
structlog.get_logger("opensquilla.test_bridge").info("console_bridge_info_event")
|
||||
console_handlers[0].flush()
|
||||
assert "console_bridge_info_event" in stream.getvalue()
|
||||
finally:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
structlog.configure(**old_config)
|
||||
opensquilla_logger.setLevel(original_level)
|
||||
|
||||
|
||||
def test_structlog_events_reach_debug_log_with_traceback(tmp_path, monkeypatch) -> None:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
old_config = structlog.get_config()
|
||||
opensquilla_logger = logging.getLogger("opensquilla")
|
||||
original_level = opensquilla_logger.level
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_LEVEL", "DEBUG")
|
||||
try:
|
||||
structlog.reset_defaults()
|
||||
_setup_file_logging(GatewayConfig())
|
||||
slog = structlog.get_logger("opensquilla.test_bridge")
|
||||
try:
|
||||
raise ValueError("synthetic-bridge-error")
|
||||
except ValueError:
|
||||
slog.error("bridge_event", session_key="agent:test:bridge", exc_info=True)
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.flush()
|
||||
text = (tmp_path / "debug.log").read_text(encoding="utf-8")
|
||||
assert "[ERROR] opensquilla.test_bridge: bridge_event" in text
|
||||
assert "session_key='agent:test:bridge'" in text
|
||||
assert "ValueError: synthetic-bridge-error" in text
|
||||
assert text.count("bridge_event") == 1
|
||||
finally:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
structlog.configure(**old_config)
|
||||
opensquilla_logger.setLevel(original_level)
|
||||
|
||||
|
||||
def test_structlog_bridge_respects_log_level(tmp_path, monkeypatch) -> None:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
old_config = structlog.get_config()
|
||||
opensquilla_logger = logging.getLogger("opensquilla")
|
||||
original_level = opensquilla_logger.level
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_LEVEL", "INFO")
|
||||
try:
|
||||
structlog.reset_defaults()
|
||||
_setup_file_logging(GatewayConfig())
|
||||
structlog.get_logger("opensquilla.test_bridge").debug("bridge_debug_event")
|
||||
structlog.get_logger("opensquilla.test_bridge").info("bridge_info_event")
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.flush()
|
||||
text = (tmp_path / "debug.log").read_text(encoding="utf-8")
|
||||
assert "bridge_info_event" in text
|
||||
assert "bridge_debug_event" not in text
|
||||
finally:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
structlog.configure(**old_config)
|
||||
opensquilla_logger.setLevel(original_level)
|
||||
|
||||
|
||||
def test_bridge_overrides_cli_structlog_default(tmp_path, monkeypatch) -> None:
|
||||
"""`gateway run` enters via the CLI callback, which installs the CLI
|
||||
structlog default before boot; the bridge must treat that default as
|
||||
overridable so structlog events still reach debug.log."""
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
old_config = structlog.get_config()
|
||||
was_configured = structlog.is_configured()
|
||||
opensquilla_logger = logging.getLogger("opensquilla")
|
||||
original_level = opensquilla_logger.level
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_LEVEL", "DEBUG")
|
||||
try:
|
||||
configure_cli_structlog()
|
||||
_setup_file_logging(GatewayConfig())
|
||||
structlog.get_logger("opensquilla.test_bridge").info("bridge_over_cli_default_event")
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.flush()
|
||||
text = (tmp_path / "debug.log").read_text(encoding="utf-8")
|
||||
assert "bridge_over_cli_default_event" in text
|
||||
finally:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
if was_configured:
|
||||
structlog.configure(**old_config)
|
||||
else:
|
||||
structlog.reset_defaults()
|
||||
opensquilla_logger.setLevel(original_level)
|
||||
|
||||
|
||||
def test_bridge_respects_non_cli_explicit_configuration(tmp_path, monkeypatch) -> None:
|
||||
"""An explicit non-CLI-default configuration (e.g. the interactive TUI's)
|
||||
must survive `_setup_file_logging` unchanged."""
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
old_config = structlog.get_config()
|
||||
was_configured = structlog.is_configured()
|
||||
opensquilla_logger = logging.getLogger("opensquilla")
|
||||
original_level = opensquilla_logger.level
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_LEVEL", "DEBUG")
|
||||
try:
|
||||
sentinel_factory = structlog.ReturnLoggerFactory()
|
||||
structlog.configure(logger_factory=sentinel_factory)
|
||||
_setup_file_logging(GatewayConfig())
|
||||
assert structlog.get_config()["logger_factory"] is sentinel_factory
|
||||
finally:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
if was_configured:
|
||||
structlog.configure(**old_config)
|
||||
else:
|
||||
structlog.reset_defaults()
|
||||
opensquilla_logger.setLevel(original_level)
|
||||
|
||||
|
||||
def test_bridge_failure_does_not_block_setup(tmp_path, monkeypatch) -> None:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(tmp_path))
|
||||
import opensquilla.gateway.boot as boot_module
|
||||
|
||||
def _boom() -> None:
|
||||
raise RuntimeError("synthetic bridge failure")
|
||||
|
||||
monkeypatch.setattr(boot_module, "_bridge_structlog_to_stdlib", _boom)
|
||||
try:
|
||||
_setup_file_logging(GatewayConfig())
|
||||
assert (tmp_path / "debug.log").exists()
|
||||
finally:
|
||||
_remove_debug_handlers()
|
||||
_remove_console_handlers()
|
||||
Reference in New Issue
Block a user