Skip to content

refactor: acta#21

Open
Sn0wo2 wants to merge 13 commits into
mainfrom
refactor/acta
Open

refactor: acta#21
Sn0wo2 wants to merge 13 commits into
mainfrom
refactor/acta

Conversation

@Sn0wo2
Copy link
Copy Markdown
Owner

@Sn0wo2 Sn0wo2 commented May 21, 2026

Summary by Sourcery

重构 acta 的日志管道与配置,以简化分层结构、改进过滤器处理,并现代化颜色与格式化工具。

New Features:

  • 引入构建期辅助 crate,用于从项目源码树中计算默认的路径列宽。
  • 添加 Filter::from_directive API,并在 ConfigConfigBuilder 上直接暴露过滤器配置。
  • 新增 Layers 复合类型,用于将多个 tracing-subscriber layer 作为单一单元进行管理。

Bug Fixes:

  • 修复过滤器重载行为,使 set_filterset_levelset_target_level 能够与原始指令及按目标覆盖规则保持一致的交互。
  • 确保颜色深度检测会尊重显式 writer 设置,并在禁用 ANSI 时正确回退。
  • 使路径截断与规范化在超长路径和 Windows 风格路径下更加健壮。

Enhancements:

  • 通过使用装箱的 layer 向量和 Layers 包装器替代深度嵌套的 layer 金字塔,简化 subscriber 构造。
  • 通过新的 Styled 抽象统一颜色处理,并切换到 anstyle-lossy 以进行 ANSI16 映射。
  • 围绕 from_palette 辅助函数重构 Theme 的构造,并清理主题定义。
  • 通过使用拥有所有权的 String、增加更多测试并集成新的构建期宽度辅助工具,改进路径格式化。
  • 通过移除挂起写入追踪和未使用的辅助函数,简化异步 writer 的内部实现。
  • 从公共接口中移除底层重载 API,转而推荐使用更高层的 init/TracingGuard 流程。

Documentation:

  • 为新的 acta-build 辅助工具撰写文档,并展示如何将构建期路径宽度接入 Formatter
  • 更新配置与过滤器示例,以使用 Filter::from_directive、结构化级别以及基于 LayerConfig 的格式。
  • 说明文件日志记录与重载的用法,反映底层 builder 与重载函数已不再对外公开的现状。

Tests:

  • 添加关于过滤器默认值、指令构造以及在原始指令与运行时变更下守卫行为的测试。
  • 扩展 formatter 测试以覆盖右对齐、截断策略和 Windows 路径规范化。
  • 调整或移除颜色与 writer 测试,以匹配新的颜色和异步 writer 实现。
Original summary in English

Summary by Sourcery

Refactor acta’s logging pipeline and configuration to simplify layering, improve filter handling, and modernize color and formatting utilities.

New Features:

  • Introduce a build-time helper crate to compute default path column widths from a project’s source tree.
  • Add a Filter::from_directive API and expose filter configuration directly on Config and ConfigBuilder.
  • Add a Layers composite type to manage multiple tracing-subscriber layers as a single unit.

Bug Fixes:

  • Fix filter reload behavior so that set_filter, set_level, and set_target_level interact consistently with raw directives and per-target overrides.
  • Ensure color depth detection respects explicit writer settings and falls back correctly when ANSI is disabled.
  • Make path truncation and normalization robust across very long and Windows-style paths.

Enhancements:

  • Simplify subscriber construction by replacing the deeply-nested layer pyramid with a boxed layer vector and a Layers wrapper.
  • Unify color handling via a new Styled abstraction and switch to anstyle-lossy for ANSI16 mapping.
  • Rework Theme construction around a from_palette helper and clean up theme definitions.
  • Improve path formatting by using owned Strings, adding more tests, and integrating the new build-time width helper.
  • Streamline async writer internals by removing pending-write tracking and unused helpers.
  • Remove low-level reload APIs from the public surface in favor of the higher-level init/TracingGuard flow.

Documentation:

  • Document the new acta-build helper and show how to wire build-time path width into Formatter.
  • Update configuration and filter examples to use Filter::from_directive, structured levels, and LayerConfig-based formats.
  • Clarify file-logging and reload usage now that low-level builder and reload functions are no longer public.

Tests:

  • Add tests for filter defaults, directive construction, and guard behavior with raw directives and runtime changes.
  • Expand formatter tests to cover right alignment, truncation strategies, and Windows path normalization.
  • Adjust or remove color and writer tests to match the new color and async writer implementations.
Original summary in English

Summary by Sourcery

重构 acta 的日志管道与配置,以简化分层结构、改进过滤器处理,并现代化颜色与格式化工具。

New Features:

  • 引入构建期辅助 crate,用于从项目源码树中计算默认的路径列宽。
  • 添加 Filter::from_directive API,并在 ConfigConfigBuilder 上直接暴露过滤器配置。
  • 新增 Layers 复合类型,用于将多个 tracing-subscriber layer 作为单一单元进行管理。

Bug Fixes:

  • 修复过滤器重载行为,使 set_filterset_levelset_target_level 能够与原始指令及按目标覆盖规则保持一致的交互。
  • 确保颜色深度检测会尊重显式 writer 设置,并在禁用 ANSI 时正确回退。
  • 使路径截断与规范化在超长路径和 Windows 风格路径下更加健壮。

Enhancements:

  • 通过使用装箱的 layer 向量和 Layers 包装器替代深度嵌套的 layer 金字塔,简化 subscriber 构造。
  • 通过新的 Styled 抽象统一颜色处理,并切换到 anstyle-lossy 以进行 ANSI16 映射。
  • 围绕 from_palette 辅助函数重构 Theme 的构造,并清理主题定义。
  • 通过使用拥有所有权的 String、增加更多测试并集成新的构建期宽度辅助工具,改进路径格式化。
  • 通过移除挂起写入追踪和未使用的辅助函数,简化异步 writer 的内部实现。
  • 从公共接口中移除底层重载 API,转而推荐使用更高层的 init/TracingGuard 流程。

Documentation:

  • 为新的 acta-build 辅助工具撰写文档,并展示如何将构建期路径宽度接入 Formatter
  • 更新配置与过滤器示例,以使用 Filter::from_directive、结构化级别以及基于 LayerConfig 的格式。
  • 说明文件日志记录与重载的用法,反映底层 builder 与重载函数已不再对外公开的现状。

Tests:

  • 添加关于过滤器默认值、指令构造以及在原始指令与运行时变更下守卫行为的测试。
  • 扩展 formatter 测试以覆盖右对齐、截断策略和 Windows 路径规范化。
  • 调整或移除颜色与 writer 测试,以匹配新的颜色和异步 writer 实现。
Original summary in English

Summary by Sourcery

Refactor acta’s logging pipeline and configuration to simplify layering, improve filter handling, and modernize color and formatting utilities.

New Features:

  • Introduce a build-time helper crate to compute default path column widths from a project’s source tree.
  • Add a Filter::from_directive API and expose filter configuration directly on Config and ConfigBuilder.
  • Add a Layers composite type to manage multiple tracing-subscriber layers as a single unit.

Bug Fixes:

  • Fix filter reload behavior so that set_filter, set_level, and set_target_level interact consistently with raw directives and per-target overrides.
  • Ensure color depth detection respects explicit writer settings and falls back correctly when ANSI is disabled.
  • Make path truncation and normalization robust across very long and Windows-style paths.

Enhancements:

  • Simplify subscriber construction by replacing the deeply-nested layer pyramid with a boxed layer vector and a Layers wrapper.
  • Unify color handling via a new Styled abstraction and switch to anstyle-lossy for ANSI16 mapping.
  • Rework Theme construction around a from_palette helper and clean up theme definitions.
  • Improve path formatting by using owned Strings, adding more tests, and integrating the new build-time width helper.
  • Streamline async writer internals by removing pending-write tracking and unused helpers.
  • Remove low-level reload APIs from the public surface in favor of the higher-level init/TracingGuard flow.

Documentation:

  • Document the new acta-build helper and show how to wire build-time path width into Formatter.
  • Update configuration and filter examples to use Filter::from_directive, structured levels, and LayerConfig-based formats.
  • Clarify file-logging and reload usage now that low-level builder and reload functions are no longer public.

Tests:

  • Add tests for filter defaults, directive construction, and guard behavior with raw directives and runtime changes.
  • Expand formatter tests to cover right alignment, truncation strategies, and Windows path normalization.
  • Adjust or remove color and writer tests to match the new color and async writer implementations.
Original summary in English

Summary by Sourcery

重构 acta 的日志管道与配置,以简化分层结构、改进过滤器处理,并现代化颜色与格式化工具。

New Features:

  • 引入构建期辅助 crate,用于从项目源码树中计算默认的路径列宽。
  • 添加 Filter::from_directive API,并在 ConfigConfigBuilder 上直接暴露过滤器配置。
  • 新增 Layers 复合类型,用于将多个 tracing-subscriber layer 作为单一单元进行管理。

Bug Fixes:

  • 修复过滤器重载行为,使 set_filterset_levelset_target_level 能够与原始指令及按目标覆盖规则保持一致的交互。
  • 确保颜色深度检测会尊重显式 writer 设置,并在禁用 ANSI 时正确回退。
  • 使路径截断与规范化在超长路径和 Windows 风格路径下更加健壮。

Enhancements:

  • 通过使用装箱的 layer 向量和 Layers 包装器替代深度嵌套的 layer 金字塔,简化 subscriber 构造。
  • 通过新的 Styled 抽象统一颜色处理,并切换到 anstyle-lossy 以进行 ANSI16 映射。
  • 围绕 from_palette 辅助函数重构 Theme 的构造,并清理主题定义。
  • 通过使用拥有所有权的 String、增加更多测试并集成新的构建期宽度辅助工具,改进路径格式化。
  • 通过移除挂起写入追踪和未使用的辅助函数,简化异步 writer 的内部实现。
  • 从公共接口中移除底层重载 API,转而推荐使用更高层的 init/TracingGuard 流程。

Documentation:

  • 为新的 acta-build 辅助工具撰写文档,并展示如何将构建期路径宽度接入 Formatter
  • 更新配置与过滤器示例,以使用 Filter::from_directive、结构化级别以及基于 LayerConfig 的格式。
  • 说明文件日志记录与重载的用法,反映底层 builder 与重载函数已不再对外公开的现状。

Tests:

  • 添加关于过滤器默认值、指令构造以及在原始指令与运行时变更下守卫行为的测试。
  • 扩展 formatter 测试以覆盖右对齐、截断策略和 Windows 路径规范化。
  • 调整或移除颜色与 writer 测试,以匹配新的颜色和异步 writer 实现。
Original summary in English

Summary by Sourcery

Refactor acta’s logging pipeline and configuration to simplify layering, improve filter handling, and modernize color and formatting utilities.

New Features:

  • Introduce a build-time helper crate to compute default path column widths from a project’s source tree.
  • Add a Filter::from_directive API and expose filter configuration directly on Config and ConfigBuilder.
  • Add a Layers composite type to manage multiple tracing-subscriber layers as a single unit.

Bug Fixes:

  • Fix filter reload behavior so that set_filter, set_level, and set_target_level interact consistently with raw directives and per-target overrides.
  • Ensure color depth detection respects explicit writer settings and falls back correctly when ANSI is disabled.
  • Make path truncation and normalization robust across very long and Windows-style paths.

Enhancements:

  • Simplify subscriber construction by replacing the deeply-nested layer pyramid with a boxed layer vector and a Layers wrapper.
  • Unify color handling via a new Styled abstraction and switch to anstyle-lossy for ANSI16 mapping.
  • Rework Theme construction around a from_palette helper and clean up theme definitions.
  • Improve path formatting by using owned Strings, adding more tests, and integrating the new build-time width helper.
  • Streamline async writer internals by removing pending-write tracking and unused helpers.
  • Remove low-level reload APIs from the public surface in favor of the higher-level init/TracingGuard flow.

Documentation:

  • Document the new acta-build helper and show how to wire build-time path width into Formatter.
  • Update configuration and filter examples to use Filter::from_directive, structured levels, and LayerConfig-based formats.
  • Clarify file-logging and reload usage now that low-level builder and reload functions are no longer public.

Tests:

  • Add tests for filter defaults, directive construction, and guard behavior with raw directives and runtime changes.
  • Expand formatter tests to cover right alignment, truncation strategies, and Windows path normalization.
  • Adjust or remove color and writer tests to match the new color and async writer implementations.
Original summary in English

Summary by Sourcery

重构 acta 的日志管道与配置,以简化分层结构、改进过滤器处理,并现代化颜色与格式化工具。

New Features:

  • 引入构建期辅助 crate,用于从项目源码树中计算默认的路径列宽。
  • 添加 Filter::from_directive API,并在 ConfigConfigBuilder 上直接暴露过滤器配置。
  • 新增 Layers 复合类型,用于将多个 tracing-subscriber layer 作为单一单元进行管理。

