From a7ec5998c2588868330749c765ead9aefb0fabbc Mon Sep 17 00:00:00 2001 From: "congxiao.wxx" Date: Thu, 18 Jun 2026 14:09:25 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A9=20SDK=20=E6=97=A5=E5=BF=97=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E5=9C=A8=20UTC=20=E5=AE=B9=E5=99=A8=E4=B8=AD=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E6=98=BE=E7=A4=BA=E4=B8=9C=E5=85=AB=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constraint: Aone 83336550 requires quick-created Agent logs to show UTC+8 instead of UTC. Rejected: process TZ or frontend conversion | broader blast radius and does not target SDK-emitted asctime. Confidence: high Scope-risk: narrow Directive: Keep timezone policy local to SDK formatter unless future requirements make it configurable. Tested: uv run pytest tests/unittests/test_log.py -q; TZ=UTC/Asia/Shanghai/America/Los_Angeles uv run pytest tests/unittests/test_log.py -q; uv run mypy --config-file mypy.ini agentrun/utils/log.py tests/unittests/test_log.py; git diff --check; independent code-reviewer APPROVE; architect CLEAR; UltraQA PASS. Change-Id: I7ce521856fe79489fae17f97d35f516ddc82491b Co-developed-by: Codex Not-tested: full repo mypy has unrelated pre-existing examples/conversation_service_langgraph_server.py:175 error; full pytest includes credential-gated e2e and was not used as merge gate. Signed-off-by: congxiao.wxx --- agentrun/utils/log.py | 17 +++++++++- tests/unittests/test_log.py | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/test_log.py 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