1
0

feat: 运行时日志系统,Pino + pino-pretty + pino-roll,console/file 双输出,敏感信息 redaction

This commit is contained in:
2026-05-21 12:21:59 +08:00
parent 0d709c7681
commit 007d74934d
26 changed files with 1713 additions and 114 deletions

View File

@@ -632,3 +632,53 @@
#### Scenario: llm expect stream firstTokenMs 非法
- **WHEN** YAML 中 llm target 的 `expect.stream.firstTokenMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.stream.firstTokenMs 格式错误
### Requirement: 日志配置格式
系统 SHALL 支持可选的顶层 `logging` 配置,用于定义运行时日志等级、命令行日志等级、文件日志等级、文件路径和滚动策略。`logging` 未配置时 SHALL 使用内置默认值。系统 SHALL NOT 支持 `logging.console.enabled``logging.console.format``logging.file.enabled``logging.file.format``logging.file.rotation.enabled` 字段。
#### Scenario: 未配置 logging 使用默认值
- **WHEN** 配置文件未声明 `logging`
- **THEN** 系统 SHALL 使用 `logging.level=info``logging.console.level=info``logging.file.level=info``logging.file.path=<resolved dataDir>/logs/dial.log``logging.file.rotation.size=50MB``logging.file.rotation.frequency=daily``logging.file.rotation.maxFiles=14`
#### Scenario: console 和 file level 继承全局 level
- **WHEN** 配置声明 `logging.level: warn` 且未声明 `logging.console.level``logging.file.level`
- **THEN** 系统 SHALL 将 console 和 file 的日志等级均解析为 `warn`
#### Scenario: 显式配置文件日志路径
- **WHEN** 配置声明 `logging.file.path`
- **THEN** 系统 SHALL 使用该路径作为文件日志路径,而不是默认 `<resolved dataDir>/logs/dial.log`
#### Scenario: 相对日志路径
- **WHEN** `logging.file.path` 是相对路径
- **THEN** 系统 SHALL 基于配置文件所在目录解析为绝对路径
#### Scenario: 绝对日志路径
- **WHEN** `logging.file.path` 是绝对路径
- **THEN** 系统 SHALL 原样使用该绝对路径,并允许该路径位于 `dataDir` 之外
#### Scenario: 不支持日志开关和格式字段
- **WHEN** 配置声明 `logging.console.enabled``logging.console.format``logging.file.enabled``logging.file.format``logging.file.rotation.enabled`
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段
### Requirement: 日志配置校验
系统 SHALL 在启动期校验 `logging` 配置。日志等级 SHALL 只能是 `trace``debug``info``warn``error``fatal``rotation.size` SHALL 使用有效 size 格式且解析为正整数字节数。`rotation.frequency` SHALL 只能是 `hourly``daily``weekly``rotation.maxFiles` SHALL 是正整数。
#### Scenario: 非法日志等级
- **WHEN** 配置声明 `logging.level: verbose`
- **THEN** 系统 SHALL 以配置错误退出并提示日志等级非法
#### Scenario: 非法滚动大小
- **WHEN** 配置声明 `logging.file.rotation.size: "large"`
- **THEN** 系统 SHALL 以配置错误退出并提示 size 格式非法
#### Scenario: 非法滚动频率
- **WHEN** 配置声明 `logging.file.rotation.frequency: monthly`
- **THEN** 系统 SHALL 以配置错误退出并提示 frequency 非法
#### Scenario: 非法归档数量
- **WHEN** 配置声明 `logging.file.rotation.maxFiles: 0`
- **THEN** 系统 SHALL 以配置错误退出并提示 maxFiles 必须为正整数
#### Scenario: 非法日志路径
- **WHEN** 配置声明 `logging.file.path` 为空字符串或空白字符串
- **THEN** 系统 SHALL 以配置错误退出并提示日志路径非法

View File

@@ -0,0 +1,91 @@
## Purpose
定义运行时日志输出、日志等级、命令行输出、文件 JSONL 输出、滚动策略和敏感信息保护。
## Requirements
### Requirement: 运行时 logger 输出
系统 SHALL 在配置解析成功后初始化统一运行时 logger。logger SHALL 同时输出命令行 pretty 日志和文件 JSONL 日志。命令行输出、文件输出和文件滚动 SHALL 始终启用,不提供关闭开关。
#### Scenario: 默认初始化 logger
- **WHEN** 配置文件未声明 `logging`
- **THEN** 系统 SHALL 使用默认等级 `info` 初始化 console pretty 输出和 `<resolved dataDir>/logs/dial.log` 文件 JSONL 输出
#### Scenario: 模块 child logger
- **WHEN** bootstrap 创建 engine、server 或其他运行时模块
- **THEN** 系统 SHALL 为模块创建带 `component` 字段的 child logger
#### Scenario: 配置成功后的启动失败
- **WHEN** 配置解析成功后数据库、logger、engine 或 HTTP server 初始化失败
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零状态退出
### Requirement: 日志等级语义
系统 SHALL 支持 `trace``debug``info``warn``error``fatal` 六个日志等级。`logging.level` SHALL 作为全局默认等级,`logging.console.level``logging.file.level` SHALL 在省略时继承全局等级。
#### Scenario: 目的地等级继承
- **WHEN** 配置只声明 `logging.level: warn`
- **THEN** console 和 file 输出均 SHALL 使用 `warn` 作为最低输出等级
#### Scenario: 目的地等级覆盖
- **WHEN** 配置声明 `logging.level: info``logging.console.level: warn``logging.file.level: debug`
- **THEN** console SHALL 输出 `warn` 及以上日志file SHALL 输出 `debug` 及以上日志
#### Scenario: 默认不输出 debug 检查摘要
- **WHEN** 系统使用默认 `info` 日志等级执行拨测
- **THEN** 每次检查的 debug 摘要 SHALL NOT 输出到 console 或 file
### Requirement: 文件日志滚动
系统 SHALL 对文件日志启用滚动策略。滚动 SHALL 在 `logging.file.rotation.size``logging.file.rotation.frequency` 任一条件满足时触发。`logging.file.rotation.maxFiles` SHALL 表示最多保留的归档文件数量,不包含当前正在写入的日志文件。
#### Scenario: 按大小滚动
- **WHEN** 当前日志文件达到配置的 `rotation.size`
- **THEN** 系统 SHALL 滚动到新的日志文件继续写入
#### Scenario: 按频率滚动
- **WHEN** 当前时间达到配置的 `rotation.frequency` 周期边界
- **THEN** 系统 SHALL 滚动到新的日志文件继续写入
#### Scenario: 限制归档数量
- **WHEN** 归档日志文件数量超过 `rotation.maxFiles`
- **THEN** 系统 SHALL 删除最旧的归档日志文件并保留当前正在写入的文件
### Requirement: 日志事件内容边界
系统 SHALL 将运行日志作为运行时事件记录,而不是将每次拨测结果完整复制到日志文件。`info` SHALL 记录生命周期事件,`warn` SHALL 记录需要关注但进程可继续的异常和目标 DOWN 状态变化,`error` SHALL 记录内部异常,`debug` SHALL 记录每次检查摘要。
#### Scenario: 成功检查默认不产生日志噪音
- **WHEN** target 连续检查成功且日志等级为默认 `info`
- **THEN** 系统 SHALL NOT 为每次成功检查输出 info 日志
#### Scenario: 目标首次 DOWN
- **WHEN** target 没有历史状态且本次检查结果为 DOWN
- **THEN** 系统 SHALL 输出 `warn` 日志,包含 targetId、targetType、durationMs 和 failure 摘要
#### Scenario: 目标恢复 UP
- **WHEN** target 最近状态为 DOWN 且本次检查结果为 UP
- **THEN** 系统 SHALL 输出 `info` 日志,包含 targetId、targetType 和 durationMs
#### Scenario: checker rejected
- **WHEN** checker 执行抛出未捕获异常导致 Promise rejected
- **THEN** 系统 SHALL 输出 `error` 日志并写入 `matched: false` 的 check_result
### Requirement: 敏感信息保护
系统 SHALL 避免在日志中输出敏感配置和运行时数据。日志事件 SHALL 优先记录白名单字段,并通过日志库 redaction 对常见敏感字段进行兜底保护。
#### Scenario: HTTP 敏感 header 不输出
- **WHEN** target 配置包含 `Authorization``Cookie``Set-Cookie` 相关值
- **THEN** 系统 SHALL NOT 在 console 或 file 日志中输出这些原始值
#### Scenario: LLM 敏感字段不输出
- **WHEN** LLM target 配置包含 `key``authToken``prompt` 或 providerOptions
- **THEN** 系统 SHALL NOT 在 console 或 file 日志中输出这些原始值
#### Scenario: 命令环境变量不输出
- **WHEN** cmd target 配置包含 `env`
- **THEN** 系统 SHALL NOT 在 console 或 file 日志中输出完整环境变量表
### Requirement: 单可执行文件兼容
运行时日志能力 SHALL 在 Bun 单可执行文件构建产物中可用。生产构建后的 executable SHALL 不依赖目标机器安装 Bun、Node.js 或 node_modules 即可输出 console 日志和滚动文件日志。
#### Scenario: standalone executable 写入文件日志
- **WHEN** 开发者运行 `bun run build` 并启动生成的 `dist/dial-server`
- **THEN** executable SHALL 在配置的数据目录下创建并写入滚动文件日志

View File

@@ -5,23 +5,27 @@ TBD - 统一服务启动引导函数,封装开发和生产模式的完整启
## Requirements
### Requirement: 统一启动引导函数
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、初始化正式 logger、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。配置加载失败发生在正式 logger 初始化之前,系统 SHALL 使用 console fallback 输出启动失败信息。配置加载成功后的启动失败 SHALL 使用正式 logger 输出 `fatal` 后退出。
#### Scenario: 开发模式启动
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets,并初始化运行时 logger
#### Scenario: 生产模式启动(带静态资源)
- **WHEN** code-generated entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
- **THEN** 系统 SHALL 完成完整启动序列,将 staticAssets 传递给 startServer
- **THEN** 系统 SHALL 完成完整启动序列,将 staticAssets 传递给 startServer,并初始化运行时 logger
#### Scenario: 启动失败处理
- **WHEN** 启动过程中任何步骤抛出异常
- **THEN** 系统 SHALL 输出错误信息并以非零退出码退出进程
#### Scenario: 配置加载失败处理
- **WHEN** 配置文件读取、YAML 解析或配置校验失败
- **THEN** 系统 SHALL 通过 console fallback 输出错误信息并以非零退出码退出进程
#### Scenario: 配置加载后的启动失败处理
- **WHEN** logger、store、engine 或 HTTP server 初始化失败
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零退出码退出进程
#### Scenario: 优雅关机
- **WHEN** 进程收到 SIGINT 或 SIGTERM 信号
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop()store.close() 后退出
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop()store.close() 和 logger.flush() 后退出
### Requirement: BootstrapOptions 接口
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string``mode: RuntimeMode` 和可选的 `staticAssets?: StaticAssets`