Bug Fixes:

  • 修复过滤器重载行为,使 set_filterset_levelset_target_level 能够与原始指令及按目标覆盖规则保持一致的交互。
  • 确保颜色深度检测会尊重显式 writer 设置,并在禁用 ANSI 时正确回退。
  • 使路径截断与规范化在超长路径和 Windows 风格路径下更加健壮。

Enhancements:

  • 通过使用装箱的 layer 向量和 Layers 包装器替代深度嵌套的 layer 金字塔,简化 subscriber 构造。
  • 通过新的 Styled 抽象统一颜色处理,并切换到 anstyle-lossy 以进行 ANSI16 映射。
  • 围绕 from_palette 辅助函数重构 Theme 的构造,并清理主题定义。
  • 通过使用拥有所有权的 String、增加更多测试并集成新的构建期宽度辅助工具,改进路径格式化。
  • 通过移除挂起写入追踪和未使用的辅助函数,简化异步 writer 的内部实现。
  • 从公共接口中移除底层重载 API,转而推荐使用更高层的 init/TracingGuard 流程。

Documentation:

  • 为新的 acta-build 辅助工具撰写文档,并展示如何将构建期路径宽度接入 Formatter
  • 更新配置与过滤器示例,以使用 Filter::from_directive、结构化级别以及基于 LayerConfig 的格式。
  • 说明文件日志记录与重载的用法,反映底层 builder 与重载函数已不再对外公开的现状。

Tests:

  • 添加关于过滤器默认值、指令构造以及在原始指令与运行时变更下守卫行为的测试。
  • 扩展 formatter 测试以覆盖右对齐、截断策略和 Windows 路径规范化。
  • 调整或移除颜色与 writer 测试,以匹配新的颜色和异步 writer 实现。
Original summary in English

Summary by Sourcery

Refactor acta’s logging pipeline and configuration to simplify layering, improve filter handling, and modernize color and formatting utilities.

New Features:

  • Introduce a build-time helper crate to compute default path column widths from a project’s source tree.
  • Add a Filter::from_directive API and expose filter configuration directly on Config and ConfigBuilder.
  • Add a Layers composite type to manage multiple tracing-subscriber layers as a single unit.

Bug Fixes:

  • Fix filter reload behavior so that set_filter, set_level, and set_target_level interact consistently with raw directives and per-target overrides.
  • Ensure color depth detection respects explicit writer settings and falls back correctly when ANSI is disabled.
  • Make path truncation and normalization robust across very long and Windows-style paths.

Enhancements:

  • Simplify subscriber construction by replacing the deeply-nested layer pyramid with a boxed layer vector and a Layers wrapper.
  • Unify color handling via a new Styled abstraction and switch to anstyle-lossy for ANSI16 mapping.
  • Rework Theme construction around a from_palette helper and clean up theme definitions.
  • Improve path formatting by using owned Strings, adding more tests, and integrating the new build-time width helper.
  • Streamline async writer internals by removing pending-write tracking and unused helpers.
  • Remove low-level reload APIs from the public surface in favor of the higher-level init/TracingGuard flow.

Documentation:

  • Document the new acta-build helper and show how to wire build-time path width into Formatter.
  • Update configuration and filter examples to use Filter::from_directive, structured levels, and LayerConfig-based formats.
  • Clarify file-logging and reload usage now that low-level builder and reload functions are no longer public.

Tests:

  • Add tests for filter defaults, directive construction, and guard behavior with raw directives and runtime changes.
  • Expand formatter tests to cover right alignment, truncation strategies, and Windows path normalization.
  • Adjust or remove color and writer tests to match the new color and async writer implementations.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

📝 Walkthrough

走查

该 PR 对 acta 的颜色处理、配置系统和初始化流程进行了系统性重构。核心变化包括引入 Styled 结构体取代函数式颜色转换、将 Filter 配置改为紧凑字符串形式、简化 LayerBuilder 和 init 流程,以及清理异步写入器的计数机制。同时更新了文档、演示程序和相关测试。

变更概览

核心系统重构

层 / 文件 概述
依赖更新与构建工具
Cargo.toml, build.rs, crates/acta-build/...
移除 arrayvec 和冗余颜色依赖;改进 build.rs 错误处理,将路径宽度计算从错误返回改为兜底值;更新 acta-build 签名使用 AsRef。
Styled 颜色抽象
src/color/style.rs, src/color/mod.rs, src/color/tests.rs, src/color/mapping.rs
新增 Styled 结构体封装 RGB、ColorDepth 和 on 标志;实现 From for OwoStyle;移除函数式颜色转换 API (rgb_to_owo/rgb_to_owo_on/theme_fg_dimmed);收缩 color 模块对外导出。
Filter 与 Level 重构
src/config/mod.rs, src/config/depth.rs, src/config/test.rs
消除公共 color depth 检测 API;将 Filter 从 Level 枚举改为 CompactString base 指令;新增 Filter::from_directive 工厂;Config 改用 filter 字段替代 level;ConfigBuilder 新增 filter() 方法。
Builder 与初始化
src/builder.rs
新增 Layers 包装和 InnerSubscriber 类型别名;build_layer 改为返回 BoxedLayer;init 直接组装层、创建 reload 层;移除 SubscriberParts 和 build_reload_filter;TracingGuard 新增 set_filter、调整 set_level/set_target_level 签名。
Formatter 样式迁移
src/fmt/mod.rs, src/fmt/test.rs
format_path 返回 String 而非 ArrayString;所有样式使用 Styled + OwoStyle;移除 LevelStyles 和 style_handle;更新路径宽度常量为 DEFAULT_PATH_WIDTH;新增路径截断与 UTF-8 边界的测试。
AsyncWriter 简化
src/writer/custom.rs, src/writer/mod.rs, src/writer/test.rs
移除 Arc 计数字段和 pending_writes 方法;删除 async_writer() 工厂函数;简化通道错误处理;仅保留 AsyncWriter 和 async_writer_for 导出。
API 导出整理
src/lib.rs, src/prelude.rs
合并 config 导出语句;移除 SubscriberParts/build_reload_filter/build_subscriber 导出;调整 async_writer 导出;更新 builder 重导出。

文档与演示

层 / 文件 概述
README 示例更新
README.md
更新快速开始、控制台格式、文件日志、过滤指令的代码示例,展示新的 Filter::from_directive、Format::Compact(LayerConfig) 和 guard.set_filter 用法;更新主题和图标的常量初始化方式。
演示程序调整
crates/acta-debug/src/main.rs
移除 build_reload_filter 导入,新增 Filter;删除 RELOAD 演示流程,替代为 Filter::from_directive 展示;改写 init 演示为展示运行时更新(set_level/set_target_level/with_style)。
测试助手与用例
src/test.rs
用 build_test_guard 替代 build_reload_filter;统一 build_layer 调用使用类型推断;新增 set_filter/set_level/set_target_level 运行时更新行为的测试。

🎯 4 (复杂) | ⏱️ ~75 分钟

🐰 在五彩斑斓的颜色森林里,
Styled 小兔蹦蹦跳跳替换了老旧函数;
Filter 指令串编织新的过滤之轮,
LayerBuilder 为初始化道路扫清了杂草。
简洁 API 在春风中绽放光彩!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning 标题过于宽泛模糊,"refactor: acta"没有具体说明此次重构的核心内容或主要变更点。 使用更具体的标题描述主要变更,例如"refactor: simplify acta logging pipeline and modernize filter handling"或类似表述。
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed 描述内容与变更集高度相关,详细说明了新特性、bug修复、增强、文档和测试变更,完全符合此次PR的实际改动。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 21, 2026

Reviewer's Guide

重构 acta 的 tracing 设置以使用 boxed layers 和基于向量的分层 subscriber,引入带有 serde 支持的基于 Filter 的配置模型,简化围绕 init/TracingGuard 的公共 API,集中管理颜色/样式处理,使路径宽度计算可通过 acta-build 在构建时配置,并相应更新文档和测试。

通过 TracingGuard 在运行时重新加载过滤器的时序图

sequenceDiagram
    actor User
    participant App
    participant TracingGuard
    participant ReloadHandle
    participant EnvFilter

    User->>App: request filter update
    App->>TracingGuard: set_level(level) / set_filter(filter)
    TracingGuard->>EnvFilter: EnvFilter::try_new(filter.as_directive())
    EnvFilter-->>TracingGuard: EnvFilter
    TracingGuard->>ReloadHandle: modify(closure_set_envfilter)
    ReloadHandle-->>TracingGuard: Result
    TracingGuard-->>App: Result
    App-->>User: filtering updated
Loading

File-Level Changes

Change Details Files
使用 boxed layers 和自定义 Layers 聚合器替换深度嵌套的泛型 layer 栈,并简化 subscriber 的构造/初始化。
  • 引入 BoxedLayer 和实现 Layer 的 Layers 包装器,将生命周期回调扇出到内部 layers。
  • 用 InnerSubscriber = Layered<Layers, Registry> 和直接的 reload Handle 替换 Layer1–Layer4 泛型类型金字塔以及自定义 ReloadHandle newtype。
  • 重写 build_layer/build_subscriber 为 build_layer/build_layer_with,加上新的 init(Config),用于构建 Registry + Layers + EnvFilter reload layer,并返回 TracingGuard。
src/builder.rs
src/test.rs
crates/acta-debug/src/main.rs
src/lib.rs
src/prelude.rs
重新设计配置,使用基于 EnvFilter 指令的 Filter 类型,并相应调整 Config/ConfigBuilder 和 Level 语义。
  • 移除 Level::Custom,使 Level 成为一个简单的枚举,内部为 &'static 指令字符串,并添加 serde 支持。
  • 引入 Filter{ base, targets },提供 new(Level)、from_directive(String)、with_target/remove_target/as_directive,且 Default = info。
  • 修改 Config,使其保存 filter: Filter 而非 level: Level,更新 Config::default 和 ConfigBuilder 以从 level 或显式的 Filter 构建过滤器。
  • 调整 TracingGuard API 以端到端使用 Filter(set_filter、set_level、set_target_level),并通过 EnvFilter::try_new(filter.as_directive()) 重新应用过滤器。
src/config/mod.rs
src/builder.rs
src/config/test.rs
src/test.rs
crates/acta-debug/src/main.rs
README.md
优化格式化内部实现:基于动态 String 的路径格式化、新的 Styled 辅助工具用于 RGB 到样式映射,并通过 acta-build 在构建时配置路径宽度。
  • 用返回 String 的 Formatter::format_path 替换基于 ArrayString 的路径格式化,通过 build-time include! 推导 DEFAULT_PATH_WIDTH,并可通过 Formatter::with_path_width 配置。
  • 修改路径规范化/截断逻辑,以 String 为基础操作,实现右对齐、去除 src 前缀的路径,对超长路径使用省略号,并为多种截断场景添加测试。
  • 移除 LevelStyles 和 rgb_to_owo/rgb_to_owo_on/theme_fg_dimmed 辅助函数,改为使用 Styled 辅助工具封装 RGB + ColorDepth + dim/bg 标志,并通过 anstyle-lossy 转换为 OwoStyle。
  • 更新所有格式化调用点(时间、路径、span、level 方括号、键/值对)以使用 Styled 构造样式,并通过 OwoStyle::from 应用。
src/fmt/mod.rs
src/fmt/test.rs
src/color/style.rs
src/color/mod.rs
src/color/tests.rs
收紧公共 API 表面和 prelude,只暴露高级辅助工具,移除低级 reload/映射工具。
  • 停止从根模块导出 rgb_to_ansi16 和颜色映射辅助函数;通过 Styled 将其保持为内部实现。
  • 从公共 API 中移除 build_reload_filter、build_subscriber、SubscriberParts、shutdown 以及其他低级 reload 钩子;TracingGuard 现在仅覆盖样式和过滤器管理。
  • 从 lib 和 prelude 重新导出 init、build_layer 以及配置类型(Config、Filter、Format、LayerConfig、Level、Rotation、Writer、WriterTarget 等),同时从公共表面移除 async_writer,改为使用 async_writer_for。
  • 使 README 示例和说明与新的高级 API 保持一致,移除手动 subscriber + build_reload_filter 示例。
src/lib.rs
src/prelude.rs
src/builder.rs
README.md
src/color/mod.rs
改进异步 writer 行为并简化自定义异步 writer 的实现。
  • 从 writer/custom.rs 中移除挂起写入计数跟踪和 AsyncWriter::pending_writes,仅保留通过有界 mpsc 通道实现的背压机制以及溢出时写入 stderr 的警告。
  • 删除 async_writer 便捷构造函数,改为依赖 async_writer_for(target) 构建 AsyncWriter 实例。
  • 保留错误路径行为:当 TrySendError::Full 时记录警告并丢弃消息;TrySendError::Closed 则返回 BrokenPipe 错误。
