# pi_logger
`pi_logger` 是一个 Rust 日志与可观测性库。当前项目同时保留旧的 `log4rs` HTTP/SLS appender 能力,并新增了基于 `tracing` 的 `observability` 统一接口。
新接口重点支持:
- 控制台日志;
- 本地 rolling 日志;
- 远端 OpenTelemetry logs 上传;
- OpenTelemetry traces 上传;
- OpenTelemetry metrics 上传;
- 默认等级 + override 覆盖规则;
- target/span 模糊匹配;
- event 字段过滤;
- 运行时动态修改过滤规则;
- 按 override `name` 动态启用/禁用规则;
- 本地/控制台与远端 logs 可使用不同过滤配置。
## 1. 模块概览
```text
src/
observability/ # 新 tracing/OTel 可观测性接口
ali_sls.rs # 旧 log4rs 阿里云 SLS appender
http.rs # 旧 log4rs HTTP appender
encode_json.rs # 旧 JsonEncoder
encode_json_str.rs # 旧 JsonStrEncoder
encode_sls.rs # 旧 SLSEncoder
opentelemetry.rs # 旧 OpenTelemetry 初始化接口
```
推荐新项目优先使用:
```rust
pi_logger::observability
```
旧接口仍保留,避免破坏现有调用。
## 2. 快速开始
### 2.1 Cargo.toml
```toml
[dependencies]
anyhow = "1"
pi_logger = { path = "../pi_logger" }
toml = "0.8"
tracing = "0.1"
```
如果使用当前仓库内的 demo,可参考:
```text
../pi_logger_demo
```
## 3. observability.toml 示例
```toml
[log]
format = "json"
[log.filter]
default_level = "info"
[[log.filter.overrides]]
name = "decoder_trace_by_id"
enabled = true
level = "trace"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "contains", pattern = "decoder" },
]
field_rules = [
{ field = "id", op = "eq", value = 100 },
{ field = "msg", op = "contains", value = "decode" },
{ field = "protocol", op = "in", value = ["ton", "eth"] },
{ field = "payload_len", op = "lte", value = 4096 },
{ field = "sampled", op = "eq", value = true },
]
[[log.filter.overrides]]
name = "noisy_db_warn"
enabled = true
level = "warn"
priority = 50
fuzzy_rules = [
{ kind = "target", match_type = "contains", pattern = "db" },
]
[log.console]
enabled = true
[log.local]
enabled = true
file_dir = "logs"
file_name = "app.log"
[log.remote]
enabled = true
endpoint = "http://127.0.0.1:4318"
[log.remote.filter]
default_level = "off"
[[log.remote.filter.overrides]]
name = "remote_decoder_id_100"
enabled = true
level = "info"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "contains", pattern = "decoder" },
]
field_rules = [
{ field = "id", op = "eq", value = 100 },
{ field = "test_kind", op = "contains", value = "remote" },
{ field = "module", op = "starts_with", value = "remote" },
{ field = "status", op = "in", value = ["ok", "retry"] },
{ field = "retry_count", op = "lte", value = 3 },
{ field = "sampled", op = "eq", value = true },
]
[log.dynamic]
enabled = true
[trace]
enabled = true
exporter = "otlp"
endpoint = "http://127.0.0.1:4318"
service_name = "my-rust-service"
service_version = "0.1.0"
[trace.filter]
default_level = "off"
[[trace.filter.overrides]]
name = "trace_test_on"
enabled = true
level = "info"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "contains", pattern = "trace_test" },
]
[metrics]
enabled = true
exporter = "otlp"
endpoint = "http://127.0.0.1:4318"
service_name = "my-rust-service"
service_version = "0.1.0"
```
## 4. 初始化
```rust
use anyhow::Result;
use pi_logger::observability::init_observability_from_optional_path;
use std::path::Path;
fn main() -> Result<()> {
let config_path = Some(Path::new("observability.toml"));
let (reload, guards) = init_observability_from_optional_path(config_path)?;
tracing::info!(target: "my_app", "application started");
guards.shutdown();
Ok(())
}
```
注意:
- `guards` 必须保活到进程退出前,否则 non-blocking 本地日志可能丢失。
- `guards.shutdown()` 会刷新并关闭 OpenTelemetry logs/traces/metrics provider。
- 不要重复调用 `init_observability` 初始化全局 subscriber。
配置路径行为:
- 路径存在:读取配置,并启动文件监听;
- 文件修改并稳定 10 秒后,自动 reload local、remote、trace 过滤规则;
- 文件解析或过滤规则校验失败时,旧规则继续生效;
- 路径为 `None` 或文件不存在:使用 `RUST_LOG` 创建仅控制台日志配置;
- `console/local/remote/trace/metrics enabled`、endpoint、格式和文件路径不支持热更新,需要重启。
- remote filter 在“复用 local”和“独立 `[log.remote.filter]`”之间切换需要重启。
`RUST_LOG` 支持转换为 ordered filter:
```text
RUST_LOG=info,my_app=debug,hyper=warn
default_level = info
my_app target prefix override = debug
hyper target prefix override = warn
```
外部系统可以在应用配置前调用检查接口:
```rust
use pi_logger::observability::validate_observability_config_path;
let config = validate_observability_config_path("observability.toml")?;
```
该接口只读取、解析和校验配置,不连接 Collector,也不会修改当前日志状态。
### 4.1 使用 pi_config 初始化
启用可选 feature:
```toml
[dependencies]
pi_logger = { version = "0.7", features = ["pi-config"] }
pi_config = "0.6"
```
应用总配置可以将日志配置放在 `observability` 子树中。`pi_logger` 使用
`Config::sub_value("observability")` 一次读取合并后的完整子树,因此 TOML、CLI、
ENV、DEFAULT 和 overlay 的优先级处理仍由 `pi_config` 负责。
```rust
use anyhow::Result;
use pi_config::ConfigBuilder;
use pi_logger::observability::init_observability_from_pi_config;
fn main() -> Result<()> {
let mut config = ConfigBuilder::new()
.add_toml_file("application.toml", None)?
.build()?;
let (reload, guards) =
init_observability_from_pi_config(&mut config, "observability")?;
tracing::info!(target: "my_app", "application started");
guards.shutdown();
Ok(())
}
```
`pi_config` 更新通知到达后,配置管理器将最新配置交给日志库:
```rust
use pi_config::Config;
use pi_logger::observability::{
reload_observability_filters_from_pi_config, ObservabilityReloadHandle,
};
fn on_config_changed(
config: &mut Config,
reload: &ObservabilityReloadHandle,
) -> anyhow::Result<()> {
reload_observability_filters_from_pi_config(reload, config, "observability")
}
```
更新接口会先读取并校验完整 `observability` 子树,再更新 local、remote、trace
过滤规则。校验失败时旧规则继续生效。`enabled`、endpoint、格式、文件路径和
metrics 配置仍然需要重启后生效。
## 5. 过滤规则模型
当前过滤模型是:
```text
default_level + overrides
```
判断逻辑:
```text
1. 一条 tracing event 到达。
2. 根据 target/span 匹配 overrides。
3. 如果没有 override 命中,使用 default_level 判断日志等级。
4. 如果有多条 override 命中,使用 priority 最大的一条。
5. 命中的 override 使用自己的 level 判断日志等级。
6. 如果该 override 配了 field_rules,再按 event 字段继续过滤。
```
这不是旧的全局串行 AND 模型。`field_rules` 只作用于所属 override,不会影响其他模块。
例如:
```toml
[log.filter]
default_level = "info"
[[log.filter.overrides]]
name = "decoder_trace_by_id"
enabled = true
level = "trace"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "contains", pattern = "decoder" },
]
field_rules = [
{ field = "id", op = "eq", value = 100 },
{ field = "msg", op = "contains", value = "decode" },
{ field = "protocol", op = "in", value = ["ton", "eth"] },
{ field = "payload_len", op = "lte", value = 4096 },
{ field = "sampled", op = "eq", value = true },
]
```
语义:
- 默认所有模块 `info` 及以上输出;
- target 包含 `decoder` 的日志命中 override;
- decoder 模块被临时提升到 `trace`;
- decoder 只有 event 字段同时满足 `id == 100`、`msg` 包含 `decode`、`protocol` 在指定列表内等条件时才通过这条 override;
- 其他模块不受这些 field rules 影响。
## 6. default_level
```toml
[log.filter]
default_level = "info"
```
支持:
```text
trace
debug
info
warn
error
off
```
含义:
```text
trace 输出 trace/debug/info/warn/error
debug 输出 debug/info/warn/error
info 输出 info/warn/error
warn 输出 warn/error
error 只输出 error
off 全部关闭
```
常见生产配置是:
```toml
default_level = "info"
```
排查问题时,通过 overrides 临时把部分模块调成 `debug` 或 `trace`。
## 7. overrides
override 是按 target/span 选择一批日志后,覆盖默认等级的规则。
```toml
[[log.filter.overrides]]
name = "rpc_debug"
enabled = true
level = "debug"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "glob", pattern = "my_app::rpc*" },
]
```
字段说明:
```text
name 规则名,必须唯一;用于错误诊断和运行时动态开关
enabled 是否启用该规则;缺省为 true
level 命中该规则后的日志等级
priority 多条规则同时命中时,数值大的优先
fuzzy_rules target/span 匹配规则;同一 override 内是 OR 语义
field_rules event 字段过滤规则;同一 override 内是 AND 语义
```
注意:
- `name` 不参与日志匹配,但必须唯一。
- `enabled = false` 时,规则保留在配置里,但不参与匹配。
- 没有 `fuzzy_rules` 的 override 会匹配所有日志,只有明确需要全局覆盖时才这样配置。
- reload 失败时,错误链会带上具体 override `name`,方便定位问题。
## 8. Fuzzy Rules
`fuzzy_rules` 用来快速匹配一批模块或调用链。
```toml
fuzzy_rules = [
{ kind = "target", match_type = "contains", pattern = "decoder" },
{ kind = "span", match_type = "contains", pattern = "decode_transaction" },
]
```
支持:
```text
kind:
target
span
match_type:
exact
prefix
contains
glob
regex
```
同一个 override 中,多条 fuzzy rule 是 OR 语义,命中任意一条即可。
建议:
- 优先使用 `exact`、`prefix`、`contains`。
- `glob` 和 `regex` 会预编译,但运行时仍比普通字符串匹配更重。
- 高流量日志场景下,尽量避免大量 regex 规则。
## 9. Field Rules
`field_rules` 基于 `tracing` event fields。
```toml
field_rules = [
{ field = "id", op = "eq", value = 100 },
{ field = "msg", op = "contains", value = "decode" },
{ field = "module", op = "starts_with", value = "remote" },
{ field = "status", op = "in", value = ["ok", "retry"] },
{ field = "retry_count", op = "lte", value = 3 },
{ field = "sampled", op = "eq", value = true },
{ field = "error_code", op = "not_exists" },
]
```
支持操作:
```text
eq
ne
contains
starts_with
ends_with
regex
in
exists
not_exists
gt
gte
lt
lte
```
同一个 override 中,多条 field rule 是 AND 语义。
`value` 支持 TOML 标量和数组,常用类型包括 number、string、bool、array。例如 `id = 100_u64` 可以用 `value = 100` 匹配,`status = "ok"` 可以用 `value = ["ok", "retry"]` 配合 `op = "in"` 匹配。
字段不存在时:
- `exists` 返回 false;
- `not_exists` 返回 true;
- 其他操作返回 false。
数值比较按数值语义处理,例如配置 `value = 100` 可以匹配业务日志里的 `id = 100_u64`。
`field_rules` 只读取 event 字段:
```rust
tracing::info!(id = 100_u64, "decode done");
```
`#[tracing::instrument(fields(id = id))]` 里的字段是 span 字段,不属于 event 字段过滤。
## 10. 本地与远端过滤
本地/控制台使用:
```toml
[log.filter]
default_level = "info"
```
远端 OpenTelemetry logs 可以使用独立过滤:
```toml
[log.remote]
enabled = true
endpoint = "http://127.0.0.1:4318"
[log.remote.filter]
default_level = "off"
[[log.remote.filter.overrides]]
name = "remote_decoder_id_100"
enabled = true
level = "info"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "contains", pattern = "decoder" },
]
field_rules = [
{ field = "id", op = "eq", value = 100 },
{ field = "test_kind", op = "contains", value = "remote" },
{ field = "module", op = "starts_with", value = "remote" },
{ field = "status", op = "in", value = ["ok", "retry"] },
{ field = "retry_count", op = "lte", value = 3 },
{ field = "sampled", op = "eq", value = true },
]
```
这样可以做到:
- 本地默认保留所有模块 `info` 及以上;
- 远端默认不上报 logs;
- 远端只临时上传指定模块、指定字段的日志。
如果不配置 `[log.remote.filter]`,远端 logs 会复用 `[log.filter]`。
## 11. 输出格式
```toml
[log]
format = "json"
```
支持四种格式:
```text
json 结构化 JSON,适合生产采集和机器解析
compact 单行紧凑文本,适合开发时控制台查看
full 完整文本格式,信息比 compact 更充分
pretty 多行美化文本,适合本地调试
```
`format` 影响 console 和 local file 输出;远端 OpenTelemetry logs 会转换为 OTel LogRecord,不受该字段影响。
## 12. 控制台日志
```toml
[log.console]
enabled = true
```
启用后,命中过滤规则的日志会输出到 stdout。实际格式由 `[log].format` 决定。
## 13. 本地 rolling 日志
```toml
[log.local]
enabled = true
file_dir = "logs"
file_name = "app.log"
```
输出文件示例:
```text
logs/app.log.2026-06-05
```
`format = "json"` 时,本地日志包含:
- target;
- file;
- line number;
- thread id;
- thread name;
- current span;
- span list。
## 14. 远端 OpenTelemetry logs
```toml
[log.remote]
enabled = true
endpoint = "http://127.0.0.1:4318"
```
实际上传地址会自动拼接为:
```text
http://127.0.0.1:4318/v1/logs
```
远端日志使用 `opentelemetry-appender-tracing` 将 `tracing` event 转换为 OpenTelemetry LogRecord。
## 15. OpenTelemetry traces
```toml
[trace]
enabled = true
exporter = "otlp"
endpoint = "http://127.0.0.1:4318"
service_name = "my-rust-service"
service_version = "0.1.0"
```
实际上传地址会自动拼接为:
```text
http://127.0.0.1:4318/v1/traces
```
trace layer 基于 `tracing-opentelemetry`,兼容 `#[tracing::instrument]`。
traces pipeline 与 logs pipeline 独立。关闭某个 target 的日志 event,不会阻止 `#[tracing::instrument]` 创建的 span 上传到 traces pipeline。
## 16. 业务埋点示例
```rust
#[tracing::instrument(
name = "decode_transaction",
level = "debug",
skip_all,
fields(module = "decoder", protocol = "ton", id = id)
)]
fn decode_transaction(id: u64, payload: &[u8]) -> anyhow::Result<()> {
tracing::debug!(
target: "my_app::decoder",
id = id,
msg = "start decode transaction",
payload_len = payload.len(),
"decode transaction"
);
tracing::info!(
target: "my_app::decoder",
id = id,
msg = "decode transaction finished",
"decode done"
);
Ok(())
}
```
说明:
- `target` 可用于 target fuzzy rule;
- `decode_transaction` span 名可用于 span fuzzy rule;
- `id`、`msg`、`payload_len` 是 event fields,可用于 `field_rules`;
- `#[tracing::instrument(fields(...))]` 中的字段是 span 字段,主要用于 trace/span 上下文。
## 17. 动态 reload
初始化后会返回 `ObservabilityReloadHandle`:
```rust
let (reload, guards) = init_observability(config)?;
```
reload 分为三个过滤作用域:
```rust
reload.local(); // console 和本地文件日志
reload.remote()?; // 远端 OTLP logs
reload.trace()?; // 远端 OTLP traces
```
metrics 暂不支持动态 reload。
### 17.1 修改默认等级
```rust
reload.local().reload_default_level("debug")?;
reload.remote()?.reload_default_level("info")?;
reload.trace()?.reload_default_level("trace")?;
```
旧的无作用域方法仍然保留,并代理到 local:
```rust
reload.reload_default_level("debug")?;
```
### 17.2 替换全部 overrides
```rust
use pi_logger::observability::{
FuzzyMatchType, FuzzyRuleConfig, FuzzyRuleKind, LogFilterOverrideConfig,
};
reload.trace()?.reload_overrides(vec![LogFilterOverrideConfig {
name: "decoder_trace".to_string(),
enabled: Some(true),
level: "trace".to_string(),
priority: 100,
fuzzy_rules: vec![FuzzyRuleConfig {
kind: FuzzyRuleKind::Target,
match_type: FuzzyMatchType::Contains,
pattern: "decoder".to_string(),
level: None,
}],
field_rules: Vec::new(),
}])?;
```
### 17.3 按 name 启用或禁用 override
```rust
reload.local().set_override_enabled("decoder_trace", false)?;
reload.remote()?.set_override_enabled("remote_decoder", true)?;
reload.trace()?.set_override_enabled("trace_test_on", true)?;
```
`name` 不存在会返回错误。更新失败时,旧规则继续生效。
### 17.4 替换完整 filter
```rust
reload.local().reload_filter(config.log.filter.clone())?;
reload.remote()?.reload_filter(config.log.remote.filter.clone().unwrap())?;
reload.trace()?.reload_filter(config.trace.filter.clone().unwrap())?;
```
reload 行为:
- 新规则会先解析、校验、预编译 regex;
- override `name` 必须唯一;
- 成功后才替换当前规则;
- 失败时旧规则继续生效;
- 错误链会尽量带上具体 override `name`。
- 配置文件监听使用 10 秒防抖,连续修改会重新计时;
- 文件监听自动 reload 不受手动 API 的 `log.dynamic.enabled` 限制;
- `log.dynamic.enabled = false` 只禁止外部代码主动调用 reload API。
- `reload_observability_filters` 和 `reload_observability_filters_from_pi_config`
用于配置文件监听或配置中心通知,只更新 local、remote、trace 过滤规则;
作用域说明:
- remote logs 未配置独立 `[log.remote.filter]` 时,会复用 local filter,因此 local 和 remote reload 指向同一过滤状态;
- trace 未配置 `[trace.filter]` 时默认使用 `default_level = "trace"`,保持全部 spans/events 上传;
- trace filter 与日志 filter 独立,开启日志 `trace` 等级不会自动开启 traces,反之亦然。
## 18. 多进程动态调整
`reload` 只影响当前进程内的 subscriber。多进程场景下,推荐使用共享配置文件或配置中心:
```text
配置文件/配置中心更新
-> 每个进程各自监听
-> 每个进程读取并校验新配置
-> 每个进程调用 reload_filter/reload_overrides
```
不要假设一个进程的 reload 会自动影响其他进程。
## 19. 验证方式
当前项目建议使用:
```bash
cargo +nightly-2025-03-20 check
cargo +nightly-2025-03-20 test observability
```
observability 过滤器基准测试参考 `tracing` / `tracing-subscriber` 官方 benchmark:
```bash
cargo +nightly-2025-03-20 bench --bench observability_filter
```
只编译 benchmark,不执行:
```bash
cargo +nightly-2025-03-20 bench --bench observability_filter --no-run
```
当前 benchmark 使用 no-op layer 隔离文件和网络 IO,主要覆盖:
- tracing no-op subscriber baseline;
- 默认等级通过/拒绝;
- `1 / 10 / 100` 条 override 的命中与未命中;
- 单字段和多字段过滤;
- 默认等级 reload;
- 按 name 动态开关 override;
- 重新加载 100 条 overrides。
默认工具链 `nightly-2026-06-01` 在当前依赖 `pi_share 0.5.2` 上可能失败,这是既有依赖与最新 nightly API 的兼容问题,不是 observability 代码导致。
## 20. observability 示例
当前 crate 内置 observability 示例:
```text
examples/observability_demo.rs
examples/observability.toml
```
运行:
```bash
PI_LOGGER_OBSERVABILITY_CONFIG=examples/observability.toml \
cargo +nightly-2025-03-20 run --example observability_demo
```
不传配置路径时使用 `RUST_LOG`,并只启用控制台日志:
```bash
RUST_LOG=info,pi_logger_example::trace_test=debug \
cargo +nightly-2025-03-20 run --example observability_demo
```
示例演示:
- 本地/控制台默认等级;
- 远端 logs 独立过滤;
- 多种 field_rules 字段过滤:数字、字符串、数组、布尔;
- `#[tracing::instrument]` 调用链上传 traces;
- 独立 `[trace.filter]` 控制应用跟踪上传;
- counter、histogram、up_down_counter 指标上传 metrics;
- logs pipeline、traces pipeline 与 metrics pipeline 独立。
## 21. 旧接口说明
旧接口仍然保留:
- `pi_logger::http::HttpAppender`
- `pi_logger::ali_sls::SLSAppender`
- `pi_logger::ali_sls::SLBatchConfig`
- `pi_logger::encode_json::JsonEncoder`
- `pi_logger::encode_json_str::JsonStrEncoder`
- `pi_logger::encode_sls::SLSEncoder`
- `pi_logger::opentelemetry::{init, is_init, reload_trace_level, create_baggage}`
这些接口主要服务于已有 `log4rs` 接入方式。新项目建议优先使用 `pi_logger::observability`。
## 22. 注意事项
- override `name` 必须唯一。
- `enabled = false` 的 override 不参与匹配。
- 多条 override 命中时,最高 `priority` 生效。
- 同一 override 内,`fuzzy_rules` 是 OR 语义。
- 同一 override 内,`field_rules` 是 AND 语义。
- regex/glob 规则会预编译,并有数量和长度限制。
- field_rules 会读取 event fields,只建议挂在需要临时收敛的 override 上。
- remote logs/traces 需要 OpenTelemetry Collector。
- 本地日志使用 non-blocking writer,必须持有 `ObservabilityGuards`。
- demo 中固定 `time = "=0.3.45"` 是为了兼容 `nightly-2025-03-20`。