diff --git a/agentrun/utils/log.py b/agentrun/utils/log.py index 9f40999..9abde14 100644 --- a/agentrun/utils/log.py +++ b/agentrun/utils/log.py @@ -6,11 +6,20 @@ import logging import os +import time from dotenv import load_dotenv load_dotenv() +UTC8_OFFSET_SECONDS = 8 * 60 * 60 + + +def _utc8_converter(timestamp: float) -> time.struct_time: + """Return a UTC+8 ``struct_time`` for logging.Formatter.""" + + return time.gmtime(timestamp + UTC8_OFFSET_SECONDS) + class CustomFormatter(logging.Formatter): """自定义日志格式化器 / Custom Log Formatter @@ -48,11 +57,17 @@ def __init__(self) -> None: f" {self.DIM}%(pathname)s:%(lineno)s{self.RESET}" "\n%(message)s" ) - self._formatters[level] = logging.Formatter(fmt) + self._formatters[level] = self._create_formatter(fmt) self._default = logging.Formatter( "\n%(levelname)s [%(name)s] %(asctime)s" " %(pathname)s:%(lineno)s\n%(message)s" ) + self._default.converter = _utc8_converter + + def _create_formatter(self, fmt: str) -> logging.Formatter: + formatter = logging.Formatter(fmt) + formatter.converter = _utc8_converter + return formatter def format(self, record: logging.LogRecord) -> str: return self._formatters.get(record.levelname, self._default).format( diff --git a/tests/unittests/test_log.py b/tests/unittests/test_log.py new file mode 100644 index 0000000..65dc79d --- /dev/null +++ b/tests/unittests/test_log.py @@ -0,0 +1,66 @@ +"""Unit tests for AgentRun SDK logging.""" + +import logging +import time + +import pytest + +from agentrun.utils.log import CustomFormatter, _utc8_converter + + +def make_record(level: int, level_name: str) -> logging.LogRecord: + record = logging.LogRecord( + name="agentrun-logger", + level=level, + pathname=__file__, + lineno=1, + msg="hello", + args=(), + exc_info=None, + ) + record.created = 0 + record.levelname = level_name + return record + + +def test_utc8_converter_is_independent_from_local_timezone(): + assert _utc8_converter(0) == time.gmtime(8 * 60 * 60) + + +def test_custom_formatter_uses_utc8_for_all_inner_formatters(): + formatter = CustomFormatter() + expected = time.gmtime(8 * 60 * 60) + + for inner in formatter._formatters.values(): + assert inner.converter(0) == expected + assert formatter._default.converter(0) == expected + + +@pytest.mark.parametrize( + ("level", "level_name"), + [ + (logging.DEBUG, "DEBUG"), + (logging.INFO, "INFO"), + (logging.WARNING, "WARNING"), + (logging.ERROR, "ERROR"), + (logging.CRITICAL, "CRITICAL"), + ], +) +def test_custom_formatter_formats_known_levels_in_utc8( + level: int, level_name: str +): + formatter = CustomFormatter() + + output = formatter.format(make_record(level, level_name)) + + assert "1970-01-01 08:00:00" in output + assert "1970-01-01 00:00:00" not in output + + +def test_custom_formatter_formats_fallback_level_in_utc8(): + formatter = CustomFormatter() + + output = formatter.format(make_record(5, "TRACE")) + + assert "1970-01-01 08:00:00" in output + assert "1970-01-01 00:00:00" not in output