src/writer/custom.rs
src/writer/mod.rs
强化 acta-build 的路径宽度计算,并简化主 crate 的构建脚本,使其无失败分支。
  • 修改 acta-build::walk_src_max_width,使其接受 AsRef,直接返回 usize,当未找到任何 .rs 文件时默认使用 FALLBACK_WIDTH (40),并添加填充值常量 PADDING = 4。
  • 在 crate 级文档中记录 acta-build,并更新其 Cargo.toml 的 description。
  • 简化根 build.rs,只调用 walk_src_max_width、写入 path_width 文件,并仅在写入失败时输出一条 cargo:warning,同时在 src 变更时始终重新运行。
crates/acta-build/src/lib.rs
crates/acta-build/Cargo.toml
build.rs
更新文档和测试以体现基于 Filter 的配置、LayerConfig 的使用、主题/标签常量、路径宽度自定义以及 reload 行为变化。
  • 调整 README 代码示例,使其使用 Config.filter 配合 Level.into()/Filter::from_directive、Format 变体中的 LayerConfig、新的 Theme 构造方式以及 LevelLabels::DEFAULT/SHORT 常量。
  • 记录通过 acta-build 和 ACTA_PATH_WIDTH 进行构建时和用户提供的路径宽度自定义方式。
  • 更新测试内容,包括配置默认值、builder 行为、过滤器指令、formatter 默认值/路径格式化、TracingGuard 过滤行为和颜色映射,并添加围绕原始指令和路径截断的新测试。
  • 移除 color::mapping 模块及其测试,改为依赖 Styled + anstyle-lossy 进行颜色映射。
README.md
src/config/test.rs
src/fmt/test.rs
src/test.rs
src/color/tests.rs
src/color/mapping.rs

Tips and commands

Interacting with Sourcery

  • 触发新的代码审查: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 通过回复某条审查评论,让 Sourcery 基于该评论创建 issue。你也可以回复审查评论 @sourcery-ai issue 来从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题中任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 描述正文任意位置写上 @sourcery-ai summary,即可在相应位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来在任意时间(重新)生成摘要。
  • 生成审查者指南: 在 pull request 上评论 @sourcery-ai guide,即可在任意时间(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 上评论 @sourcery-ai resolve 以解决所有 Sourcery 评论。当你已经处理完所有评论且不希望再看到它们时,这会非常有用。
  • 关闭所有 Sourcery 审查: 在 pull request 上评论 @sourcery-ai dismiss 以关闭所有现有 Sourcery 审查。尤其适用于你希望从一个全新的审查开始的场景——别忘了再评论 @sourcery-ai review 来触发新的审查!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用诸如 Sourcery 自动生成的 pull request 摘要、审查者指南等审查功能。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查指令。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Refactors acta’s tracing setup to use boxed layers and a vector-based layered subscriber, introduces a new Filter-based configuration model with serde support, simplifies the public API around init/TracingGuard, centralizes color/style handling, makes path-width computation build-time configurable via acta-build, and updates docs/tests accordingly.

Sequence diagram for runtime filter reload via TracingGuard

sequenceDiagram
    actor User
    participant App
    participant TracingGuard
    participant ReloadHandle
    participant EnvFilter

    User->>App: request filter update
    App->>TracingGuard: set_level(level) / set_filter(filter)
    TracingGuard->>EnvFilter: EnvFilter::try_new(filter.as_directive())
    EnvFilter-->>TracingGuard: EnvFilter
    TracingGuard->>ReloadHandle: modify(closure_set_envfilter)
    ReloadHandle-->>TracingGuard: Result
    TracingGuard-->>App: Result
    App-->>User: filtering updated
Loading

File-Level Changes

Change Details Files
Replace deeply nested generic layer stacks with boxed layers and a custom Layers aggregator, and simplify subscriber construction/init.
  • Introduce BoxedLayer and Layers wrapper implementing Layer that fans out lifecycle callbacks to inner layers.
  • Replace the Layer1–Layer4 generic type pyramid and custom ReloadHandle newtype with InnerSubscriber = Layered<Layers, Registry> and a direct reload Handle.
  • Rewrite build_layer/build_subscriber as build_layer/build_layer_with plus a new init(Config) that builds a Registry + Layers + EnvFilter reload layer and returns a TracingGuard.
src/builder.rs
src/test.rs
crates/acta-debug/src/main.rs
src/lib.rs
src/prelude.rs
Rework configuration to use a Filter type over EnvFilter directives and adjust Config/ConfigBuilder and Level semantics accordingly.
  • Remove Level::Custom and make Level a simple enum with &'static directive strings and serde support.
  • Introduce Filter{ base, targets } with new(Level), from_directive(String), with_target/remove_target/as_directive, plus Default = info.
  • Change Config to hold filter: Filter instead of level: Level, update Config::default and ConfigBuilder to build filters from levels or explicit Filter values.
  • Adjust TracingGuard API to use Filter end-to-end (set_filter, set_level, set_target_level) and to reapply filters via EnvFilter::try_new(filter.as_directive()).
src/config/mod.rs
src/builder.rs
src/config/test.rs
src/test.rs
crates/acta-debug/src/main.rs
README.md
Refine formatting internals: dynamic String-based path formatting, new Styled helper for RGB-to-style mapping, and build-time configurable path width via acta-build.
  • Replace ArrayString-based path formatting with Formatter::format_path returning String, using DEFAULT_PATH_WIDTH derived from build-time include! and configurable via Formatter::with_path_width.
  • Change path normalization/truncation logic to operate on Strings with right-aligned, src-stripped paths, ellipsis for overlong paths, and tests for multiple truncation scenarios.
  • Remove LevelStyles and rgb_to_owo/rgb_to_owo_on/theme_fg_dimmed helpers in favor of a Styled helper that encapsulates RGB + ColorDepth + dim/bg flags and converts into OwoStyle via anstyle-lossy.
  • Update all formatting call sites (time, path, spans, level brackets, key/value pairs) to construct styles using Styled and OwoStyle::from.
src/fmt/mod.rs
src/fmt/test.rs
src/color/style.rs
src/color/mod.rs
src/color/tests.rs
Tighten the public API surface and prelude to expose only high-level helpers and remove low-level reload/mapping utilities.
  • Stop exporting rgb_to_ansi16 and color mapping helpers from the root module; keep them internal via Styled.
  • Remove build_reload_filter, build_subscriber, SubscriberParts, shutdown, and other low-level reload hooks from the public API; TracingGuard now covers only style and filter management.
  • Re-export init, build_layer, and configuration types (Config, Filter, Format, LayerConfig, Level, Rotation, Writer, WriterTarget, etc.) from lib and prelude, while dropping async_writer from the public surface in favor of async_writer_for.
  • Align README examples and narrative with the new high-level API, removing manual subscriber + build_reload_filter examples.
src/lib.rs
src/prelude.rs
src/builder.rs
README.md
src/color/mod.rs
Improve async writer behavior and simplify custom async writer implementation.
  • Remove pending write-count tracking and AsyncWriter::pending_writes from writer/custom.rs, leaving only backpressure via bounded mpsc channel and stderr warnings on overflow.
  • Drop the async_writer convenience constructor and rely on async_writer_for(target) to build AsyncWriter instances.
  • Keep error-path behavior: TrySendError::Full logs a warning and drops the message, TrySendError::Closed returns a BrokenPipe error.
src/writer/custom.rs
src/writer/mod.rs
Harden acta-build’s path-width computation and simplify the main crate’s build script to be fallible-free.
  • Change acta-build::walk_src_max_width to take an AsRef, return usize directly, default to FALLBACK_WIDTH (40) when no .rs files are found, and add padding constant PADDING = 4.
  • Document acta-build in its crate-level docs and update its Cargo.toml description.
  • Simplify the root build.rs to call walk_src_max_width, write the path_width file, and only emit a cargo:warning if writing fails, while always rerunning on src changes.
crates/acta-build/src/lib.rs
crates/acta-build/Cargo.toml
build.rs
Update documentation and tests to reflect Filter-based configuration, LayerConfig usage, theme/label constants, path-width customization, and reload behavior changes.
  • Adjust README code samples to use Config.filter with Level.into()/Filter::from_directive, LayerConfig in Format variants, new Theme construction style, and LevelLabels::DEFAULT/SHORT constants.
  • Document build-time and user-provided path-width customization via acta-build and ACTA_PATH_WIDTH.
  • Update tests for config defaults, builder behavior, filter directives, formatter defaults/path formatting, TracingGuard filter behavior, and color mapping, including new tests around raw directives and path truncation.
  • Remove color::mapping module and its tests, relying on Styled + anstyle-lossy for color mapping.
README.md
src/config/test.rs
src/fmt/test.rs
src/test.rs
src/color/tests.rs
src/color/mapping.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the subscriber initialization by replacing a complex layered type system with a vector of boxed layers and simplifies the configuration and filtering logic. It also removes several dependencies, including arrayvec, smart-default, and derive_more. Feedback identifies critical bugs in the path formatting logic, including incorrect string manipulation that reverses directory names and potential panics due to unsafe string slicing on non-character boundaries. Additionally, the reviewer raised concerns regarding performance regressions caused by introducing heap allocations in the logging hot path.

Comment thread src/fmt/mod.rs Outdated
Comment thread src/fmt/mod.rs Outdated
Comment thread src/fmt/mod.rs
@Sn0wo2
Copy link
Copy Markdown
Owner Author

Sn0wo2 commented May 21, 2026

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 5 个问题,并提供了一些高层次的反馈:

  • 新的 Formatter::format_path 实现比较难以理解(多次分配、先反转再切分的逻辑等);建议把截断逻辑抽取到一个有文档的小辅助函数中,并简化目录尾部计算逻辑,这样预期行为会更清晰、更容易推理。
  • color::style::Styled::from 中,ANSI16 映射每次转换都会新建一个 16 元素数组并调用一次 Palette::default();你可以通过复用一个静态的调色板和颜色表(或者一个小的辅助函数),避免每次调用都重新构建它们,从而降低开销。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `Formatter::format_path` implementation is quite hard to follow (multiple allocations, reverse-then-split logic, etc.); consider extracting the truncation logic into a small, documented helper and simplifying the directory tail calculation so the intended behavior is clearer and easier to reason about.
- In `color::style::Styled::from`, the ANSI16 mapping allocates a fresh 16-element array and calls `Palette::default()` on every conversion; you could reduce overhead by reusing a static palette and color table (or a small helper function) instead of rebuilding them per call.

## Individual Comments

### Comment 1
<location path="src/builder.rs" line_range="18-27" />
<code_context>
-    }
-}
+pub(crate) type BoxedLayer = Box<dyn tracing_subscriber::Layer<Registry> + Send + Sync>;
+pub(crate) type InnerSubscriber = Layered<Vec<BoxedLayer>, Registry>;
+pub(crate) type ReloadHandle =
+    tracing_subscriber::reload::Handle<tracing_subscriber::EnvFilter, InnerSubscriber>;
</code_context>
<issue_to_address>
**issue (bug_risk):** 使用 `Vec<BoxedLayer>` 作为 `Registry``Layer` 看起来并不安全,而且很可能会因为孤儿规则(orphan rules)而无法通过编译。

`InnerSubscriber` 假设 `Vec<BoxedLayer>` 实现了 `Layer<Registry>`,从而让 `Registry::default().with(layers)` 能通过类型检查,但你不能在这里添加 `impl Layer<Registry> for Vec<BoxedLayer>`,因为根据孤儿规则,`Layer``Vec` 都是外部类型。这意味着在当前 crate 中不可能让 `Vec<BoxedLayer>` 满足 `Layer<S>` 约束。

如果现在代码是可以编译的,很可能说明 `InnerSubscriber` 已经不再匹配实际的订阅者类型,因此 `reload::Handle<EnvFilter, InnerSubscriber>` 也是不正确的。相反,建议使用一个本地的新类型包装,例如 `struct Layers(Vec<BoxedLayer>); impl<S> Layer<S> for Layers where S: Subscriber + LookupSpan<'_> { … }`,然后在 `.with(...)``InnerSubscriber` 类型别名中都使用 `Layers`。
</issue_to_address>

