Adds a few new logging utility classes. One to save to files with a date, one to support optional fields in formats, last to filter partial log messages. ref: N25B-401
219 lines
6.6 KiB
Python
219 lines
6.6 KiB
Python
import logging
|
|
|
|
import pytest
|
|
|
|
from control_backend.logging.optional_field_formatter import OptionalFieldFormatter
|
|
|
|
|
|
@pytest.fixture
|
|
def logger():
|
|
"""Create a fresh logger for each test."""
|
|
logger = logging.getLogger(f"test_{id(object())}")
|
|
logger.setLevel(logging.DEBUG)
|
|
logger.handlers = []
|
|
return logger
|
|
|
|
|
|
@pytest.fixture
|
|
def log_output(logger):
|
|
"""Capture log output and return a function to get it."""
|
|
|
|
class ListHandler(logging.Handler):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.records = []
|
|
|
|
def emit(self, record):
|
|
self.records.append(self.format(record))
|
|
|
|
handler = ListHandler()
|
|
logger.addHandler(handler)
|
|
|
|
def get_output():
|
|
return handler.records
|
|
|
|
return get_output
|
|
|
|
|
|
def test_optional_field_present(logger, log_output):
|
|
"""Optional field should appear when provided in extra."""
|
|
formatter = OptionalFieldFormatter("%(levelname)s - %(role?)s - %(message)s")
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test message", extra={"role": "user"})
|
|
|
|
assert log_output() == ["INFO - user - test message"]
|
|
|
|
|
|
def test_optional_field_missing_no_default(logger, log_output):
|
|
"""Missing optional field with no default should be None."""
|
|
formatter = OptionalFieldFormatter("%(levelname)s - %(role?)s - %(message)s")
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test message")
|
|
|
|
assert log_output() == ["INFO - None - test message"]
|
|
|
|
|
|
def test_optional_field_missing_with_default(logger, log_output):
|
|
"""Missing optional field should use provided default."""
|
|
formatter = OptionalFieldFormatter(
|
|
"%(levelname)s - %(role?)s - %(message)s", defaults={"role": "assistant"}
|
|
)
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test message")
|
|
|
|
assert log_output() == ["INFO - assistant - test message"]
|
|
|
|
|
|
def test_optional_field_overrides_default(logger, log_output):
|
|
"""Provided extra value should override default."""
|
|
formatter = OptionalFieldFormatter(
|
|
"%(levelname)s - %(role?)s - %(message)s", defaults={"role": "assistant"}
|
|
)
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test message", extra={"role": "user"})
|
|
|
|
assert log_output() == ["INFO - user - test message"]
|
|
|
|
|
|
def test_multiple_optional_fields(logger, log_output):
|
|
"""Multiple optional fields should work independently."""
|
|
formatter = OptionalFieldFormatter(
|
|
"%(levelname)s - %(role?)s - %(request_id?)s - %(message)s", defaults={"role": "assistant"}
|
|
)
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test", extra={"request_id": "123"})
|
|
|
|
assert log_output() == ["INFO - assistant - 123 - test"]
|
|
|
|
|
|
def test_mixed_optional_and_required_fields(logger, log_output):
|
|
"""Standard fields should work alongside optional fields."""
|
|
formatter = OptionalFieldFormatter("%(levelname)s %(name)s %(role?)s %(message)s")
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test", extra={"role": "user"})
|
|
|
|
output = log_output()[0]
|
|
assert "INFO" in output
|
|
assert "user" in output
|
|
assert "test" in output
|
|
|
|
|
|
def test_no_optional_fields(logger, log_output):
|
|
"""Formatter should work normally with no optional fields."""
|
|
formatter = OptionalFieldFormatter("%(levelname)s %(message)s")
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test message")
|
|
|
|
assert log_output() == ["INFO test message"]
|
|
|
|
|
|
def test_integer_format_specifier(logger, log_output):
|
|
"""Optional fields with %d specifier should work."""
|
|
formatter = OptionalFieldFormatter(
|
|
"%(levelname)s %(count?)d %(message)s", defaults={"count": 0}
|
|
)
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test", extra={"count": 42})
|
|
|
|
assert log_output() == ["INFO 42 test"]
|
|
|
|
|
|
def test_float_format_specifier(logger, log_output):
|
|
"""Optional fields with %f specifier should work."""
|
|
formatter = OptionalFieldFormatter(
|
|
"%(levelname)s %(duration?)f %(message)s", defaults={"duration": 0.0}
|
|
)
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test", extra={"duration": 1.5})
|
|
|
|
assert "1.5" in log_output()[0]
|
|
|
|
|
|
def test_empty_string_default(logger, log_output):
|
|
"""Empty string default should work."""
|
|
formatter = OptionalFieldFormatter("%(levelname)s %(role?)s %(message)s", defaults={"role": ""})
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test")
|
|
|
|
assert log_output() == ["INFO test"]
|
|
|
|
|
|
def test_none_format_string():
|
|
"""None format string should not raise."""
|
|
formatter = OptionalFieldFormatter(fmt=None)
|
|
assert formatter.optional_fields == set()
|
|
|
|
|
|
def test_optional_fields_parsed_correctly():
|
|
"""Check that optional fields are correctly identified."""
|
|
formatter = OptionalFieldFormatter("%(asctime)s %(role?)s %(level?)d %(name)s")
|
|
|
|
assert formatter.optional_fields == {("role", "s"), ("level", "d")}
|
|
|
|
|
|
def test_format_string_normalized():
|
|
"""Check that ? is removed from format string."""
|
|
formatter = OptionalFieldFormatter("%(role?)s %(message)s")
|
|
|
|
assert "?" not in formatter._style._fmt
|
|
assert "%(role)s" in formatter._style._fmt
|
|
|
|
|
|
def test_field_with_underscore(logger, log_output):
|
|
"""Field names with underscores should work."""
|
|
formatter = OptionalFieldFormatter("%(levelname)s %(user_id?)s %(message)s")
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test", extra={"user_id": "abc123"})
|
|
|
|
assert log_output() == ["INFO abc123 test"]
|
|
|
|
|
|
def test_field_with_numbers(logger, log_output):
|
|
"""Field names with numbers should work."""
|
|
formatter = OptionalFieldFormatter("%(levelname)s %(field2?)s %(message)s")
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test", extra={"field2": "value"})
|
|
|
|
assert log_output() == ["INFO value test"]
|
|
|
|
|
|
def test_multiple_log_calls(logger, log_output):
|
|
"""Formatter should work correctly across multiple log calls."""
|
|
formatter = OptionalFieldFormatter(
|
|
"%(levelname)s %(role?)s %(message)s", defaults={"role": "other"}
|
|
)
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("first", extra={"role": "assistant"})
|
|
logger.info("second")
|
|
logger.info("third", extra={"role": "user"})
|
|
|
|
assert log_output() == [
|
|
"INFO assistant first",
|
|
"INFO other second",
|
|
"INFO user third",
|
|
]
|
|
|
|
|
|
def test_default_not_mutated(logger, log_output):
|
|
"""Original defaults dict should not be mutated."""
|
|
defaults = {"role": "other"}
|
|
formatter = OptionalFieldFormatter("%(levelname)s %(role?)s %(message)s", defaults=defaults)
|
|
logger.handlers[0].setFormatter(formatter)
|
|
|
|
logger.info("test")
|
|
|
|
assert defaults == {"role": "other"}
|