129 lines
4 KiB
Python
129 lines
4 KiB
Python
|
|
"""Tests for the obs (structured logging) module."""
|
||
|
|
|
||
|
|
import io
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import os
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import structlog
|
||
|
|
|
||
|
|
from obs import configure_logging, get_logger
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture(autouse=True)
|
||
|
|
def reset_logging():
|
||
|
|
"""Reset structlog + stdlib state between tests so configure_logging
|
||
|
|
is forced to re-run."""
|
||
|
|
import obs
|
||
|
|
|
||
|
|
obs._CONFIGURED = False
|
||
|
|
structlog.reset_defaults()
|
||
|
|
structlog.contextvars.clear_contextvars()
|
||
|
|
root = logging.getLogger()
|
||
|
|
root.handlers = []
|
||
|
|
root.setLevel(logging.WARNING)
|
||
|
|
yield
|
||
|
|
obs._CONFIGURED = False
|
||
|
|
structlog.reset_defaults()
|
||
|
|
structlog.contextvars.clear_contextvars()
|
||
|
|
root.handlers = []
|
||
|
|
|
||
|
|
|
||
|
|
def _capture_stderr(monkeypatch):
|
||
|
|
buf = io.StringIO()
|
||
|
|
monkeypatch.setattr("sys.stderr", buf)
|
||
|
|
return buf
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigureLogging:
|
||
|
|
def test_json_format_emits_json(self, monkeypatch):
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_FORMAT", "json")
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_LEVEL", "INFO")
|
||
|
|
buf = _capture_stderr(monkeypatch)
|
||
|
|
|
||
|
|
configure_logging(force=True)
|
||
|
|
log = get_logger("marchwarden.test")
|
||
|
|
log.info("hello", key="value", count=3)
|
||
|
|
|
||
|
|
line = buf.getvalue().strip()
|
||
|
|
assert line, "expected at least one log line"
|
||
|
|
parsed = json.loads(line)
|
||
|
|
assert parsed["event"] == "hello"
|
||
|
|
assert parsed["key"] == "value"
|
||
|
|
assert parsed["count"] == 3
|
||
|
|
assert parsed["level"] == "info"
|
||
|
|
assert parsed["logger"] == "marchwarden.test"
|
||
|
|
assert "timestamp" in parsed
|
||
|
|
|
||
|
|
def test_console_format_is_human_readable(self, monkeypatch):
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_FORMAT", "console")
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_LEVEL", "INFO")
|
||
|
|
buf = _capture_stderr(monkeypatch)
|
||
|
|
|
||
|
|
configure_logging(force=True)
|
||
|
|
log = get_logger("marchwarden.test")
|
||
|
|
log.info("greeting", who="world")
|
||
|
|
|
||
|
|
text = buf.getvalue()
|
||
|
|
assert "greeting" in text
|
||
|
|
assert "world" in text
|
||
|
|
# Console renderer is not JSON
|
||
|
|
with pytest.raises(json.JSONDecodeError):
|
||
|
|
json.loads(text.strip().splitlines()[-1])
|
||
|
|
|
||
|
|
def test_context_binding_propagates(self, monkeypatch):
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_FORMAT", "json")
|
||
|
|
buf = _capture_stderr(monkeypatch)
|
||
|
|
|
||
|
|
configure_logging(force=True)
|
||
|
|
log = get_logger("marchwarden.test")
|
||
|
|
structlog.contextvars.bind_contextvars(trace_id="abc-123", researcher="web")
|
||
|
|
try:
|
||
|
|
log.info("step")
|
||
|
|
finally:
|
||
|
|
structlog.contextvars.clear_contextvars()
|
||
|
|
|
||
|
|
parsed = json.loads(buf.getvalue().strip())
|
||
|
|
assert parsed["trace_id"] == "abc-123"
|
||
|
|
assert parsed["researcher"] == "web"
|
||
|
|
|
||
|
|
def test_log_level_filters(self, monkeypatch):
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_FORMAT", "json")
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_LEVEL", "WARNING")
|
||
|
|
buf = _capture_stderr(monkeypatch)
|
||
|
|
|
||
|
|
configure_logging(force=True)
|
||
|
|
log = get_logger("marchwarden.test")
|
||
|
|
log.info("ignored")
|
||
|
|
log.warning("kept")
|
||
|
|
|
||
|
|
lines = [l for l in buf.getvalue().strip().splitlines() if l]
|
||
|
|
assert len(lines) == 1
|
||
|
|
assert json.loads(lines[0])["event"] == "kept"
|
||
|
|
|
||
|
|
def test_idempotent(self, monkeypatch):
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_FORMAT", "json")
|
||
|
|
configure_logging(force=True)
|
||
|
|
|
||
|
|
import obs
|
||
|
|
|
||
|
|
obs._CONFIGURED = True
|
||
|
|
# Second call should be a no-op
|
||
|
|
before = logging.getLogger().handlers
|
||
|
|
configure_logging()
|
||
|
|
after = logging.getLogger().handlers
|
||
|
|
assert before is after
|
||
|
|
|
||
|
|
def test_get_logger_auto_configures(self, monkeypatch):
|
||
|
|
monkeypatch.setenv("MARCHWARDEN_LOG_FORMAT", "json")
|
||
|
|
buf = _capture_stderr(monkeypatch)
|
||
|
|
|
||
|
|
# No explicit configure_logging() call
|
||
|
|
log = get_logger("marchwarden.test")
|
||
|
|
log.info("auto")
|
||
|
|
|
||
|
|
parsed = json.loads(buf.getvalue().strip())
|
||
|
|
assert parsed["event"] == "auto"
|