### Comment 2
<location path="src/fmt/mod.rs" line_range="135-144" />
<code_context>
+    pub(crate) fn format_path(&self, file: &str, line: u32) -> String {
</code_context>
<issue_to_address>
**issue (bug_risk):** 新的路径压缩逻辑比较难以推理,而且很可能没有产生你期望的目录前缀。

新的实现会对 `[..last_slash]` 做反转,然后在反转后的字符串上按 `'/'` 切分。由于目录分隔符也跟着字符一起被反转,得到的片段不再和原始路径组件一一对应,很难真正表示“仍然能容纳的尾部目录名”。它还在一个热点路径上创建了若干中间 `String`,并做了额外的逐 `char` 处理。

更简单、更安全的方法是在概念上保持旧行为:在正向路径字节上操作,从左侧裁剪直到 `len <= max_width`,如果发生了裁剪就前置 ``。如果你想保留文件名加部分目录上下文,可以:
1. 构造 `file_with_line = format!("{filename}:{line}")`2. 如果长度合适,就像现在一样右对齐。
3. 如果不合适,就计算在保证 `format!("{tail}:{line}")` 能放得下的前提下,可以保留 `path_str` 末尾的多少内容,或者退回到 `` + 后缀的逻辑——整个过程不需要反转字符串。

这样逻辑更容易理解,也可以避免被破坏的目录名称。
</issue_to_address>

### Comment 3
<location path="src/fmt/test.rs" line_range="9" />
<code_context>
 fn formatter_defaults() {
     let fmt = Formatter::new();
     assert_eq!(fmt.time_format, "%H:%M:%S");
-    assert_eq!(fmt.path_width, BUILD_PATH_WIDTH);
+    assert_eq!(fmt.path_width, DEFAULT_PATH_WIDTH);
     assert!(fmt.show_path);
     assert!(fmt.show_spans);
</code_context>
<issue_to_address>
**suggestion (testing):** 为新的 `Formatter::format_path` 截断和规范化逻辑添加有针对性的测试。

当前新的 `Formatter::format_path` 逻辑(基于字符串的截断、`src/``src\` 的处理、省略号以及目录尾部启发式规则)除了这个默认宽度检查外,没有被覆盖到。

请添加一些聚焦的测试,用来:
- 覆盖短路径(无需截断,右对齐),
- 覆盖会变成相对路径并被截断的长 `src/` 路径,
- 验证 Windows 风格路径被规范为 `/`- 覆盖 `file_with_line.len() + 2 <= max_width` 这个分支,
- 覆盖最终带前导省略号的分支。

你可以通过将 `fmt.path_width` 设置为较小值并断言精确的格式化字符串,让这些测试具有确定性。

Suggested implementation:

```rust
fn formatter_defaults() {
    let fmt = Formatter::new();
    assert_eq!(fmt.time_format, "%H:%M:%S");
    assert_eq!(fmt.path_width, DEFAULT_PATH_WIDTH);
    assert!(fmt.show_path);
    assert!(fmt.show_spans);
}

#[test]
fn format_path_short_no_truncation_right_aligned() {
    let mut fmt = Formatter::new();
    // Wide enough that no truncation happens
    fmt.path_width = 40;

    let formatted = fmt.format_path("src/lib.rs", 10);

    // No truncation markers
    assert!(!formatted.contains('…'), "expected no ellipsis: {formatted}");
    assert!(!formatted.starts_with("..."), "expected no ascii ellipsis: {formatted}");

    // Should end with the full file-with-line tail and be right-aligned within the field
    assert!(formatted.ends_with("src/lib.rs:10"), "expected to end with full path: {formatted}");
    assert!(
        formatted.len() <= fmt.path_width,
        "formatted length {} should not exceed path_width {}",
        formatted.len(),
        fmt.path_width
    );
}

#[test]
fn format_path_long_src_path_truncated_relative() {
    let mut fmt = Formatter::new();
    // Force truncation even after making it relative to src/
    fmt.path_width = 24;

    let formatted = fmt.format_path(
        "/home/user/project/src/module/submodule/deeply_nested_file_name.rs",
        42,
    );

    // Should be made relative to src/ and then truncated
    assert!(
        !formatted.contains("/home/user/project/"),
        "expected path to be made relative to src/: {formatted}",
    );
    assert!(
        formatted.contains("deeply_nested_file_name.rs:42") || formatted.contains("file_name.rs:42"),
        "expected file name with line to be present in truncated output: {formatted}",
    );

    // Truncation marker and width bound
    assert!(
        formatted.contains('…') || formatted.starts_with("..."),
        "expected some form of ellipsis/truncation marker: {formatted}",
    );
    assert!(
        formatted.len() <= fmt.path_width,
        "formatted length {} should not exceed path_width {}",
        formatted.len(),
        fmt.path_width
    );
}

#[test]
fn format_path_windows_style_paths_normalized() {
    let mut fmt = Formatter::new();
    fmt.path_width = 40;

    let formatted = fmt.format_path(r"C:\project\src\module\file.rs", 7);

    // Windows separators should be normalized to '/'
    assert!(
        !formatted.contains('\\'),
        "expected backslashes to be normalized out: {formatted}",
    );
    assert!(
        formatted.contains("src/module/file.rs:7"),
        "expected normalized src path with line: {formatted}",
    );
}

#[test]
fn format_path_hits_file_with_line_branch() {
    let mut fmt = Formatter::new();

    // Choose a width such that `file_with_line.len() + 2 <= max_width`
    // so that branch is exercised without needing truncation.
    let file_with_line = "src/main.rs:1";
    let max_width = file_with_line.len() + 2;
    fmt.path_width = max_width as u16;

    let formatted = fmt.format_path("src/main.rs", 1);

    // We expect the full file-with-line to appear without truncation markers
    assert!(
        formatted.contains(file_with_line),
        "expected full file_with_line in output: {formatted}",
    );
    assert!(
        !formatted.contains('…') && !formatted.starts_with("..."),
        "expected no ellipsis for file_with_line branch: {formatted}",
    );
    assert!(
        formatted.len() <= fmt.path_width as usize,
        "formatted length {} should not exceed path_width {}",
        formatted.len(),
        fmt.path_width
    );
}

#[test]
fn format_path_leading_ellipsis_for_very_long_paths() {
    let mut fmt = Formatter::new();

    // Very narrow width to ensure we hit the final leading-ellipsis truncation branch
    fmt.path_width = 16;

    let formatted = fmt.format_path(
        "/very/long/prefix/that/is/not/src/and/should/be/trimmed/file.rs",
        99,
    );

    // Final branch should produce a leading ellipsis and keep just the tail
    assert!(
        formatted.starts_with('…') || formatted.starts_with("..."),
        "expected leading ellipsis for aggressively truncated path: {formatted}",
    );
    assert!(
        formatted.contains("file.rs:99"),
        "expected file name with line to be preserved in final-branch truncation: {formatted}",
    );
    assert!(
        formatted.len() <= fmt.path_width,
        "formatted length {} should not exceed path_width {}",
        formatted.len(),
        fmt.path_width
    );
}

```

These tests assume:
1. `Formatter::format_path(&self, path: &str, line: u32) -> String` exists and is accessible from this module.
2. `Formatter::path_width` is a public or otherwise settable numeric field (here treated as `u16`; adjust the cast or type if it differs).
3. The implementation normalizes `\` to `/`, makes absolute `src/`-containing paths relative to `src/`, and uses either the Unicode ellipsis `` or a `"..."` prefix for truncation.

If the actual behavior differs (e.g., different ellipsis marker, no right-alignment, different relativization rule), update the assertions to match the concrete formatting rules while keeping the test structure (short/no truncation, long `src/` + truncation, Windows normalization, `file_with_line.len() + 2 <= max_width` branch, and final leading-ellipsis branch) the same.
</issue_to_address>

### Comment 4
<location path="src/test.rs" line_range="18-27" />
<code_context>
+fn build_test_guard(level: Level, style: Style) -> (TracingGuard, TestSubscriber) {
</code_context>
<issue_to_address>
**suggestion (testing):** 为新的 `TracingGuard::set_filter` 以及带原始指令(raw directive)的 level/target API 添加测试覆盖。

现有测试覆盖了 `with_style``set_target_level``remove_target_level`,但新的 `TracingGuard::set_filter` API 和更严格的 `Level`/`Filter` 语义目前还没有测试。

请添加一些测试,例如:
- 通过 `build_test_guard(Level::Info, ...)` 构建一个 guard,调用 `set_filter(Filter::from_directive("info,my_crate=debug"))`,并断言 `guard.filter.as_directive()` 匹配该指令且调用返回 `Ok(())`- 调用 `set_level(Level::Warn)` 并断言 `guard.filter.as_directive()` 变为 `"warn"` 且没有错误。
- 在基于原始指令的 `set_filter` 之后调用 `set_target_level("demo", Level::Trace)`,并断言每个 target 的覆盖设置被正确反映且调用成功。

这些测试将有助于锁定新的公共 API 行为,而无需依赖 `EnvFilter` 的内部细节。

Suggested implementation:

```rust
fn build_test_guard(level: Level, style: Style) -> (TracingGuard, TestSubscriber) {
    let filter = Filter::new(level);
    let env_filter =
        tracing_subscriber::EnvFilter::try_new(filter.as_directive()).unwrap_or_default();
    let (env_layer, raw): (_, ReloadHandle) = tracing_subscriber::reload::Layer::new(env_filter);

    let layers: Vec<BoxedLayer> = vec![
        tracing_subscriber::fmt::Layer::default()
            .with_writer(io::sink)
            .boxed(),
    ];

    // NOTE: The rest of `build_test_guard` is assumed to remain unchanged.

}

#[cfg(test)]
mod tracing_guard_filter_tests {
    use super::*;

    #[test]
    fn set_filter_with_raw_directive_updates_guard() {
        // Start from an initial guard with a simple level-based filter.
        let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());

        // Apply a raw directive that includes a per-target override.
        let filter = Filter::from_directive("info,my_crate=debug");

        guard
            .set_filter(filter)
            .expect("set_filter with raw directive should succeed");

        // The guard's filter should now match the directive we provided.
        assert_eq!(
            guard.filter.as_directive(),
            "info,my_crate=debug",
            "guard.filter.as_directive() should reflect the raw directive applied via set_filter"
        );
    }

    #[test]
    fn set_level_after_filter_replaces_with_simple_level_directive() {
        let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());

        // First, set a more complex filter.
        guard
            .set_filter(Filter::from_directive("info,my_crate=debug"))
            .expect("set_filter should succeed");

        // Now replace it with a simple level using the level API.
        guard
            .set_level(Level::Warn)
            .expect("set_level(Level::Warn) should succeed");

        // After calling set_level, the directive should be a simple level directive.
        assert_eq!(
            guard.filter.as_directive(),
            "warn",
            "set_level should replace the filter with a simple level directive"
        );
    }

    #[test]
    fn set_target_level_after_raw_directive_adds_per_target_override() {
        let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());

        guard
            .set_filter(Filter::from_directive("info,my_crate=debug"))
            .expect("initial set_filter should succeed");

        // Add a per-target override after the raw directive has been applied.
        guard
            .set_target_level("demo", Level::Trace)
            .expect("set_target_level should succeed after set_filter");

        let directive = guard.filter.as_directive();

        // The overall directive should still contain the base level and original override.
        assert!(
            directive.contains("info"),
            "directive should still include base level after set_target_level: {directive}"
        );
        assert!(
            directive.contains("my_crate=debug"),
            "directive should retain existing per-target override after set_target_level: {directive}"
        );
        assert!(
            directive.contains("demo=trace"),
            "directive should include the new per-target override added via set_target_level: {directive}"
        );
    }

```

The replacement above assumes:
1. The snippet you shared is the full body of `build_test_guard`. If there is additional code in that function, reinsert it between the `let layers: Vec<BoxedLayer> = vec![ ... ];` line and the closing `}` so the helper continues to build and return a valid `(TracingGuard, TestSubscriber)`.
2. `Style` implements `Default`. If it does not, replace `Style::default()` in the tests with whatever style constructor you use elsewhere in your test suite (for example, `Style::Pretty` or a crate-specific default).
3. `Level` uses `CamelCase` variants (`Level::Info`, `Level::Warn`, `Level::Trace`) as implied by your review comment. If instead you re-export `tracing::Level`, adjust the variants to `Level::INFO`, `Level::WARN`, and `Level::TRACE`.
4. `Filter::from_directive(&str)` and `guard.filter` are available as shown in your comment. If `filter` is not public, replace direct field access with the appropriate accessor (for example, `guard.filter().as_directive()`).
</issue_to_address>

### Comment 5
<location path="src/color/style.rs" line_range="10" />
<code_context>
-    let r = rgb.0;
-    let g = rgb.1;
-    let b = rgb.2;
+#[derive(Clone, Copy, Debug)]
+pub struct Styled {
+    rgb: Rgb,
</code_context>
<issue_to_address>
**issue (complexity):** 建议在 `Styled` 之上重新引入简单的公共辅助函数,并把 ANSI16 表提升为常量,这样既能让 API 意图更清晰,也能让实现更精简。

你可以保留现在新的共享转换逻辑,同时恢复更简单、更具意图表达能力的 API,并降低部分内部复杂度。

### 1. 在 `Styled` 之上重新引入小的辅助函数

暴露和之前相同形状的辅助函数,把 `Styled` + `From<Styled>` 作为内部实现细节。这可以保持调用点简单,避免泄露 `Styled` 这个抽象:

```rust
pub fn rgb_to_owo(rgb: (u8, u8, u8), depth: ColorDepth) -> OwoStyle {
    Styled::new(Rgb(rgb.0, rgb.1, rgb.2), depth).into()
}