View File

@@ -7,22 +7,22 @@
## Requirements
### Requirement: 测试不应产生无关 console 输出
测试运行时,由测试用例预期的容错行为(如 JSON 解析失败、checker rejected触发的 `console.warn` 输出 SHALL 在测试代码中被抑制,不污染测试报告。
测试运行时,由测试用例预期的容错行为(如 JSON 解析失败、checker rejected触发的 logger 输出 SHALL 在测试代码中被抑制,不污染测试报告。测试 SHALL 优先通过注入 no-op logger 或 memory logger 抑制预期日志,而不是直接覆盖全局 `console.warn`
#### Scenario: 容错测试抑制 console.warn
#### Scenario: 容错测试抑制 logger 输出
- **WHEN** 测试用例故意注入损坏数据或触发异常以验证系统容错行为
- **THEN** 测试 SHALL 在执行前临时覆盖 `console.warn` 为空函数,在 finally 块中恢复原始函数
- **THEN** 测试 SHALL 注入 no-op logger 或 memory logger并在断言完成后恢复默认测试上下文
#### Scenario: 非预期 console.warn 不被抑制
#### Scenario: 非预期 logger 输出不被抑制
- **WHEN** 测试用例并非专门测试容错行为
- **THEN** 测试 SHALL NOT 抑制 `console.warn`,确保意外 warn 可被观测
- **THEN** 测试 SHALL NOT 抑制 logger 输出,确保意外 warn 或 error 可被观测
### Requirement: 探针执行失败日志输出单行消息
ProbeEngine 在捕获 checker rejected 时,`console.warn` SHALL 输出单行错误消息文本MUST NOT 输出 Error 对象(会导致多行堆栈噪音)。
ProbeEngine 在捕获 checker rejected 时,logger SHALL 输出单行错误消息文本MUST NOT 输出 Error 对象(会导致多行堆栈噪音)。该日志 SHALL 使用 `error` 等级,并包含 targetId、targetType 和 `formatReason()` 提取的错误消息。
#### Scenario: checker rejected 输出单行日志
- **WHEN** checker 执行抛出未捕获异常Promise rejected
- **THEN** `console.warn` SHALL 输出格式为 `探针执行失败: <message>` 的单行文本,其中 message 使用 `formatReason()` 提取
- **THEN** logger SHALL 输出格式为 `探针执行失败: <message>` 的单行消息,其中 message 使用 `formatReason()` 提取
#### Scenario: formatReason 复用
- **WHEN** 构建失败日志消息和写入 CheckFailure