From 862914c9b01cc0124c672f58ced6a453a787e8bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9A=86=E5=AE=87?= Date: Sun, 19 Jul 2026 22:36:11 +0800 Subject: [PATCH 1/5] docs: design configurable exception tracebacks --- ...7-19-exception-traceback-logging-design.md | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-19-exception-traceback-logging-design.md diff --git a/docs/superpowers/specs/2026-07-19-exception-traceback-logging-design.md b/docs/superpowers/specs/2026-07-19-exception-traceback-logging-design.md new file mode 100644 index 0000000000..f646a1a637 --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-exception-traceback-logging-design.md @@ -0,0 +1,189 @@ +# 可配置的异常堆栈日志设计 + +## 状态 + +- 关联 Issue:[alibaba/ROCK#1260](https://github.com/alibaba/ROCK/issues/1260) +- 基线:`origin/master` +- 日期:2026-07-19 + +## 背景 + +`rock.common.exception.handle_exceptions` 捕获普通异常时已经使用 `exc_info=True`: + +```python +logger.error(f"Error in {func.__name__}: {str(e)}", exc_info=True) +``` + +但 `rock.logger.StandardFormatter.format()` 完全手工拼接日志,只读取 `record.getMessage()`,没有处理 `record.exc_info`。因此异常对象仍在 `LogRecord` 中,最终输出却没有异常类型和 traceback。对于 `str(e)` 为空的异常,日志只剩下类似: + +```text +Error in http_proxy: +``` + +这会丢失定位问题所需的异常类型、调用路径和异常链。 + +## 目标 + +1. 自定义 formatter 在存在有效 `exc_info` 时输出完整 Python traceback。 +2. 在首行附加异常的全限定类型;即使日志平台按行切分,仍能看到异常种类。 +3. 默认启用新能力,同时提供环境变量和 YAML 开关,可恢复当前的单行输出模式。 +4. 保持现有日志头、时区、颜色、文件/stdout 选择和普通日志格式不变。 +5. 明确 admin、proxy 与 rocklet 的不同配置加载方式。 + +## 非目标 + +- 不判断这次线上错误的具体异常类型或根因。 +- 不改变异常捕获、HTTP 状态码、`RockResponse` 或 API 返回内容。 +- 不在 traceback 中增加局部变量、请求体或其他可能敏感的数据。 +- 不为 rocklet 引入 `RockConfig`/YAML 加载链路。 +- 不支持通过 Nacos 动态更新该开关。 + +## 方案选择 + +采用“公共 formatter + 进程级运行时配置”的方案。 + +不直接修改每个 `logger.error(...)` 调用点,因为这会遗漏其他使用 `exc_info=True` 的日志,也会让输出行为散落到业务代码中。也不只依赖环境变量,因为 admin/proxy 已有统一 YAML 配置体系,运维需要通过现有配置文件干预。 + +formatter 在每次格式化时查询当前有效开关,而不是在 logger/handler 创建时固化值。这一点很重要:模块级 logger 通常早于 FastAPI lifespan 创建,而 YAML 是在 lifespan 中才加载的。 + +## 配置模型与优先级 + +新增 YAML 配置: + +```yaml +logging: + exception_traceback_enabled: true +``` + +新增环境变量: + +```text +ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE=true|false +``` + +有效值按下列优先级解析: + +| 优先级 | 来源 | 适用范围 | +|---|---|---| +| 1 | 显式设置的 `ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE` | 所有进程,包括 admin、proxy、rocklet 和 CLI | +| 2 | YAML `logging.exception_traceback_enabled` | 加载 `RockConfig` 的 admin 与 proxy | +| 3 | 默认值 `true` | 未提供上述配置的所有进程 | + +环境变量是否“显式设置”通过 `env_vars.is_set()` 判断,避免环境变量的默认值无条件覆盖 YAML。环境变量和 YAML 任一处设置为 `false`(且未被更高优先级覆盖)时,恢复当前行为:只输出原始日志消息,不附加异常类型和 traceback。 + +`RockConfig` 新增 `LoggingConfig`,其中 `exception_traceback_enabled` 默认为 `true`。旧 YAML 不包含 `logging` 时保持向后兼容,不会解析失败。 + +## 日志输出契约 + +### 开关启用 + +当 `record.exc_info` 有效时,首行在原消息后附加全限定异常类型,随后换行输出 `logging.Formatter.formatException()` 生成的标准 traceback: + +```text +2026-07-18T23:42:40.329+08:00 ERROR:exception.py:61 [rock.common.exception] [] [0b5128ae17843892402452213e0cb1] -- Error in http_proxy: [exception_type=httpx.PoolTimeout] +Traceback (most recent call last): + ... +httpx.PoolTimeout +``` + +若异常消息非空,原消息完整保留,异常类型追加在其后。若原消息以空白结尾,只在启用模式下清理末尾空白,避免类型标签前出现多余空格。 + +异常类型取实际异常类的 `module + qualname`,不使用 `repr(exception)`,以免额外输出异常对象中可能包含的敏感信息。标准 `formatException()` 会保留 Python 异常链,但不会主动输出局部变量。 + +### 开关关闭 + +输出与当前 `origin/master` 完全一致: + +```text +2026-07-18T23:42:40.329+08:00 ERROR:exception.py:61 [rock.common.exception] [] [0b5128ae17843892402452213e0cb1] -- Error in http_proxy: +``` + +不附加异常类型,也不输出 traceback。没有有效 `exc_info` 的普通日志无论开关为何都保持不变。 + +## 组件改动 + +### `rock/logger.py` + +- 增加进程级 YAML 配置值及设置函数,参数只接收布尔值,避免 logger 反向依赖 `RockConfig`。 +- 增加有效开关解析函数:显式环境变量优先,其次进程级 YAML 值,最后默认 `true`。 +- `StandardFormatter.format()` 在完成现有日志头和消息拼接后,按需追加异常类型和 `formatException(record.exc_info)` 的结果。 +- 仅当 `record.exc_info` 存在且异常类型不为 `None` 时视为有效,避免在异常上下文之外传入 `exc_info=True` 产生无意义输出。 +- stdout 的彩色 formatter 与文件的非彩色 formatter 共用同一逻辑,每个 handler 各输出一次,不修改或复用 `record.exc_text`,避免多 handler 重复堆栈。 + +### `rock/env_vars.py` + +- 声明并解析 `ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE`。 +- 继续使用现有布尔环境变量风格,对 `true`/`false` 做大小写无关解析。 +- 使用现有 `is_set()` 区分“未设置”和“显式设置”。 + +### `rock/config.py` + +- 新增 `LoggingConfig` dataclass。 +- 在 `RockConfig` 中增加 `logging` 字段。 +- 在 `RockConfig.from_env()` 中解析 YAML 的 `logging` 节。 + +### `rock/admin/main.py` + +- `RockConfig.from_env()` 返回后,立即把 `rock_config.logging.exception_traceback_enabled` 注入公共 logger。 +- admin 角色与 proxy 角色由同一个 `create_app()`/`lifespan()` 启动链路创建,所以两者使用同一处改动。 +- 在 YAML 加载完成之前的极早期启动日志使用环境变量或默认值;加载完成后的业务日志使用完整优先级。 + +### `rock/rocklet/server.py` + +不修改。rocklet 当前不调用 `RockConfig.from_env()`,因此不读取 admin 的 YAML 配置。它通过公共 formatter 使用环境变量;环境变量未设置时使用默认值 `true`。这样不会为轻量运行时引入额外配置依赖。 + +### 配置示例 + +在仓库的 admin 配置示例中展示 `logging.exception_traceback_enabled`,使 YAML 能力可发现。字段缺失仍使用默认值,不要求现有部署立刻修改 YAML。 + +## 初始化与并发 + +模块导入阶段创建的 formatter 不缓存开关,格式化每条异常日志时读取有效值。因此 lifespan 中的配置注入会作用于已经创建的 logger/handler。 + +YAML 值只在服务启动阶段设置一次。Python 模块级布尔引用的读写对该场景足够,不引入锁;多 worker 进程分别执行各自的 lifespan 并持有各自的配置值。 + +## 兼容性与回滚 + +- 默认值从“formatter 丢弃 traceback”改变为“输出 traceback”,这是有意的可观测性增强。 +- 需要立即回滚日志形态时,将环境变量设为 `false`;admin/proxy 也可在 YAML 中设为 `false`。 +- 环境变量优先级最高,可在不修改/重新生成 YAML 的情况下统一覆盖。 +- 现有日志头字段顺序不变,普通日志不变;只影响带有效 `exc_info` 的日志。 + +## 测试策略 + +### Formatter 单元测试 + +1. 默认启用时,普通异常同时输出首行全限定类型与完整 traceback。 +2. 使用 `httpx.PoolTimeout("")` 验证空异常消息仍在首行输出 `httpx.PoolTimeout`。 +3. 开关关闭时,输出与当前格式完全一致,既无类型标签也无 traceback。 +4. 普通 INFO/WARNING/ERROR(无 `exc_info`)在开关启用和关闭时均保持原格式。 +5. stdout 彩色格式与文件非彩色格式均输出一次 traceback,不重复。 +6. 异常链通过标准 formatter 正确输出。 + +### 配置单元测试 + +1. YAML `true`/`false` 均能解析到 `RockConfig.logging`。 +2. 环境变量未设置时使用 YAML 值。 +3. 环境变量显式 `true` 覆盖 YAML `false`。 +4. 环境变量显式 `false` 覆盖 YAML `true`。 +5. YAML 与环境变量均未设置时默认为 `true`。 + +### 回归验证 + +至少执行: + +```bash +uv run pytest tests/unit/test_logger.py tests/unit/test_config.py -v +uv run ruff check rock/logger.py rock/env_vars.py rock/config.py rock/admin/main.py tests/unit/test_logger.py tests/unit/test_config.py +uv run ruff format --check rock/logger.py rock/env_vars.py rock/config.py rock/admin/main.py tests/unit/test_logger.py tests/unit/test_config.py +``` + +现有时间戳和 billing 日志格式测试必须继续通过。 + +## 实施顺序 + +1. 先补 formatter 与配置优先级的失败测试。 +2. 实现环境变量、YAML dataclass 与解析。 +3. 实现 formatter 的异常类型和 traceback 输出。 +4. 在 admin/proxy lifespan 注入 YAML 配置。 +5. 更新配置示例并执行回归测试。 From 7789e7212dc0a0e484f24458f7b6690d64df7bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9A=86=E5=AE=87?= Date: Sun, 19 Jul 2026 22:45:35 +0800 Subject: [PATCH 2/5] docs: plan configurable exception tracebacks --- ...07-19-configurable-exception-tracebacks.md | 618 ++++++++++++++++++ 1 file changed, 618 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-19-configurable-exception-tracebacks.md diff --git a/docs/superpowers/plans/2026-07-19-configurable-exception-tracebacks.md b/docs/superpowers/plans/2026-07-19-configurable-exception-tracebacks.md new file mode 100644 index 0000000000..2a513e072b --- /dev/null +++ b/docs/superpowers/plans/2026-07-19-configurable-exception-tracebacks.md @@ -0,0 +1,618 @@ +# Configurable Exception Tracebacks Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让 ROCK 的自定义日志 formatter 默认保留异常类型与完整 traceback,并允许通过环境变量或 admin/proxy YAML 配置恢复旧输出模式。 + +**Architecture:** 在 `rock.logger` 中维护进程级 YAML 配置值,并在格式化每条日志时按“显式环境变量 > YAML 注入值 > 默认开启”解析有效开关。`StandardFormatter` 只对带有效 `exc_info` 的记录追加全限定异常类型和标准 traceback;admin 与 proxy 在公共 lifespan 中注入 YAML 值,rocklet 继续只使用环境变量或默认值。 + +**Tech Stack:** Python 3.10–3.12、标准库 `logging`、dataclasses、PyYAML、FastAPI lifespan、pytest、httpx、ruff。 + +**References:** + +- Issue: [alibaba/ROCK#1260](https://github.com/alibaba/ROCK/issues/1260) +- Design: `docs/superpowers/specs/2026-07-19-exception-traceback-logging-design.md` + +## Global Constraints + +- 新能力默认开启;未设置环境变量且未注入 YAML 时必须输出异常类型和 traceback。 +- 配置优先级固定为:显式 `ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE` > YAML `logging.exception_traceback_enabled` > 默认 `true`。 +- 开关关闭时必须保持 `origin/master` 的日志文本,不附加异常类型,不输出 traceback,也不清理原消息末尾空白。 +- 普通日志、日志头字段顺序、时区、颜色、文件/stdout 路由不得改变。 +- 只使用标准 `logging.Formatter.formatException()`;不得记录局部变量或额外序列化异常对象。 +- 不修改 `rock/common/exception.py`、API 响应、HTTP 状态码和异常处理流程。 +- 不修改 `rock/rocklet/server.py`,也不为 rocklet 引入 `RockConfig`。 +- 不支持通过 Nacos 动态更新该开关。 +- commit message 使用英文 Conventional Commits,且不得包含 `Co-Authored-By`。 + +--- + +## File Structure + +- `rock/env_vars.py`:声明并解析异常堆栈环境变量。 +- `rock/config.py`:定义 `LoggingConfig`,并从 YAML 构造 `RockConfig.logging`。 +- `rock/logger.py`:持有进程级配置、解析最终开关并格式化异常类型与 traceback。 +- `rock/admin/main.py`:在 admin/proxy 共用 lifespan 中注入 YAML 日志配置。 +- `rock-conf/rock-local.yml`:展示本地 admin 的日志开关。 +- `rock-conf/rock-dev.yml`:展示开发环境 admin/proxy 的日志开关。 +- `rock-conf/rock-test.yml`:展示测试环境 admin 的日志开关。 +- `tests/unit/test_config.py`:覆盖 YAML 默认值和 true/false 解析。 +- `tests/unit/test_logger.py`:覆盖配置优先级、异常格式、回滚模式、颜色和异常链。 +- `tests/unit/admin/test_logging_config.py`:覆盖 admin/proxy 公共启动链路向 logger 传递 YAML 值。 + +--- + +### Task 1: Add logging configuration and precedence resolution + +**Files:** + +- Modify: `rock/env_vars.py:8-13,78-83` +- Modify: `rock/config.py:42-56,519-612` +- Modify: `rock/logger.py:8-13` +- Modify: `tests/unit/test_config.py:1-18` +- Modify: `tests/unit/test_logger.py:1-12` + +**Interfaces:** + +- Produces: `LoggingConfig(exception_traceback_enabled: bool = True)`. +- Produces: `RockConfig.logging: LoggingConfig`. +- Produces: `configure_logging(*, exception_traceback_enabled: bool) -> None`. +- Produces: `is_exception_traceback_enabled() -> bool`. +- Consumes: existing `env_vars.is_set(name: str)` to distinguish an unset environment variable from its default value. + +- [ ] **Step 1: Write failing YAML configuration tests** + +In `tests/unit/test_config.py`, extend the existing import and add these tests after `test_rock_config`: + +```python +from rock.config import ImageRegistryMirror, LoggingConfig, RockConfig, RuntimeConfig, _resolve_k8s_template_includes + + +def test_logging_config_defaults_to_exception_tracebacks_enabled(): + config = LoggingConfig() + + assert config.exception_traceback_enabled is True + assert RockConfig().logging.exception_traceback_enabled is True + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_logging_config_from_yaml(tmp_path, monkeypatch, enabled): + yaml_path = tmp_path / "rock-test.yml" + yaml_path.write_text(yaml.safe_dump({"logging": {"exception_traceback_enabled": enabled}})) + monkeypatch.setenv("ROCK_PYTHON_ENV_PATH", "/usr") + monkeypatch.setenv("ROCK_ENVHUB_DB_URL", "sqlite:////tmp/test.db") + + rock_config = RockConfig.from_env(str(yaml_path)) + + assert rock_config.logging.exception_traceback_enabled is enabled +``` + +- [ ] **Step 2: Write failing precedence tests** + +In `tests/unit/test_logger.py`, add `pytest` and import the new logger interfaces: + +```python +import pytest + +from rock.logger import configure_logging, init_logger, is_exception_traceback_enabled +``` + +Add an autouse fixture and precedence tests before the existing logger tests: + +```python +@pytest.fixture(autouse=True) +def reset_exception_traceback_config(monkeypatch): + monkeypatch.delenv("ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE", raising=False) + configure_logging(exception_traceback_enabled=True) + yield + configure_logging(exception_traceback_enabled=True) + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_runtime_logging_config_used_when_environment_is_unset(enabled): + configure_logging(exception_traceback_enabled=enabled) + + assert is_exception_traceback_enabled() is enabled + + +@pytest.mark.parametrize( + ("configured", "environment_value", "expected"), + [ + (False, "true", True), + (True, "false", False), + (False, "TRUE", True), + (True, "FALSE", False), + ], +) +def test_environment_overrides_runtime_logging_config(monkeypatch, configured, environment_value, expected): + configure_logging(exception_traceback_enabled=configured) + monkeypatch.setenv("ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE", environment_value) + + assert is_exception_traceback_enabled() is expected +``` + +- [ ] **Step 3: Run the new tests and verify RED** + +Run: + +```bash +uv run pytest \ + tests/unit/test_config.py::test_logging_config_defaults_to_exception_tracebacks_enabled \ + tests/unit/test_config.py::test_logging_config_from_yaml \ + tests/unit/test_logger.py::test_runtime_logging_config_used_when_environment_is_unset \ + tests/unit/test_logger.py::test_environment_overrides_runtime_logging_config -v +``` + +Expected: collection fails because `LoggingConfig`, `configure_logging`, and `is_exception_traceback_enabled` do not exist yet. + +- [ ] **Step 4: Add the environment variable** + +In the `TYPE_CHECKING` block of `rock/env_vars.py`, place this declaration beside the existing logging variables: + +```python +ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE: bool = True +``` + +In `environment_variables`, place this entry after `ROCK_LOGGING_APPEND`: + +```python +"ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE": lambda: os.getenv( + "ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE", "true" +).lower() +== "true", +``` + +Run `uv run ruff format rock/env_vars.py` immediately after editing so the multiline lambda is normalized by the repository formatter. + +- [ ] **Step 5: Add `LoggingConfig` and YAML parsing** + +In `rock/config.py`, add this dataclass after `RedisConfig`: + +```python +@dataclass +class LoggingConfig: + exception_traceback_enabled: bool = True +``` + +Add the field beside the other top-level `RockConfig` sections: + +```python +logging: LoggingConfig = field(default_factory=LoggingConfig) +``` + +In `RockConfig.from_env()`, add this conversion after the `redis` section: + +```python +if "logging" in config: + kwargs["logging"] = LoggingConfig(**config["logging"]) +``` + +- [ ] **Step 6: Add the process-level resolver** + +In `rock/logger.py`, immediately after imports, add: + +```python +_exception_traceback_enabled = True + + +def configure_logging(*, exception_traceback_enabled: bool) -> None: + global _exception_traceback_enabled + _exception_traceback_enabled = exception_traceback_enabled + + +def is_exception_traceback_enabled() -> bool: + if env_vars.is_set("ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE"): + return env_vars.ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE + return _exception_traceback_enabled +``` + +The formatter must call `is_exception_traceback_enabled()` for each record; no handler or formatter may cache the result. + +- [ ] **Step 7: Run the focused tests and verify GREEN** + +Run the same command from Step 3. + +Expected: all parameterized cases pass; YAML defaults to `true`, YAML accepts both booleans, and a case-insensitive explicit environment value overrides the configured value in both directions. + +- [ ] **Step 8: Run formatting and lint for Task 1** + +Run: + +```bash +uv run ruff format rock/env_vars.py rock/config.py rock/logger.py tests/unit/test_config.py tests/unit/test_logger.py +uv run ruff check rock/env_vars.py rock/config.py rock/logger.py tests/unit/test_config.py tests/unit/test_logger.py +``` + +Expected: both commands exit 0. + +- [ ] **Step 9: Commit Task 1** + +```bash +git add rock/env_vars.py rock/config.py rock/logger.py tests/unit/test_config.py tests/unit/test_logger.py +git commit -m "feat: add configurable traceback logging" +``` + +--- + +### Task 2: Preserve exception type and traceback in `StandardFormatter` + +**Files:** + +- Modify: `rock/logger.py:13-52` +- Modify: `tests/unit/test_logger.py` + +**Interfaces:** + +- Consumes: `is_exception_traceback_enabled() -> bool` from Task 1. +- Consumes: `record.exc_info`, requiring a non-`None` exception class at index 0. +- Produces: enabled first-line suffix `[exception_type=.]` followed by `formatException(record.exc_info)`. +- Produces: disabled output identical to the baseline `StandardFormatter` result. + +- [ ] **Step 1: Add a deterministic exception-record helper** + +Add `sys` and `httpx` imports to `tests/unit/test_logger.py`, then add: + +```python +import sys + +import httpx + + +def _make_exception_record(exc: Exception) -> logging.LogRecord: + try: + raise exc + except Exception: + return logging.LogRecord( + name="rock.common.exception", + level=logging.ERROR, + pathname="/tmp/exception.py", + lineno=61, + msg="Error in http_proxy: %s", + args=(str(exc),), + exc_info=sys.exc_info(), + ) +``` + +Extend the logger import to include `TimezoneFormatter`: + +```python +from rock.logger import TimezoneFormatter, configure_logging, init_logger, is_exception_traceback_enabled +``` + +- [ ] **Step 2: Write failing enabled and rollback-mode tests** + +Add: + +```python +@pytest.mark.parametrize("log_color_enable", [True, False]) +def test_formatter_includes_empty_exception_type_and_traceback_once(log_color_enable): + formatter = TimezoneFormatter(log_color_enable=log_color_enable, tz_string="Asia/Shanghai") + record = _make_exception_record(httpx.PoolTimeout("")) + + output = formatter.format(record) + + assert output.count("[exception_type=httpx.PoolTimeout]") == 1 + assert output.count("Traceback (most recent call last):") == 1 + assert "Error in http_proxy: [exception_type=httpx.PoolTimeout]\nTraceback" in output + assert output.rstrip().endswith("httpx.PoolTimeout") + + +def test_formatter_disabled_preserves_current_single_line_output(): + configure_logging(exception_traceback_enabled=False) + formatter = TimezoneFormatter(log_color_enable=False, tz_string="Asia/Shanghai") + record = _make_exception_record(httpx.PoolTimeout("")) + + output = formatter.format(record) + + assert output.endswith("-- Error in http_proxy: ") + assert "exception_type=" not in output + assert "Traceback (most recent call last):" not in output +``` + +- [ ] **Step 3: Write failing ordinary-log and exception-chain tests** + +Add: + +```python +def test_formatter_does_not_change_records_without_exc_info(): + formatter = TimezoneFormatter(log_color_enable=False, tz_string="Asia/Shanghai") + record = logging.LogRecord( + name="rock.test", + level=logging.ERROR, + pathname="/tmp/test.py", + lineno=10, + msg="ordinary error", + args=(), + exc_info=None, + ) + + configure_logging(exception_traceback_enabled=True) + enabled_output = formatter.format(record) + configure_logging(exception_traceback_enabled=False) + disabled_output = formatter.format(record) + + assert enabled_output == disabled_output + assert enabled_output.endswith("-- ordinary error") + + +def test_formatter_preserves_standard_exception_chain(): + formatter = TimezoneFormatter(log_color_enable=False, tz_string="Asia/Shanghai") + try: + try: + raise ValueError("inner") + except ValueError as exc: + raise RuntimeError("outer") from exc + except RuntimeError: + record = logging.LogRecord( + name="rock.common.exception", + level=logging.ERROR, + pathname="/tmp/exception.py", + lineno=61, + msg="chained failure", + args=(), + exc_info=sys.exc_info(), + ) + + output = formatter.format(record) + + assert "[exception_type=builtins.RuntimeError]" in output + assert "ValueError: inner" in output + assert "The above exception was the direct cause" in output + assert output.rstrip().endswith("RuntimeError: outer") +``` + +- [ ] **Step 4: Run formatter tests and verify RED** + +Run: + +```bash +uv run pytest \ + tests/unit/test_logger.py::test_formatter_includes_empty_exception_type_and_traceback_once \ + tests/unit/test_logger.py::test_formatter_disabled_preserves_current_single_line_output \ + tests/unit/test_logger.py::test_formatter_does_not_change_records_without_exc_info \ + tests/unit/test_logger.py::test_formatter_preserves_standard_exception_chain -v +``` + +Expected: enabled-mode and exception-chain assertions fail because the current formatter discards `record.exc_info`; rollback-mode and ordinary-log tests may already pass. + +- [ ] **Step 5: Implement exception formatting without mutating the record** + +In `StandardFormatter.format()`, replace the two final return branches with this exact structure: + +```python +message = record.getMessage() +if is_exception_traceback_enabled() and record.exc_info and record.exc_info[0] is not None: + exception_class = record.exc_info[0] + exception_type = f"{exception_class.__module__}.{exception_class.__qualname__}" + message = f"{message.rstrip()} [exception_type={exception_type}]\n{self.formatException(record.exc_info)}" + +# Color the header part and keep message in default color +if self.log_color_enable: + return f"{log_color}{header_str}{RESET} {message}" +return f"{header_str} {message}" +``` + +Do not assign to `record.exc_text`, `record.msg`, `record.args`, or `record.exc_info`. This lets stdout and file handlers independently render one traceback without sharing cached text or changing downstream handlers. + +- [ ] **Step 6: Run formatter tests and verify GREEN** + +Run the same command from Step 4. + +Expected: all five parameterized formatter cases pass; enabled color and non-color output contain one traceback, disabled output remains a single line, ordinary logs are byte-for-byte equal, and the standard exception chain is present. + +- [ ] **Step 7: Run the complete logger unit test file** + +Run: + +```bash +uv run pytest tests/unit/test_logger.py -v +``` + +Expected: all existing timestamp and billing tests plus the new configuration and formatter tests pass. + +- [ ] **Step 8: Format, lint, and commit Task 2** + +```bash +uv run ruff format rock/logger.py tests/unit/test_logger.py +uv run ruff check rock/logger.py tests/unit/test_logger.py +git add rock/logger.py tests/unit/test_logger.py +git commit -m "fix: preserve exception tracebacks in logs" +``` + +Expected: ruff commands exit 0 and the commit contains only formatter behavior and its tests. + +--- + +### Task 3: Apply YAML configuration to admin and proxy startup + +**Files:** + +- Modify: `rock/admin/main.py:43-47,68-74,109-118` +- Create: `tests/unit/admin/test_logging_config.py` +- Modify: `rock-conf/rock-local.yml:1` +- Modify: `rock-conf/rock-dev.yml:1` +- Modify: `rock-conf/rock-test.yml:1` + +**Interfaces:** + +- Consumes: `RockConfig.logging.exception_traceback_enabled` from Task 1. +- Consumes: `configure_logging(*, exception_traceback_enabled: bool) -> None` from Task 1. +- Produces: `_apply_logging_config(rock_config: RockConfig) -> None` as the small startup seam shared by admin and proxy roles. +- Leaves: rocklet behavior unchanged; it receives no YAML injection and resolves environment/default state in the formatter. + +- [ ] **Step 1: Write the failing admin configuration forwarding test** + +Create `tests/unit/admin/test_logging_config.py` with: + +```python +import pytest + +from rock.admin import main as admin_main +from rock.config import LoggingConfig, RockConfig + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_apply_logging_config_forwards_yaml_value(monkeypatch, enabled): + calls = [] + + def capture_config(*, exception_traceback_enabled): + calls.append(exception_traceback_enabled) + + monkeypatch.setattr(admin_main, "configure_logging", capture_config) + rock_config = RockConfig(logging=LoggingConfig(exception_traceback_enabled=enabled)) + + admin_main._apply_logging_config(rock_config) + + assert calls == [enabled] +``` + +- [ ] **Step 2: Run the admin test and verify RED** + +Run: + +```bash +uv run pytest tests/unit/admin/test_logging_config.py -v +``` + +Expected: both parameter cases fail because `rock.admin.main` has neither `configure_logging` nor `_apply_logging_config`. + +- [ ] **Step 3: Wire the common admin/proxy lifespan** + +Replace the logger import in `rock/admin/main.py` with: + +```python +from rock.logger import configure_logging, init_logger, reset_log_file +``` + +Add this function after the module logger and constants: + +```python +def _apply_logging_config(rock_config: RockConfig) -> None: + configure_logging(exception_traceback_enabled=rock_config.logging.exception_traceback_enabled) +``` + +In `lifespan()`, call it immediately after YAML loading and before Nacos or service initialization: + +```python +rock_config = RockConfig.from_env(config_file_path) +_apply_logging_config(rock_config) +``` + +Both `--role admin` and `--role proxy` use `create_app()` with this same lifespan, so do not add role-specific branches. + +- [ ] **Step 4: Run the admin test and verify GREEN** + +Run: + +```bash +uv run pytest tests/unit/admin/test_logging_config.py -v +``` + +Expected: 2 passed and each YAML boolean is forwarded exactly once. + +- [ ] **Step 5: Expose the YAML option in shipped configurations** + +Add this block at the top of each of `rock-conf/rock-local.yml`, `rock-conf/rock-dev.yml`, and `rock-conf/rock-test.yml`: + +```yaml +logging: + exception_traceback_enabled: true + +``` + +Do not add the section to rocklet configuration because rocklet does not load `RockConfig`. + +- [ ] **Step 6: Validate all three YAML files parse and expose the boolean** + +Run: + +```bash +uv run python -c 'from pathlib import Path; import yaml; paths = [Path("rock-conf/rock-local.yml"), Path("rock-conf/rock-dev.yml"), Path("rock-conf/rock-test.yml")]; values = [yaml.safe_load(path.read_text())["logging"]["exception_traceback_enabled"] for path in paths]; assert values == [True, True, True], values' +``` + +Expected: command exits 0 with no output. + +- [ ] **Step 7: Run focused integration regression** + +Run: + +```bash +uv run pytest tests/unit/test_logger.py tests/unit/test_config.py tests/unit/admin/test_logging_config.py -v +``` + +Expected: all tests pass, including the original 67 logger/config tests and every new parameterized case. + +- [ ] **Step 8: Format, lint, and commit Task 3** + +```bash +uv run ruff format rock/admin/main.py tests/unit/admin/test_logging_config.py +uv run ruff check \ + rock/logger.py \ + rock/env_vars.py \ + rock/config.py \ + rock/admin/main.py \ + tests/unit/test_logger.py \ + tests/unit/test_config.py \ + tests/unit/admin/test_logging_config.py +git add \ + rock/admin/main.py \ + tests/unit/admin/test_logging_config.py \ + rock-conf/rock-local.yml \ + rock-conf/rock-dev.yml \ + rock-conf/rock-test.yml +git commit -m "feat: apply traceback config to admin" +``` + +Expected: ruff exits 0 and the commit contains only admin/proxy startup wiring, configuration examples, and the startup unit test. + +--- + +## Final Verification + +- [ ] Run the complete focused suite: + +```bash +uv run pytest tests/unit/test_logger.py tests/unit/test_config.py tests/unit/admin/test_logging_config.py -v +``` + +- [ ] Run the repository fast-test profile: + +```bash +uv run pytest -m "not need_ray and not need_admin and not need_admin_and_network" --reruns 1 +``` + +- [ ] Run final formatting verification without modifying files: + +```bash +uv run ruff format --check \ + rock/logger.py \ + rock/env_vars.py \ + rock/config.py \ + rock/admin/main.py \ + tests/unit/test_logger.py \ + tests/unit/test_config.py \ + tests/unit/admin/test_logging_config.py +``` + +- [ ] Run final lint verification: + +```bash +uv run ruff check \ + rock/logger.py \ + rock/env_vars.py \ + rock/config.py \ + rock/admin/main.py \ + tests/unit/test_logger.py \ + tests/unit/test_config.py \ + tests/unit/admin/test_logging_config.py +``` + +- [ ] Confirm scope and history: + +```bash +git status --short +git diff origin/master...HEAD --stat +git log --oneline origin/master..HEAD +``` + +Expected final state: worktree clean; no changes under `rock/rocklet/` or `rock/common/exception.py`; Issue #1260 design, configuration, formatter, startup integration, and tests appear as intentional Conventional Commits with no `Co-Authored-By` trailers. From 063d319195cf4d60f98834aa73756998029f0fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9A=86=E5=AE=87?= Date: Sun, 19 Jul 2026 22:51:10 +0800 Subject: [PATCH 3/5] feat: add configurable traceback logging --- rock/config.py | 8 ++++++++ rock/env_vars.py | 5 +++++ rock/logger.py | 13 +++++++++++++ tests/unit/test_config.py | 20 ++++++++++++++++++++ tests/unit/test_logger.py | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/rock/config.py b/rock/config.py index 7420aad738..83caee8d9d 100644 --- a/rock/config.py +++ b/rock/config.py @@ -54,6 +54,11 @@ class RedisConfig: password: str = "" +@dataclass +class LoggingConfig: + exception_traceback_enabled: bool = True + + @dataclass class SandboxLogConfig: """Policy for archiving stopped-sandbox log directories to OSS. @@ -523,6 +528,7 @@ class RockConfig: warmup: WarmupConfig = field(default_factory=WarmupConfig) nacos: NacosConfig = field(default_factory=NacosConfig) redis: RedisConfig = field(default_factory=RedisConfig) + logging: LoggingConfig = field(default_factory=LoggingConfig) sandbox_config: SandboxConfig = field(default_factory=SandboxConfig) oss: OssConfig = field(default_factory=OssConfig) lifecycle: SandboxLifecycleConfig = field(default_factory=SandboxLifecycleConfig) @@ -584,6 +590,8 @@ def from_env(cls, config_path: str | None = None): kwargs["nacos"] = NacosConfig(**config["nacos"]) if "redis" in config: kwargs["redis"] = RedisConfig(**config["redis"]) + if "logging" in config: + kwargs["logging"] = LoggingConfig(**config["logging"]) if "sandbox_config" in config: kwargs["sandbox_config"] = SandboxConfig(**config["sandbox_config"]) if "oss" in config: diff --git a/rock/env_vars.py b/rock/env_vars.py index 1cb66ed0cd..6cf560373b 100644 --- a/rock/env_vars.py +++ b/rock/env_vars.py @@ -10,6 +10,7 @@ ROCK_LOGGING_FILE_NAME: str | None = None ROCK_LOGGING_LEVEL: str | None = None ROCK_LOGGING_APPEND: bool = False + ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE: bool = True ROCK_SERVICE_STATUS_DIR: str | None = None ROCK_SCHEDULER_STATUS_DIR: str | None = None ROCK_CONFIG: str | None = None @@ -80,6 +81,10 @@ "ROCK_LOGGING_FILE_NAME": lambda: os.getenv("ROCK_LOGGING_FILE_NAME", "rocklet.log"), "ROCK_LOGGING_LEVEL": lambda: os.getenv("ROCK_LOGGING_LEVEL", "INFO"), "ROCK_LOGGING_APPEND": lambda: os.getenv("ROCK_LOGGING_APPEND", "false").lower() == "true", + "ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE": lambda: os.getenv( + "ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE", "true" + ).lower() + == "true", "ROCK_SERVICE_STATUS_DIR": lambda: os.getenv("ROCK_SERVICE_STATUS_DIR", "/tmp"), "ROCK_SCHEDULER_STATUS_DIR": lambda: os.getenv("ROCK_SCHEDULER_STATUS_DIR", "/data/scheduler_status"), "ROCK_CONFIG": lambda: os.getenv("ROCK_CONFIG"), diff --git a/rock/logger.py b/rock/logger.py index 10efeeae10..3b97a86705 100644 --- a/rock/logger.py +++ b/rock/logger.py @@ -8,6 +8,19 @@ from rock import env_vars from rock.utils import sandbox_id_ctx_var, trace_id_ctx_var +_exception_traceback_enabled = True + + +def configure_logging(*, exception_traceback_enabled: bool) -> None: + global _exception_traceback_enabled + _exception_traceback_enabled = exception_traceback_enabled + + +def is_exception_traceback_enabled() -> bool: + if env_vars.is_set("ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE"): + return env_vars.ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE + return _exception_traceback_enabled + # Define the formatter class at module level since it doesn't need configuration state class StandardFormatter(logging.Formatter): diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 1cd4938de8..7b835fb809 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -6,6 +6,7 @@ import pytest import yaml +import rock.config as config_module from rock.config import ImageRegistryMirror, RockConfig, RuntimeConfig, _resolve_k8s_template_includes @@ -15,6 +16,25 @@ async def test_rock_config(): assert rock_config +def test_logging_config_defaults_to_exception_tracebacks_enabled(): + config = config_module.LoggingConfig() + + assert config.exception_traceback_enabled is True + assert RockConfig().logging.exception_traceback_enabled is True + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_logging_config_from_yaml(tmp_path, monkeypatch, enabled): + yaml_path = tmp_path / "rock-test.yml" + yaml_path.write_text(yaml.safe_dump({"logging": {"exception_traceback_enabled": enabled}})) + monkeypatch.setenv("ROCK_PYTHON_ENV_PATH", "/usr") + monkeypatch.setenv("ROCK_ENVHUB_DB_URL", "sqlite:////tmp/test.db") + + rock_config = RockConfig.from_env(str(yaml_path)) + + assert rock_config.logging.exception_traceback_enabled is enabled + + @pytest.mark.asyncio async def test_runtime_config(): config = { diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index 6cf8fdc52d..bfd566fdba 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -3,12 +3,46 @@ import re from datetime import datetime +import pytest + +import rock.logger as logger_module from rock import env_vars from rock.actions.sandbox.sandbox_info import SandboxInfo from rock.admin.metrics.billing import log_billing_info from rock.logger import init_logger +@pytest.fixture(autouse=True) +def reset_exception_traceback_config(monkeypatch): + monkeypatch.delenv("ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE", raising=False) + logger_module.configure_logging(exception_traceback_enabled=True) + yield + logger_module.configure_logging(exception_traceback_enabled=True) + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_runtime_logging_config_used_when_environment_is_unset(enabled): + logger_module.configure_logging(exception_traceback_enabled=enabled) + + assert logger_module.is_exception_traceback_enabled() is enabled + + +@pytest.mark.parametrize( + ("configured", "environment_value", "expected"), + [ + (False, "true", True), + (True, "false", False), + (False, "TRUE", True), + (True, "FALSE", False), + ], +) +def test_environment_overrides_runtime_logging_config(monkeypatch, configured, environment_value, expected): + monkeypatch.setenv("ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE", environment_value) + logger_module.configure_logging(exception_traceback_enabled=configured) + + assert logger_module.is_exception_traceback_enabled() is expected + + def test_init_logger_iso8601_format(): env_vars.ROCK_LOGGING_PATH = "/tmp/rock_logs" env_vars.ROCK_TIME_ZONE = "Asia/Shanghai" From cb490741579d77f3c0eb797210b4c13b96c1aa4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9A=86=E5=AE=87?= Date: Sun, 19 Jul 2026 22:52:45 +0800 Subject: [PATCH 4/5] fix: preserve exception tracebacks in logs --- rock/logger.py | 10 ++++- tests/unit/test_logger.py | 89 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/rock/logger.py b/rock/logger.py index 3b97a86705..916fa152b8 100644 --- a/rock/logger.py +++ b/rock/logger.py @@ -59,10 +59,16 @@ def format(self, record): # Build header part header_str = f"{time_str} {level_str}:{file_str} [{logger_str}] [{sandbox_id}] [{trace_id}] --" + message = record.getMessage() + if is_exception_traceback_enabled() and record.exc_info and record.exc_info[0] is not None: + exception_class = record.exc_info[0] + exception_type = f"{exception_class.__module__}.{exception_class.__qualname__}" + message = f"{message.rstrip()} [exception_type={exception_type}]\n{self.formatException(record.exc_info)}" + # Color the header part and keep message in default color if self.log_color_enable: - return f"{log_color}{header_str}{RESET} {record.getMessage()}" - return f"{header_str} {record.getMessage()}" + return f"{log_color}{header_str}{RESET} {message}" + return f"{header_str} {message}" class TimezoneFormatter(StandardFormatter): diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index bfd566fdba..d907568ced 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -1,8 +1,10 @@ import io import logging import re +import sys from datetime import datetime +import httpx import pytest import rock.logger as logger_module @@ -43,6 +45,93 @@ def test_environment_overrides_runtime_logging_config(monkeypatch, configured, e assert logger_module.is_exception_traceback_enabled() is expected +def _make_exception_record(exc: Exception) -> logging.LogRecord: + try: + raise exc + except Exception: + return logging.LogRecord( + name="rock.common.exception", + level=logging.ERROR, + pathname="/tmp/exception.py", + lineno=61, + msg="Error in http_proxy: %s", + args=(str(exc),), + exc_info=sys.exc_info(), + ) + + +@pytest.mark.parametrize("log_color_enable", [True, False]) +def test_formatter_includes_empty_exception_type_and_traceback_once(log_color_enable): + formatter = logger_module.TimezoneFormatter(log_color_enable=log_color_enable, tz_string="Asia/Shanghai") + record = _make_exception_record(httpx.PoolTimeout("")) + + output = formatter.format(record) + + assert output.count("[exception_type=httpx.PoolTimeout]") == 1 + assert output.count("Traceback (most recent call last):") == 1 + assert "Error in http_proxy: [exception_type=httpx.PoolTimeout]\nTraceback" in output + assert output.rstrip().endswith("httpx.PoolTimeout") + + +def test_formatter_disabled_preserves_current_single_line_output(): + logger_module.configure_logging(exception_traceback_enabled=False) + formatter = logger_module.TimezoneFormatter(log_color_enable=False, tz_string="Asia/Shanghai") + record = _make_exception_record(httpx.PoolTimeout("")) + + output = formatter.format(record) + + assert output.endswith("-- Error in http_proxy: ") + assert "exception_type=" not in output + assert "Traceback (most recent call last):" not in output + + +def test_formatter_does_not_change_records_without_exc_info(): + formatter = logger_module.TimezoneFormatter(log_color_enable=False, tz_string="Asia/Shanghai") + record = logging.LogRecord( + name="rock.test", + level=logging.ERROR, + pathname="/tmp/test.py", + lineno=10, + msg="ordinary error", + args=(), + exc_info=None, + ) + + logger_module.configure_logging(exception_traceback_enabled=True) + enabled_output = formatter.format(record) + logger_module.configure_logging(exception_traceback_enabled=False) + disabled_output = formatter.format(record) + + assert enabled_output == disabled_output + assert enabled_output.endswith("-- ordinary error") + + +def test_formatter_preserves_standard_exception_chain(): + formatter = logger_module.TimezoneFormatter(log_color_enable=False, tz_string="Asia/Shanghai") + try: + try: + raise ValueError("inner") + except ValueError as exc: + raise RuntimeError("outer") from exc + except RuntimeError: + record = logging.LogRecord( + name="rock.common.exception", + level=logging.ERROR, + pathname="/tmp/exception.py", + lineno=61, + msg="chained failure", + args=(), + exc_info=sys.exc_info(), + ) + + output = formatter.format(record) + + assert "[exception_type=builtins.RuntimeError]" in output + assert "ValueError: inner" in output + assert "The above exception was the direct cause" in output + assert output.rstrip().endswith("RuntimeError: outer") + + def test_init_logger_iso8601_format(): env_vars.ROCK_LOGGING_PATH = "/tmp/rock_logs" env_vars.ROCK_TIME_ZONE = "Asia/Shanghai" From f0322659ec257cf9769403c1e8ba3f6321574024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9A=86=E5=AE=87?= Date: Sun, 19 Jul 2026 22:56:53 +0800 Subject: [PATCH 5/5] feat: apply traceback config to admin --- rock-conf/rock-dev.yml | 3 +++ rock-conf/rock-local.yml | 3 +++ rock-conf/rock-test.yml | 3 +++ rock/admin/main.py | 7 ++++++- tests/unit/admin/test_logging_config.py | 19 +++++++++++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/unit/admin/test_logging_config.py diff --git a/rock-conf/rock-dev.yml b/rock-conf/rock-dev.yml index 9fe2ecdc22..a13a572b74 100644 --- a/rock-conf/rock-dev.yml +++ b/rock-conf/rock-dev.yml @@ -1,3 +1,6 @@ +logging: + exception_traceback_enabled: true + # K8S deployment configuration k8s: # Kubeconfig path (None for in-cluster config) diff --git a/rock-conf/rock-local.yml b/rock-conf/rock-local.yml index 00761de151..de0beca58d 100644 --- a/rock-conf/rock-local.yml +++ b/rock-conf/rock-local.yml @@ -1,3 +1,6 @@ +logging: + exception_traceback_enabled: true + ray: runtime_env: working_dir: ./ diff --git a/rock-conf/rock-test.yml b/rock-conf/rock-test.yml index 16046d0cd7..b284db006e 100644 --- a/rock-conf/rock-test.yml +++ b/rock-conf/rock-test.yml @@ -1,3 +1,6 @@ +logging: + exception_traceback_enabled: true + ray: runtime_env: working_dir: ./ diff --git a/rock/admin/main.py b/rock/admin/main.py index 00113dbdd3..63bad558c5 100644 --- a/rock/admin/main.py +++ b/rock/admin/main.py @@ -43,7 +43,7 @@ from rock.admin.service.ops_service import OpsService from rock.common.exception import request_validation_exception_handler from rock.config import DatabaseConfig, RockConfig, SchedulerConfig -from rock.logger import init_logger, reset_log_file +from rock.logger import configure_logging, init_logger, reset_log_file from rock.sandbox.gem_manager import GemManager from rock.sandbox.operator.factory import OperatorContext, OperatorFactory, operator_requires_ray from rock.sandbox.sandbox_meta_store import SandboxMetaStore @@ -71,6 +71,10 @@ def _parse_args(): _SCHEDULER_METRICS_EXPORT_INTERVAL_MILLIS = 1_800_000 +def _apply_logging_config(rock_config: RockConfig) -> None: + configure_logging(exception_traceback_enabled=rock_config.logging.exception_traceback_enabled) + + def _init_scheduler_metrics(rock_config: RockConfig) -> tuple[MetricsMonitor, SchedulerMetrics]: """Create the scheduler-only monitor with a 30-minute fallback export.""" monitor = MetricsMonitor.create( @@ -114,6 +118,7 @@ async def lifespan(app: FastAPI): else env_vars.ROCK_CONFIG ) rock_config = RockConfig.from_env(config_file_path) + _apply_logging_config(rock_config) # Override config from Nacos if available (sandbox_config, proxy_service, lifecycle via update(); scheduler separately) if rock_config.nacos_provider: diff --git a/tests/unit/admin/test_logging_config.py b/tests/unit/admin/test_logging_config.py new file mode 100644 index 0000000000..66cc0b1f7e --- /dev/null +++ b/tests/unit/admin/test_logging_config.py @@ -0,0 +1,19 @@ +import pytest + +import rock.logger as logger_module +from rock.admin import main as admin_main +from rock.config import LoggingConfig, RockConfig + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_apply_logging_config_uses_yaml_value(monkeypatch, enabled): + monkeypatch.delenv("ROCK_LOGGING_EXCEPTION_TRACEBACK_ENABLE", raising=False) + logger_module.configure_logging(exception_traceback_enabled=not enabled) + rock_config = RockConfig(logging=LoggingConfig(exception_traceback_enabled=enabled)) + + try: + admin_main._apply_logging_config(rock_config) + + assert logger_module.is_exception_traceback_enabled() is enabled + finally: + logger_module.configure_logging(exception_traceback_enabled=True)