pub fn rgb_to_owo_on(r: u8, g: u8, b: u8, depth: ColorDepth) -> OwoStyle {
    Styled::new(Rgb(r, g, b), depth).on().into()
}

/// Dims the foreground by dividing brightness by 4 to ensure high text legibility.
pub fn theme_fg_dimmed(rgb: (u8, u8, u8), depth: ColorDepth) -> OwoStyle {
    Styled::new(Rgb(rgb.0, rgb.1, rgb.2), depth).dimmed().into()
}
```

这样调用点就可以恢复成:

```rust
let style = rgb_to_owo(theme.text, self.color_depth);
// or
let style = rgb_to_owo_on(bg.0, bg.1, bg.2, self.color_depth);
// or
let style = theme_fg_dimmed(theme.text, self.color_depth);
```

同时仍然可以在 `From<Styled>` 内部复用更新后的映射逻辑。

如果不希望 `Styled` 成为公共类型,也可以将其设为 `pub(crate)`,并保留这些辅助函数作为公共 API。

### 2. 避免在每次调用时重建 ANSI16 颜色表

目前 `Ansi16` 分支在每次转换时都会分配一个 16 元素数组。你可以通过将其提升为常量来降低复杂度和开销:

```rust
const ANSI16_TABLE: [AnsiColors; 16] = [
    AnsiColors::Black,
    AnsiColors::Red,
    AnsiColors::Green,
    AnsiColors::Yellow,
    AnsiColors::Blue,
    AnsiColors::Magenta,
    AnsiColors::Cyan,
    AnsiColors::White,
    AnsiColors::BrightBlack,
    AnsiColors::BrightRed,
    AnsiColors::BrightGreen,
    AnsiColors::BrightYellow,
    AnsiColors::BrightBlue,
    AnsiColors::BrightMagenta,
    AnsiColors::BrightCyan,
    AnsiColors::BrightWhite,
];
```

然后:

```rust
ColorDepth::Ansi16 => {
    let idx = rgb_to_ansi(s.as_tuple().into(), Palette::default()) as usize;
    let ansi = ANSI16_TABLE
        .get(idx)
        .unwrap_or(&AnsiColors::White);

    if s.on {
        return style.on_color(ansi);
    }
    style.color(ansi)
}
```

这些改动可以保留所有新的行为(变暗处理、背景色、更新后的映射),同时让公共使用方式更简单、内部实现也更精简。
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会根据反馈改进以后的评审。
Original comment in English

Hey - I've found 5 issues, and left some high level feedback:

  • The new Formatter::format_path implementation is quite hard to follow (multiple allocations, reverse-then-split logic, etc.); consider extracting the truncation logic into a small, documented helper and simplifying the directory tail calculation so the intended behavior is clearer and easier to reason about.
  • In color::style::Styled::from, the ANSI16 mapping allocates a fresh 16-element array and calls Palette::default() on every conversion; you could reduce overhead by reusing a static palette and color table (or a small helper function) instead of rebuilding them per call.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `Formatter::format_path` implementation is quite hard to follow (multiple allocations, reverse-then-split logic, etc.); consider extracting the truncation logic into a small, documented helper and simplifying the directory tail calculation so the intended behavior is clearer and easier to reason about.
- In `color::style::Styled::from`, the ANSI16 mapping allocates a fresh 16-element array and calls `Palette::default()` on every conversion; you could reduce overhead by reusing a static palette and color table (or a small helper function) instead of rebuilding them per call.

## Individual Comments

### Comment 1
<location path="src/builder.rs" line_range="18-27" />
<code_context>
-    }
-}
+pub(crate) type BoxedLayer = Box<dyn tracing_subscriber::Layer<Registry> + Send + Sync>;
+pub(crate) type InnerSubscriber = Layered<Vec<BoxedLayer>, Registry>;
+pub(crate) type ReloadHandle =
+    tracing_subscriber::reload::Handle<tracing_subscriber::EnvFilter, InnerSubscriber>;
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `Vec<BoxedLayer>` as a `Layer` for `Registry` appears unsound and likely won’t compile due to orphan rules.

`InnerSubscriber` assumes `Vec<BoxedLayer>` implements `Layer<Registry>` so that `Registry::default().with(layers)` type-checks, but you can’t add `impl Layer<Registry> for Vec<BoxedLayer>` here due to orphan rules (both `Layer` and `Vec` are foreign). That makes it impossible for `Vec<BoxedLayer>` to satisfy the `Layer<S>` bound in this crate.

If this currently compiles, it likely means `InnerSubscriber` no longer matches the actual subscriber type, so `reload::Handle<EnvFilter, InnerSubscriber>` would be incorrect. Instead, consider a local newtype wrapper, e.g. `struct Layers(Vec<BoxedLayer>); impl<S> Layer<S> for Layers where S: Subscriber + LookupSpan<'_> { … }`, and use `Layers` both in `.with(...)` and in the `InnerSubscriber` alias.
</issue_to_address>

### Comment 2
<location path="src/fmt/mod.rs" line_range="135-144" />
<code_context>
+    pub(crate) fn format_path(&self, file: &str, line: u32) -> String {
</code_context>
<issue_to_address>
**issue (bug_risk):** The new path-compaction logic is hard to reason about and likely not producing the intended directory prefix.

The new implementation reverses `[..last_slash]` and then splits that reversed string on `'/'`. Because the directory separators are reversed along with the characters, the resulting segments don’t map cleanly to original path components and are unlikely to represent “the trailing directory name that still fits.” It also creates several intermediate `String`s and does extra `char`-level work on a hot path.

A simpler, safer approach is to keep the old behaviour conceptually: operate on the forward path bytes, trim from the left until `len <= max_width`, and prepend `` if trimming occurred. If you want to preserve filename + some directory context:
1. Build `file_with_line = format!("{filename}:{line}")`.
2. If it fits, right-align as you do now.
3. If not, compute how much of the *end* of `path_str` can be kept so that `format!("{tail}:{line}")` fits, or fall back to `` + suffix logic—without reversing the string.

This keeps the logic easier to reason about and avoids mangled directory names.
</issue_to_address>

### Comment 3
<location path="src/fmt/test.rs" line_range="9" />
<code_context>
 fn formatter_defaults() {
     let fmt = Formatter::new();
     assert_eq!(fmt.time_format, "%H:%M:%S");
-    assert_eq!(fmt.path_width, BUILD_PATH_WIDTH);
+    assert_eq!(fmt.path_width, DEFAULT_PATH_WIDTH);
     assert!(fmt.show_path);
     assert!(fmt.show_spans);
</code_context>
<issue_to_address>
**suggestion (testing):** Add focused tests for the new `Formatter::format_path` truncation and normalization logic.

The new `Formatter::format_path` logic (string-based truncation, `src/` vs `src\` handling, ellipsis, and directory-tail heuristics) isn’t covered beyond this default width check.

Please add focused tests that:
- Cover short paths (no truncation, right-aligned),
- Cover long `src/` paths that become relative and truncated,
- Verify Windows-style paths are normalized to `/`,
- Hit the `file_with_line.len() + 2 <= max_width` branch,
- Hit the final leading-ellipsis branch.

You can make these deterministic by setting `fmt.path_width` to a small value and asserting exact formatted strings.

Suggested implementation:

```rust
fn formatter_defaults() {
    let fmt = Formatter::new();
    assert_eq!(fmt.time_format, "%H:%M:%S");
    assert_eq!(fmt.path_width, DEFAULT_PATH_WIDTH);
    assert!(fmt.show_path);
    assert!(fmt.show_spans);
}

#[test]
fn format_path_short_no_truncation_right_aligned() {
    let mut fmt = Formatter::new();
    // Wide enough that no truncation happens
    fmt.path_width = 40;

    let formatted = fmt.format_path("src/lib.rs", 10);

    // No truncation markers
    assert!(!formatted.contains('…'), "expected no ellipsis: {formatted}");
    assert!(!formatted.starts_with("..."), "expected no ascii ellipsis: {formatted}");

    // Should end with the full file-with-line tail and be right-aligned within the field
    assert!(formatted.ends_with("src/lib.rs:10"), "expected to end with full path: {formatted}");
    assert!(
        formatted.len() <= fmt.path_width,
        "formatted length {} should not exceed path_width {}",
        formatted.len(),
        fmt.path_width
    );
}

#[test]
fn format_path_long_src_path_truncated_relative() {
    let mut fmt = Formatter::new();
    // Force truncation even after making it relative to src/
    fmt.path_width = 24;

    let formatted = fmt.format_path(
        "/home/user/project/src/module/submodule/deeply_nested_file_name.rs",
        42,
    );

    // Should be made relative to src/ and then truncated
    assert!(
        !formatted.contains("/home/user/project/"),
        "expected path to be made relative to src/: {formatted}",
    );
    assert!(
        formatted.contains("deeply_nested_file_name.rs:42") || formatted.contains("file_name.rs:42"),
        "expected file name with line to be present in truncated output: {formatted}",
    );

    // Truncation marker and width bound
    assert!(
        formatted.contains('…') || formatted.starts_with("..."),
        "expected some form of ellipsis/truncation marker: {formatted}",
    );
    assert!(
        formatted.len() <= fmt.path_width,
        "formatted length {} should not exceed path_width {}",
        formatted.len(),
        fmt.path_width
    );
}

#[test]
fn format_path_windows_style_paths_normalized() {
    let mut fmt = Formatter::new();
    fmt.path_width = 40;

    let formatted = fmt.format_path(r"C:\project\src\module\file.rs", 7);

    // Windows separators should be normalized to '/'
    assert!(
        !formatted.contains('\\'),
        "expected backslashes to be normalized out: {formatted}",
    );
    assert!(
        formatted.contains("src/module/file.rs:7"),
        "expected normalized src path with line: {formatted}",
    );
}

#[test]
fn format_path_hits_file_with_line_branch() {
    let mut fmt = Formatter::new();

    // Choose a width such that `file_with_line.len() + 2 <= max_width`
    // so that branch is exercised without needing truncation.
    let file_with_line = "src/main.rs:1";
    let max_width = file_with_line.len() + 2;
    fmt.path_width = max_width as u16;

    let formatted = fmt.format_path("src/main.rs", 1);

    // We expect the full file-with-line to appear without truncation markers
    assert!(
        formatted.contains(file_with_line),
        "expected full file_with_line in output: {formatted}",
    );
    assert!(
        !formatted.contains('…') && !formatted.starts_with("..."),
        "expected no ellipsis for file_with_line branch: {formatted}",
    );
    assert!(
        formatted.len() <= fmt.path_width as usize,
        "formatted length {} should not exceed path_width {}",
        formatted.len(),
        fmt.path_width
    );
}

#[test]
fn format_path_leading_ellipsis_for_very_long_paths() {
    let mut fmt = Formatter::new();

    // Very narrow width to ensure we hit the final leading-ellipsis truncation branch
    fmt.path_width = 16;

    let formatted = fmt.format_path(
        "/very/long/prefix/that/is/not/src/and/should/be/trimmed/file.rs",
        99,
    );

    // Final branch should produce a leading ellipsis and keep just the tail
    assert!(
        formatted.starts_with('…') || formatted.starts_with("..."),
        "expected leading ellipsis for aggressively truncated path: {formatted}",
    );
    assert!(
        formatted.contains("file.rs:99"),
        "expected file name with line to be preserved in final-branch truncation: {formatted}",
    );
    assert!(
        formatted.len() <= fmt.path_width,
        "formatted length {} should not exceed path_width {}",
        formatted.len(),
        fmt.path_width
    );
}

```

These tests assume:
1. `Formatter::format_path(&self, path: &str, line: u32) -> String` exists and is accessible from this module.
2. `Formatter::path_width` is a public or otherwise settable numeric field (here treated as `u16`; adjust the cast or type if it differs).
3. The implementation normalizes `\` to `/`, makes absolute `src/`-containing paths relative to `src/`, and uses either the Unicode ellipsis `` or a `"..."` prefix for truncation.

If the actual behavior differs (e.g., different ellipsis marker, no right-alignment, different relativization rule), update the assertions to match the concrete formatting rules while keeping the test structure (short/no truncation, long `src/` + truncation, Windows normalization, `file_with_line.len() + 2 <= max_width` branch, and final leading-ellipsis branch) the same.
</issue_to_address>

### Comment 4
<location path="src/test.rs" line_range="18-27" />
<code_context>
+fn build_test_guard(level: Level, style: Style) -> (TracingGuard, TestSubscriber) {
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests that exercise the new `TracingGuard::set_filter` and level/target APIs with raw directives.

The existing tests cover `with_style`, `set_target_level`, and `remove_target_level`, but the new `TracingGuard::set_filter` API and tighter `Level`/`Filter` semantics are untested.

Please add tests that, for example:
- Build a guard via `build_test_guard(Level::Info, ...)`, call `set_filter(Filter::from_directive("info,my_crate=debug"))`, and assert `guard.filter.as_directive()` matches and the call returns `Ok(())`.
- Call `set_level(Level::Warn)` and assert `guard.filter.as_directive()` becomes `"warn"` without error.
- Call `set_target_level("demo", Level::Trace)` after a `set_filter` based on a raw directive, asserting per-target overrides are correctly reflected and succeed.

These will help lock in the new public API behavior without depending on `EnvFilter` internals.

Suggested implementation:

```rust
fn build_test_guard(level: Level, style: Style) -> (TracingGuard, TestSubscriber) {
    let filter = Filter::new(level);
    let env_filter =
        tracing_subscriber::EnvFilter::try_new(filter.as_directive()).unwrap_or_default();
    let (env_layer, raw): (_, ReloadHandle) = tracing_subscriber::reload::Layer::new(env_filter);

    let layers: Vec<BoxedLayer> = vec![
        tracing_subscriber::fmt::Layer::default()
            .with_writer(io::sink)
            .boxed(),
    ];

    // NOTE: The rest of `build_test_guard` is assumed to remain unchanged.

}

#[cfg(test)]
mod tracing_guard_filter_tests {
    use super::*;

    #[test]
    fn set_filter_with_raw_directive_updates_guard() {
        // Start from an initial guard with a simple level-based filter.
        let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());

        // Apply a raw directive that includes a per-target override.
        let filter = Filter::from_directive("info,my_crate=debug");

        guard
            .set_filter(filter)
            .expect("set_filter with raw directive should succeed");

        // The guard's filter should now match the directive we provided.
        assert_eq!(
            guard.filter.as_directive(),
            "info,my_crate=debug",
            "guard.filter.as_directive() should reflect the raw directive applied via set_filter"
        );
    }

    #[test]
    fn set_level_after_filter_replaces_with_simple_level_directive() {
        let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());

        // First, set a more complex filter.
        guard
            .set_filter(Filter::from_directive("info,my_crate=debug"))
            .expect("set_filter should succeed");

        // Now replace it with a simple level using the level API.
        guard
            .set_level(Level::Warn)
            .expect("set_level(Level::Warn) should succeed");

        // After calling set_level, the directive should be a simple level directive.
        assert_eq!(
            guard.filter.as_directive(),
            "warn",
            "set_level should replace the filter with a simple level directive"
        );
    }

    #[test]
    fn set_target_level_after_raw_directive_adds_per_target_override() {
        let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());

        guard
            .set_filter(Filter::from_directive("info,my_crate=debug"))
            .expect("initial set_filter should succeed");

        // Add a per-target override after the raw directive has been applied.
        guard
            .set_target_level("demo", Level::Trace)
            .expect("set_target_level should succeed after set_filter");

        let directive = guard.filter.as_directive();

        // The overall directive should still contain the base level and original override.
        assert!(
            directive.contains("info"),
            "directive should still include base level after set_target_level: {directive}"
        );
        assert!(
            directive.contains("my_crate=debug"),
            "directive should retain existing per-target override after set_target_level: {directive}"
        );
        assert!(
            directive.contains("demo=trace"),
            "directive should include the new per-target override added via set_target_level: {directive}"
        );
    }

```

The replacement above assumes:
1. The snippet you shared is the full body of `build_test_guard`. If there is additional code in that function, reinsert it between the `let layers: Vec<BoxedLayer> = vec![ ... ];` line and the closing `}` so the helper continues to build and return a valid `(TracingGuard, TestSubscriber)`.
2. `Style` implements `Default`. If it does not, replace `Style::default()` in the tests with whatever style constructor you use elsewhere in your test suite (for example, `Style::Pretty` or a crate-specific default).
3. `Level` uses `CamelCase` variants (`Level::Info`, `Level::Warn`, `Level::Trace`) as implied by your review comment. If instead you re-export `tracing::Level`, adjust the variants to `Level::INFO`, `Level::WARN`, and `Level::TRACE`.
4. `Filter::from_directive(&str)` and `guard.filter` are available as shown in your comment. If `filter` is not public, replace direct field access with the appropriate accessor (for example, `guard.filter().as_directive()`).
</issue_to_address>

### Comment 5
<location path="src/color/style.rs" line_range="10" />
<code_context>
-    let r = rgb.0;
-    let g = rgb.1;
-    let b = rgb.2;
+#[derive(Clone, Copy, Debug)]
+pub struct Styled {
+    rgb: Rgb,
</code_context>
<issue_to_address>
**issue (complexity):** Consider reintroducing simple public helper functions over `Styled` and hoisting the ANSI16 table into a constant to keep the API intention‑revealing and the implementation leaner.

You can keep the new shared conversion logic while restoring the simpler, intention‑revealing API and reducing some internal complexity.

### 1. Reintroduce small helper functions on top of `Styled`

Expose the same shape of helpers as before and keep `Styled` + `From<Styled>` as an internal detail. This keeps call sites simple and avoids leaking the `Styled` abstraction:

```rust
pub fn rgb_to_owo(rgb: (u8, u8, u8), depth: ColorDepth) -> OwoStyle {
    Styled::new(Rgb(rgb.0, rgb.1, rgb.2), depth).into()
}

pub fn rgb_to_owo_on(r: u8, g: u8, b: u8, depth: ColorDepth) -> OwoStyle {
    Styled::new(Rgb(r, g, b), depth).on().into()
}

/// Dims the foreground by dividing brightness by 4 to ensure high text legibility.
pub fn theme_fg_dimmed(rgb: (u8, u8, u8), depth: ColorDepth) -> OwoStyle {
    Styled::new(Rgb(rgb.0, rgb.1, rgb.2), depth).dimmed().into()
}
```

Call sites then go back to:

```rust
let style = rgb_to_owo(theme.text, self.color_depth);
// or
let style = rgb_to_owo_on(bg.0, bg.1, bg.2, self.color_depth);
// or
let style = theme_fg_dimmed(theme.text, self.color_depth);
```

while still benefiting from the updated mapping logic inside `From<Styled>`.

If `Styled` is not meant to be public, you can also make it `pub(crate)` and keep these helpers as the public API.

### 2. Avoid rebuilding the ANSI16 color table on every call

The `Ansi16` arm currently allocates a 16‑element array on every conversion. You can reduce both complexity and overhead by hoisting it to a constant:

```rust
const ANSI16_TABLE: [AnsiColors; 16] = [
    AnsiColors::Black,
    AnsiColors::Red,
    AnsiColors::Green,
    AnsiColors::Yellow,
    AnsiColors::Blue,
    AnsiColors::Magenta,
    AnsiColors::Cyan,
    AnsiColors::White,
    AnsiColors::BrightBlack,
    AnsiColors::BrightRed,
    AnsiColors::BrightGreen,
    AnsiColors::BrightYellow,
    AnsiColors::BrightBlue,
    AnsiColors::BrightMagenta,
    AnsiColors::BrightCyan,
    AnsiColors::BrightWhite,
];
```

and then:

```rust
ColorDepth::Ansi16 => {
    let idx = rgb_to_ansi(s.as_tuple().into(), Palette::default()) as usize;
    let ansi = ANSI16_TABLE
        .get(idx)
        .unwrap_or(&AnsiColors::White);

    if s.on {
        return style.on_color(ansi);
    }
    style.color(ansi)
}
```

These changes preserve all new behavior (dimming, background, updated mapping) while making the public usage simpler and the internal implementation slightly leaner.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/builder.rs Outdated
Comment thread src/fmt/mod.rs
Comment thread src/fmt/test.rs
Comment thread src/test.rs
Comment thread src/color/style.rs
@Sn0wo2 Sn0wo2 marked this pull request as ready for review May 21, 2026 01:01
@Sn0wo2
Copy link
Copy Markdown
Owner Author

Sn0wo2 commented May 21, 2026

/gemini review

@Sn0wo2
Copy link
Copy Markdown
Owner Author

Sn0wo2 commented May 21, 2026

@sourcery-ai review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the library's configuration and initialization API, replacing the Level field with a more robust Filter system and simplifying the subscriber layer management. It also reduces external dependencies and refactors the path formatting logic. Review feedback identifies a bug in how source paths are stripped when multiple 'src/' directories exist and points out an alignment issue in the path formatter that breaks log column consistency. A cleanup of redundant variables in the initialization logic was also suggested.

Comment thread src/fmt/mod.rs
Comment thread src/fmt/mod.rs Outdated
Comment thread src/builder.rs Outdated
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题,并且有一些整体性的反馈:

  • 新的 Formatter::format_path 逻辑依赖通过在字符串中按 "src/" / "src\\" 分割并手动切片,这在非标准目录结构或存在多个 src 片段时会比较脆弱;建议改用基于 Path / Path::strip_prefix 的规范化方式,这样前缀剥离和分隔符规范化会更不易出错、也更具可移植性。
  • acta-build::walk_src_max_width 中,目前对 strip_prefix 的处理是对字符串化后的路径使用 find(strip_prefix);如果该前缀出现多次(或分隔符混用),可能会导致路径被错误地剥离——在可能的情况下使用 Path::strip_prefix,仅在必要时回退到字符串操作,可以让计算出来的宽度在不同平台上更加准确。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- The new `Formatter::format_path` logic relies on string splitting on `"src/"` / `"src\\"` and manual slicing, which can be brittle for non-standard layouts or multiple `src` segments; consider switching to `Path` / `Path::strip_prefix`-based normalization so prefix-stripping and separator normalization are less error-prone and more portable.
- In `acta-build::walk_src_max_width`, the `strip_prefix` handling currently uses `find(strip_prefix)` on a stringified path; if the prefix appears more than once (or with mixed separators), this may mis-strip paths—using `Path::strip_prefix` where possible and only falling back to string operations would make the computed width more accurate across platforms.

## Individual Comments

### Comment 1
<location path="src/fmt/test.rs" line_range="145-152" />
<code_context>
+}
+
+#[test]
+fn format_path_leading_ellipsis_for_very_long_path() {
+    let mut fmt = Formatter::new();
+    // Narrow enough that file_with_line alone exceeds width,
+    // forcing the leading-ellipsis fallback branch.
+    fmt.path_width = 11;
+    let result = fmt.format_path("/very/long/prefix/deep/nested/dirs/file.rs", 99);
+    assert!(result.starts_with('\u{2026}'), "expected leading ellipsis: {result}");
+    assert!(result.contains("file.rs:99"), "expected filename preserved: {result}");
+}
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test covering multi-byte (Unicode) paths to exercise the char-boundary-safe truncation logic in `format_path`

Given `format_path` now enforces UTF-8 char boundaries, please add a test with a multi-byte path segment (e.g. `"src/模块/文件.rs"`) and a narrow `path_width` that forces truncation. The test should verify the output remains valid UTF-8 (no split characters around the ellipsis) and still preserves the `file.rs:line` suffix. This helps catch off-by-one errors in the boundary-adjustment loop.
</issue_to_address>

Sourcery 对开源项目是免费的 —— 如果你觉得这些 Review 有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈来改进后续的代码审查。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • The new Formatter::format_path logic relies on string splitting on "src/" / "src\\" and manual slicing, which can be brittle for non-standard layouts or multiple src segments; consider switching to Path / Path::strip_prefix-based normalization so prefix-stripping and separator normalization are less error-prone and more portable.
  • In acta-build::walk_src_max_width, the strip_prefix handling currently uses find(strip_prefix) on a stringified path; if the prefix appears more than once (or with mixed separators), this may mis-strip paths—using Path::strip_prefix where possible and only falling back to string operations would make the computed width more accurate across platforms.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `Formatter::format_path` logic relies on string splitting on `"src/"` / `"src\\"` and manual slicing, which can be brittle for non-standard layouts or multiple `src` segments; consider switching to `Path` / `Path::strip_prefix`-based normalization so prefix-stripping and separator normalization are less error-prone and more portable.
- In `acta-build::walk_src_max_width`, the `strip_prefix` handling currently uses `find(strip_prefix)` on a stringified path; if the prefix appears more than once (or with mixed separators), this may mis-strip paths—using `Path::strip_prefix` where possible and only falling back to string operations would make the computed width more accurate across platforms.

## Individual Comments

### Comment 1
<location path="src/fmt/test.rs" line_range="145-152" />
<code_context>
+}
+
+#[test]
+fn format_path_leading_ellipsis_for_very_long_path() {
+    let mut fmt = Formatter::new();
+    // Narrow enough that file_with_line alone exceeds width,
+    // forcing the leading-ellipsis fallback branch.
+    fmt.path_width = 11;
+    let result = fmt.format_path("/very/long/prefix/deep/nested/dirs/file.rs", 99);
+    assert!(result.starts_with('\u{2026}'), "expected leading ellipsis: {result}");
+    assert!(result.contains("file.rs:99"), "expected filename preserved: {result}");
+}
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test covering multi-byte (Unicode) paths to exercise the char-boundary-safe truncation logic in `format_path`

Given `format_path` now enforces UTF-8 char boundaries, please add a test with a multi-byte path segment (e.g. `"src/模块/文件.rs"`) and a narrow `path_width` that forces truncation. The test should verify the output remains valid UTF-8 (no split characters around the ellipsis) and still preserves the `file.rs:line` suffix. This helps catch off-by-one errors in the boundary-adjustment loop.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/fmt/test.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/builder.rs (1)

362-379: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

失败的过滤器更新会把 TracingGuard 留在未提交状态。

Line 367 先修改了 self.filter,Line 377-378 才去构造并 reload EnvFilter。如果 target 生成了无效 directive,这个方法虽然返回 Err,但 self.filter 已经变成新值了,之后的 Debugremove_target_level 和后续更新都会基于一个并未真正生效的过滤器状态。先在临时 Filter 上验证成功后再回写会更稳妥。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/builder.rs` around lines 362 - 379, The current flow mutates self.filter
(in set_target_level and remove_target_level) before attempting to construct and
reload an EnvFilter in apply_current_filter, leaving self.filter changed even if
EnvFilter::try_new fails; instead, perform the mutation on a temporary Filter
copy, validate by calling
tracing_subscriber::EnvFilter::try_new(temp.as_directive()) (or call
apply_current_filter-like validation) and only when that succeeds assign the
validated temp back to self.filter and then update the raw guard (i.e., change
set_target_level and remove_target_level to operate on a cloned filter, validate
the directive, then commit the clone to self.filter and call the existing reload
logic).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build.rs`:
- Around line 7-9: The write failure to the build artifact is currently only
logged as a warning; change the branch handling the std::fs::write(&path,
width.to_string()) Err(e) case to fail the build immediately (e.g., panic! or
call std::process::exit with non-zero) so the build stops on write errors;
update the error handling around std::fs::write (referencing path, width, and
the Err(e) match arm) to include the error details in the failure message so the
build aborts with a clear diagnostic.

In `@crates/acta-build/src/lib.rs`:
- Around line 60-61: The current code uses display.find(strip_prefix) which
matches the first occurrence anywhere instead of only at the start; replace this
with prefix semantics by using display.strip_prefix(strip_prefix) (or
starts_with + slicing) so you only strip when strip_prefix is at the beginning.
Concretely, change the .find(strip_prefix).map_or(display.len(), |i| display[i +
strip_prefix.len()..].len()) logic to use
display.strip_prefix(strip_prefix).map_or(display.len(), |s| s.len()) so the
computed length is based on a true prefix removal of strip_prefix.

In `@README.md`:
- Around line 112-115: Add a single blank line between the table ending with
"`Format::Json(LayerConfig)`" and the following "## File logging" heading so the
table and heading are separated (this prevents MD058/MD022 warnings); simply
insert one empty line after the table row for Format::Json.

In `@src/builder.rs`:
- Around line 141-143: build_layer currently lets WriterTarget::File be routed
to std::io::sink() via build_layer_with while remaining pub, causing callers who
use build_layer directly to silently drop file logs; change the API so callers
can't silently lose logs — either make build_layer return Result<BoxedLayer,
Error> and propagate an error when WriterTarget::File would use sink, or split
file-target construction into a separate non-public helper and keep build_layer
public only for safe targets; update build_layer, build_layer_with, and any code
that constructs WriterTarget::File to use the new error-returning or separate
constructor and ensure no path returns a BoxedLayer that wraps std::io::sink for
file targets.
- Around line 270-272: Current code creates a single shared ArcSwap
(shared_style) and passes it into build_formatter for multiple non-file writers,
so each writer's call to with_style_config(writer.style) overwrites the same
ArcSwap and later writers' styles win. Fix by giving each non-file writer its
own ArcSwap initialized from that writer's style instead of reusing
shared_style: inside the loop that constructs writers (the code that checks for
file vs non-file writers and calls build_formatter(..., shared_handle)), create
a new Arc::new(arc_swap::ArcSwap::new(Arc::new(writer.style))) per writer and
pass that per-writer ArcSwap into build_formatter; remove or stop using the
single shared_style for non-file writers so styles don't clobber each other.
- Around line 277-279: The current implementation in init stores only a single
file_guard and log_path (variables file_guard and log_path) and overwrites them
each time a WriterTarget::File is encountered, losing previous guards and paths;
either reject multiple WriterTarget::File entries in init (validate targets and
return an error if more than one file target is present) or change file_guard
and log_path to collections (e.g., Vec or HashMap) and push/store a guard/path
for each WriterTarget::File encountered and adjust any uses of log_path() and
the guard drop semantics accordingly so all file writers are retained; update
code paths that assume a single file (references around handling of file_guard,
log_path, and log_path()) to iterate over the collection.

In `@src/color/style.rs`:
- Around line 76-78: The code casts the anstyle::AnsiColor returned by
rgb_to_ansi to usize and indexes ANSI16_TABLE (ColorDepth::Ansi16 branch), which
relies on the enum's discriminant values and can break if upstream changes;
replace that fragile indexing with an explicit exhaustive mapping from each
anstyle::AnsiColor variant to the corresponding table entry (or to the desired
AnsiColors value) — locate where rgb_to_ansi(...) is used and instead of "as
usize" + ANSI16_TABLE.get(...).copied().unwrap_or(AnsiColors::White) implement a
match on the rgb_to_ansi(...) result (covering all AnsiColor variants) that
returns the correct entry and remove the silent White fallback so behavior is
explicit.

In `@src/test.rs`:
- Around line 137-189: Update the tests to assert actual filtering behavior via
the returned subscriber instead of only inspecting guard.filter; for example, in
set_filter_with_raw_directive_updates_guard (and/or
set_target_level_after_raw_directive_adds_per_target_override) use the
subscriber returned from build_test_guard to construct Metadata for different
targets/levels and call the subscriber's enabled check to assert that the base
level ("info") blocks lower levels and that the per-target override
("my_crate=debug" and later "demo=trace") allows the expected levels; ensure you
call build_test_guard(...) to get the subscriber and add assertions that the
subscriber.enabled(...) (or the equivalent API) returns true/false for the
specific target+level combinations that prove the filter is actually enforced at
runtime.

In `@src/writer/test.rs`:
- Around line 1-5: Remove the module-level allow for clippy::unnecessary_map_or
and rewrite the specific Option/Iterator map_or usages in this file to avoid
suppressing future warnings: search for occurrences of Option::map_or or
.map_or(...) in tests within src/writer/test.rs and replace them with more
explicit checks (e.g., use is_some_and for boolean predicates or match/if let to
handle values) so the linter rule can remain enabled; keep the other clippy
allows intact and only remove the clippy::unnecessary_map_or attribute.

---

Outside diff comments:
In `@src/builder.rs`:
- Around line 362-379: The current flow mutates self.filter (in set_target_level
and remove_target_level) before attempting to construct and reload an EnvFilter
in apply_current_filter, leaving self.filter changed even if EnvFilter::try_new
fails; instead, perform the mutation on a temporary Filter copy, validate by
calling tracing_subscriber::EnvFilter::try_new(temp.as_directive()) (or call
apply_current_filter-like validation) and only when that succeeds assign the
validated temp back to self.filter and then update the raw guard (i.e., change
set_target_level and remove_target_level to operate on a cloned filter, validate
the directive, then commit the clone to self.filter and call the existing reload
logic).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ea57346-f0d9-4895-93a1-203e971ae813

📥 Commits

Reviewing files that changed from the base of the PR and between 25a4836 and e195c63.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • .gitignore
  • Cargo.toml
  • README.md
  • build.rs
  • crates/acta-build/Cargo.toml
  • crates/acta-build/src/lib.rs
  • crates/acta-debug/src/main.rs
  • src/builder.rs
  • src/color/mapping.rs
  • src/color/mod.rs
  • src/color/style.rs
  • src/color/tests.rs
  • src/config/depth.rs
  • src/config/mod.rs
  • src/config/test.rs
  • src/fmt/mod.rs
  • src/fmt/test.rs
  • src/lib.rs
  • src/prelude.rs
  • src/test.rs
  • src/writer/custom.rs
  • src/writer/mod.rs
  • src/writer/test.rs
💤 Files with no reviewable changes (3)
  • src/color/mapping.rs
  • Cargo.toml
  • src/config/depth.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build / Build
🧰 Additional context used
🪛 markdownlint-cli2 (0.22.1)
README.md

[warning] 114-114: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 115-115: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)

🔇 Additional comments (7)
src/writer/custom.rs (1)

24-27: LGTM!

Also applies to: 60-60

src/writer/mod.rs (1)

13-13: LGTM!

src/lib.rs (1)

14-18: LGTM!

Also applies to: 26-26

src/prelude.rs (1)

1-1: LGTM!

Also applies to: 4-7, 12-12

crates/acta-debug/src/main.rs (1)

5-7: 本文件改动主要是演示代码与新 API 对齐,当前未发现需要在此处新增的问题评论。

Also applies to: 43-43, 270-276, 291-291, 320-322, 343-362

src/config/mod.rs (2)

570-575: 🏗️ Heavy lift

Request missing inputs to rewrite review comment: No <review_comment> content (and no verification results) was provided, so I can’t rewrite it. Paste the original review comment and any relevant command outputs/logs you want used.


425-428: ⚡ Quick win

Missing review comment context: Paste the original <review_comment>...</review_comment> (and any related diff/snippet it refers to) so I can rewrite it accurately.

Comment thread build.rs
Comment on lines 60 to +61
.find(strip_prefix)
.map(|i| &display[i + strip_prefix.len()..])
.unwrap_or(&display)
.len()
.map_or(display.len(), |i| display[i + strip_prefix.len()..].len())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

strip_prefix 计算应使用“前缀语义”,不要用 find

Line 60 目前会匹配到路径中首次出现的位置,而不是开头前缀;当上层目录也包含同名片段时会错误截断并高估/低估宽度。建议改为仅在字符串开头剥离。

建议修改
-            display
-                .find(strip_prefix)
-                .map_or(display.len(), |i| display[i + strip_prefix.len()..].len())
+            let normalized_prefix = strip_prefix.replace('\\', "/");
+            display
+                .strip_prefix(&normalized_prefix)
+                .unwrap_or(&display)
+                .len()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/acta-build/src/lib.rs` around lines 60 - 61, The current code uses
display.find(strip_prefix) which matches the first occurrence anywhere instead
of only at the start; replace this with prefix semantics by using
display.strip_prefix(strip_prefix) (or starts_with + slicing) so you only strip
when strip_prefix is at the beginning. Concretely, change the
.find(strip_prefix).map_or(display.len(), |i| display[i +
strip_prefix.len()..].len()) logic to use
display.strip_prefix(strip_prefix).map_or(display.len(), |s| s.len()) so the
computed length is based on a true prefix removal of strip_prefix.

Comment thread README.md
Comment on lines +112 to 115
| `Format::Compact(LayerConfig)` | Default themed formatter with optional path and span display. |
| `Format::Pretty(LayerConfig)` | `tracing-subscriber` pretty formatter with file and line metadata. |
| `Format::Json(LayerConfig)` | Flattened JSON events without ANSI colors. |
## File logging
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

补齐表格与标题之间的空行,避免 markdownlint 报警。

这里紧邻的表格和 ## File logging 标题缺少空行,当前会触发 MD058/MD022,可能影响文档检查流水线。

建议修复
 | `Format::Compact(LayerConfig)` | Default themed formatter with optional path and span display.      |
 | `Format::Pretty(LayerConfig)`  | `tracing-subscriber` pretty formatter with file and line metadata. |
 | `Format::Json(LayerConfig)`    | Flattened JSON events without ANSI colors.                         |
+
 ## File logging
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 114-114: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 115-115: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 112 - 115, Add a single blank line between the table
ending with "`Format::Json(LayerConfig)`" and the following "## File logging"
heading so the table and heading are separated (this prevents MD058/MD022
warnings); simply insert one empty line after the table row for Format::Json.

Comment thread src/builder.rs
Comment on lines +141 to +143
pub fn build_layer(writer: &Writer) -> BoxedLayer {
build_layer_with(writer, None)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

不要让 build_layer 在文件目标上静默丢日志。

Line 191 把 WriterTarget::File 路由到了 std::io::sink(),但 build_layer 仍然是 pub。外部一旦直接用这个 API 构建文件 layer,会得到“初始化成功但永远不落盘”的结果。这里至少应返回错误,或把文件目标拆到单独的公开 API。

Also applies to: 167-192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/builder.rs` around lines 141 - 143, build_layer currently lets
WriterTarget::File be routed to std::io::sink() via build_layer_with while
remaining pub, causing callers who use build_layer directly to silently drop
file logs; change the API so callers can't silently lose logs — either make
build_layer return Result<BoxedLayer, Error> and propagate an error when
WriterTarget::File would use sink, or split file-target construction into a
separate non-public helper and keep build_layer public only for safe targets;
update build_layer, build_layer_with, and any code that constructs
WriterTarget::File to use the new error-returning or separate constructor and
ensure no path returns a BoxedLayer that wraps std::io::sink for file targets.

Comment thread src/builder.rs
Comment on lines +270 to +272
let shared_style = Arc::new(arc_swap::ArcSwap::new(Arc::new(
writers.first().map_or_else(Style::default, |w| w.style),
)));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

共享的 style handle 会让后面的 writer 覆盖前面的样式。

Line 270 这里只创建了一个 shared_style,随后在 Line 287 和 Line 298 传给所有非文件 writer。build_formatter(..., shared_handle) 里的 with_style_config(writer.style) 会写回同一个 ArcSwap,所以多个 compact writer 一旦配置了不同的 theme/icons/labels,最终都会按最后一个 writer 的样式输出。

Also applies to: 287-298

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/builder.rs` around lines 270 - 272, Current code creates a single shared
ArcSwap (shared_style) and passes it into build_formatter for multiple non-file
writers, so each writer's call to with_style_config(writer.style) overwrites the
same ArcSwap and later writers' styles win. Fix by giving each non-file writer
its own ArcSwap initialized from that writer's style instead of reusing
shared_style: inside the loop that constructs writers (the code that checks for
file vs non-file writers and calls build_formatter(..., shared_handle)), create
a new Arc::new(arc_swap::ArcSwap::new(Arc::new(writer.style))) per writer and
pass that per-writer ArcSwap into build_formatter; remove or stop using the
single shared_style for non-file writers so styles don't clobber each other.

Comment thread src/builder.rs
Comment on lines 277 to 279
let mut file_guard = None;
#[cfg(feature = "file")]
let mut log_path = None;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

多个文件 writer 这里只保留了最后一个 guard/path。

Line 277-279 只有一个 file_guardlog_path,而 Line 292-293 每遇到一个 WriterTarget::File 就覆盖一次。这样多文件输出时前一个 guard 会被立刻替换掉,log_path() 也只剩最后一个路径。若不打算支持多个文件 target,最好在 init 里显式拒绝;否则这里需要改成集合。

Also applies to: 289-295, 313-316

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/builder.rs` around lines 277 - 279, The current implementation in init
stores only a single file_guard and log_path (variables file_guard and log_path)
and overwrites them each time a WriterTarget::File is encountered, losing
previous guards and paths; either reject multiple WriterTarget::File entries in
init (validate targets and return an error if more than one file target is
present) or change file_guard and log_path to collections (e.g., Vec or HashMap)
and push/store a guard/path for each WriterTarget::File encountered and adjust
any uses of log_path() and the guard drop semantics accordingly so all file
writers are retained; update code paths that assume a single file (references
around handling of file_guard, log_path, and log_path()) to iterate over the
collection.

Comment thread src/color/style.rs
Comment on lines +76 to +78
ColorDepth::Ansi16 => {
let idx = rgb_to_ansi(s.as_tuple().into(), Palette::default()) as usize;
let ansi = ANSI16_TABLE.get(idx).copied().unwrap_or(AnsiColors::White);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

不要把 anstyle::AnsiColor 的判别值当成稳定索引。

rgb_to_ansi 返回的是 4-bit anstyle::AnsiColor,这里再用 as usize 去索引本地表,相当于把正确性绑定到了上游枚举的判别值顺序;一旦上游显式指定或调整判别值,就会悄悄落到 White 回退。这里直接对 AnsiColor 做穷举映射会更稳,也能去掉这个静默兜底。 (docs.rs)

♻️ 建议改法
-                let idx = rgb_to_ansi(s.as_tuple().into(), Palette::default()) as usize;
-                let ansi = ANSI16_TABLE.get(idx).copied().unwrap_or(AnsiColors::White);
+                let ansi = match rgb_to_ansi(s.as_tuple().into(), Palette::default()) {
+                    anstyle::AnsiColor::Black => AnsiColors::Black,
+                    anstyle::AnsiColor::Red => AnsiColors::Red,
+                    anstyle::AnsiColor::Green => AnsiColors::Green,
+                    anstyle::AnsiColor::Yellow => AnsiColors::Yellow,
+                    anstyle::AnsiColor::Blue => AnsiColors::Blue,
+                    anstyle::AnsiColor::Magenta => AnsiColors::Magenta,
+                    anstyle::AnsiColor::Cyan => AnsiColors::Cyan,
+                    anstyle::AnsiColor::White => AnsiColors::White,
+                    anstyle::AnsiColor::BrightBlack => AnsiColors::BrightBlack,
+                    anstyle::AnsiColor::BrightRed => AnsiColors::BrightRed,
+                    anstyle::AnsiColor::BrightGreen => AnsiColors::BrightGreen,
+                    anstyle::AnsiColor::BrightYellow => AnsiColors::BrightYellow,
+                    anstyle::AnsiColor::BrightBlue => AnsiColors::BrightBlue,
+                    anstyle::AnsiColor::BrightMagenta => AnsiColors::BrightMagenta,
+                    anstyle::AnsiColor::BrightCyan => AnsiColors::BrightCyan,
+                    anstyle::AnsiColor::BrightWhite => AnsiColors::BrightWhite,
+                };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ColorDepth::Ansi16 => {
let idx = rgb_to_ansi(s.as_tuple().into(), Palette::default()) as usize;
let ansi = ANSI16_TABLE.get(idx).copied().unwrap_or(AnsiColors::White);
ColorDepth::Ansi16 => {
let ansi = match rgb_to_ansi(s.as_tuple().into(), Palette::default()) {
anstyle::AnsiColor::Black => AnsiColors::Black,
anstyle::AnsiColor::Red => AnsiColors::Red,
anstyle::AnsiColor::Green => AnsiColors::Green,
anstyle::AnsiColor::Yellow => AnsiColors::Yellow,
anstyle::AnsiColor::Blue => AnsiColors::Blue,
anstyle::AnsiColor::Magenta => AnsiColors::Magenta,
anstyle::AnsiColor::Cyan => AnsiColors::Cyan,
anstyle::AnsiColor::White => AnsiColors::White,
anstyle::AnsiColor::BrightBlack => AnsiColors::BrightBlack,
anstyle::AnsiColor::BrightRed => AnsiColors::BrightRed,
anstyle::AnsiColor::BrightGreen => AnsiColors::BrightGreen,
anstyle::AnsiColor::BrightYellow => AnsiColors::BrightYellow,
anstyle::AnsiColor::BrightBlue => AnsiColors::BrightBlue,
anstyle::AnsiColor::BrightMagenta => AnsiColors::BrightMagenta,
anstyle::AnsiColor::BrightCyan => AnsiColors::BrightCyan,
anstyle::AnsiColor::BrightWhite => AnsiColors::BrightWhite,
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/color/style.rs` around lines 76 - 78, The code casts the
anstyle::AnsiColor returned by rgb_to_ansi to usize and indexes ANSI16_TABLE
(ColorDepth::Ansi16 branch), which relies on the enum's discriminant values and
can break if upstream changes; replace that fragile indexing with an explicit
exhaustive mapping from each anstyle::AnsiColor variant to the corresponding
table entry (or to the desired AnsiColors value) — locate where rgb_to_ansi(...)
is used and instead of "as usize" +
ANSI16_TABLE.get(...).copied().unwrap_or(AnsiColors::White) implement a match on
the rgb_to_ansi(...) result (covering all AnsiColor variants) that returns the
correct entry and remove the silent White fallback so behavior is explicit.

Comment thread src/test.rs
Comment on lines +137 to +189
#[test]
fn set_filter_with_raw_directive_updates_guard() {
let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());
let filter = Filter::from_directive("info,my_crate=debug");
guard
.set_filter(filter)
.expect("set_filter with raw directive should succeed");
assert_eq!(
guard.filter.as_directive(),
"info,my_crate=debug",
"guard.filter should reflect the raw directive applied via set_filter"
);
}

#[test]
fn set_level_after_filter_replaces_with_simple_level_directive() {
let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());
guard
.set_filter(Filter::from_directive("info,my_crate=debug"))
.expect("set_filter should succeed");
guard
.set_level(Level::Warn)
.expect("set_level(Level::Warn) should succeed");
assert_eq!(
guard.filter.as_directive(),
"warn",
"set_level should replace the filter with a simple level directive"
);
}

#[test]
fn set_target_level_after_raw_directive_adds_per_target_override() {
let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());
guard
.set_filter(Filter::from_directive("info,my_crate=debug"))
.expect("initial set_filter should succeed");
guard
.set_target_level("demo", Level::Trace)
.expect("set_target_level should succeed after set_filter");
let directive = guard.filter.as_directive();
assert!(
directive.contains("info"),
"directive should still include base level after set_target_level: {directive}"
);
assert!(
directive.contains("my_crate=debug"),
"directive should retain existing per-target override after set_target_level: {directive}"
);
assert!(
directive.contains("demo=trace"),
"directive should include the new per-target override: {directive}"
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

过滤器测试仅校验内部字段,建议补一条“实际过滤生效”的行为断言。

这几条新增测试都只检查 guard.filter 字符串,若 reload 管道失效但字段仍更新,测试仍会通过。建议至少在一条用例里通过 returned subscriber 包裹执行并断言不同 target/level 的启用状态。

示例(可加到 set_filter 用例)
 #[test]
 fn set_filter_with_raw_directive_updates_guard() {
-    let (mut guard, _subscriber) = build_test_guard(Level::Info, Style::default());
+    let (mut guard, subscriber) = build_test_guard(Level::Info, Style::default());
     let filter = Filter::from_directive("info,my_crate=debug");
-    guard
-        .set_filter(filter)
-        .expect("set_filter with raw directive should succeed");
+    tracing::subscriber::with_default(subscriber, || {
+        guard
+            .set_filter(filter)
+            .expect("set_filter with raw directive should succeed");
+        assert!(tracing::enabled!(target: "my_crate", tracing::Level::DEBUG));
+        assert!(!tracing::enabled!(target: "other_crate", tracing::Level::DEBUG));
+    });
     assert_eq!(
         guard.filter.as_directive(),
         "info,my_crate=debug",
         "guard.filter should reflect the raw directive applied via set_filter"
     );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test.rs` around lines 137 - 189, Update the tests to assert actual
filtering behavior via the returned subscriber instead of only inspecting
guard.filter; for example, in set_filter_with_raw_directive_updates_guard
(and/or set_target_level_after_raw_directive_adds_per_target_override) use the
subscriber returned from build_test_guard to construct Metadata for different
targets/levels and call the subscriber's enabled check to assert that the base
level ("info") blocks lower levels and that the per-target override
("my_crate=debug" and later "demo=trace") allows the expected levels; ensure you
call build_test_guard(...) to get the subscriber and add assertions that the
subscriber.enabled(...) (or the equivalent API) returns true/false for the
specific target+level combinations that prove the filter is actually enforced at
runtime.

Comment thread src/writer/test.rs
Comment on lines +1 to +5
#![allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::unnecessary_map_or
)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

收窄 unnecessary_map_or 的模块级放行范围

Line 1-5 现在对整个模块放行 clippy::unnecessary_map_or,会掩盖后续新增测试中的同类问题。建议移除该项并改写对应表达式(如改用 is_some_and)。

♻️ 建议修改
 #![allow(
     clippy::unwrap_used,
     clippy::indexing_slicing,
-    clippy::unnecessary_map_or
 )]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/writer/test.rs` around lines 1 - 5, Remove the module-level allow for
clippy::unnecessary_map_or and rewrite the specific Option/Iterator map_or
usages in this file to avoid suppressing future warnings: search for occurrences
of Option::map_or or .map_or(...) in tests within src/writer/test.rs and replace
them with more explicit checks (e.g., use is_some_and for boolean predicates or
match/if let to handle values) so the linter rule can remain enabled; keep the
other clippy allows intact and only remove the clippy::unnecessary_map_or
attribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant