# pi_logger Observability 程序员使用指南
本文面向接入和维护 `pi_logger` 的 Rust 程序员,介绍新版
`pi_logger::observability` 的能力、与旧版日志接口的差异、过滤规则语法,以及日志、
指标和应用跟踪的常用写法。
完整过滤边界和动态 reload 行为参考
[Observability 过滤规则使用说明](observability_filter_rules.md)。
## 1. 新版本解决了什么问题
新版 observability 将普通日志、应用调用链和指标统一到一套初始化与生命周期中:
```text
tracing::event / log::Record -> console / local file / OTLP logs
tracing span -> OTLP traces
OpenTelemetry instruments -> OTLP metrics
```
主要优势:
- **统一初始化**:一次初始化 logs、traces 和 metrics,不重复注册全局 subscriber。
- **结构化日志**:`tracing::...!` 可以直接记录 `id`、`status`、`duration_ms` 等字段。
- **灵活过滤**:默认等级配合 target/span override 和 event field 规则。
- **输出独立控制**:console/local、remote logs 和 traces 可以使用不同过滤规则。
- **动态调整**:local、remote 和 trace 过滤规则支持运行时 reload。
- **安全更新**:新配置先解析、校验并编译;失败时继续使用旧规则。
- **标准日志兼容**:已有 `log::info!`、`log::warn!` 等调用可进入同一日志管道。
- **可观测性完整**:同时支持普通日志、完整调用链和指标。
- **OTLP 传输可选**:支持 OTLP HTTP Binary 和 gRPC。
- **同步应用可用 gRPC**:启用 gRPC 时,库内部创建并持有专用 Tokio runtime。
- **跨语言预过滤**:可以向 JS 层导出当前日志过滤配置,减少无效跨语言调用。
## 2. 新旧版本差异
| 主要技术栈 | `log4rs` appender、旧 HTTP/SLS 实现 | `tracing`、OpenTelemetry |
| 初始化 | 分散配置不同 appender | 一次初始化 logs/traces/metrics |
| 日志 API | 主要使用 `log::...!` | 推荐 `tracing::...!`,兼容 `log::...!` |
| 结构化字段 | 通常拼接到消息字符串 | event fields 原生记录 |
| 过滤 | 等级、模块配置为主 | 默认等级、target/span、字段规则、优先级 |
| 本地和远端过滤 | 通常与 appender 配置耦合 | local、remote、trace 作用域独立 |
| 应用跟踪 | 需要独立旧接口 | 使用 `#[tracing::instrument]` 构建调用链 |
| 指标 | 不属于日志初始化流程 | OpenTelemetry metrics 统一初始化 |
| 远端协议 | 旧 HTTP/SLS appender | 标准 OTLP HTTP Binary 或 gRPC |
| 动态更新 | 依赖旧配置机制 | 支持过滤规则 reload 和配置文件监听 |
| 关闭流程 | appender 各自处理 | `ObservabilityGuards::shutdown()` 统一刷新关闭 |
新项目应优先使用 `pi_logger::observability`。旧 appender 仍为历史项目保留,但不建议作为
新功能的基础。
### 2.1 `tracing::...!` 与 `log::...!`
推荐使用 `tracing::...!`:
```rust
tracing::info!(
target: "my_app::order",
order_id = 10001_u64,
status = "paid",
amount = 99.5_f64,
"order paid"
);
```
它同时提供等级、target、message 和结构化 event fields,字段可以用于远端检索和
`field_rules`。
已有代码可以继续使用标准日志门面:
```rust
log::info!(target: "my_app::legacy", "legacy module started");
```
`log::...!` 适合等级、target 和 message 过滤,但不支持 `tracing::...!` 的结构化业务
字段语法。需要按业务字段过滤时应使用 `tracing::...!`。
### 2.2 新版日志结构
新版普通日志由以下信息组成:
| level | `tracing::info!` 等宏 | 等级过滤和告警 |
| target | `target: "my_app::order"` | 标识稳定模块,用于路由和过滤 |
| message | 日志宏最后的描述文本 | 供人阅读的事件描述 |
| event fields | `order_id = 10001` 等字段 | 结构化检索和 `field_rules` |
| source metadata | Rust 调用位置 | 本地排障;具体输出取决于日志来源和格式 |
| span context | 当前 `#[tracing::instrument]` 调用链 | 关联调用链上下文 |
| resource | `service.name`、`service.version` | 标识远端 OTLP 数据所属服务 |
例如:
```rust
tracing::info!(
target: "my_app::order",
order_id = 10001_u64,
status = "paid",
"order paid"
);
```
使用本地 JSON 格式时,输出结构类似:
```json
{
"timestamp": "2026-06-15T10:00:00Z",
"level": "INFO",
"target": "my_app::order",
"fields": {
"message": "order paid",
"order_id": 10001,
"status": "paid"
},
"filename": "src/order.rs",
"line_number": 42,
"threadName": "main"
}
```
远端 logs 会转换为标准 OpenTelemetry LogRecord。业务 event fields 会作为 LogRecord
attributes 上报,最终在日志平台中的展示结构由 Collector exporter 和后端决定。
`pi_logger` 不保证远端平台中的动态业务字段成为最终 JSON 最外层字段。
建议将稳定模块名写入 target,将动态业务值写入 event fields。不要把订单号、用户 ID
等动态值拼接到 target 中。
## 3. 初始化与生命周期
通过可选配置路径初始化:
```rust
use anyhow::Result;
use pi_logger::observability::init_observability_from_optional_path;
use std::path::Path;
fn main() -> Result<()> {
let (reload, guards) =
init_observability_from_optional_path(Some(Path::new("observability.toml")))?;
tracing::info!(target: "my_app", "application started");
// reload 用于运行时读取或修改过滤规则。
let revision = reload.current_filter_revision();
tracing::debug!(revision, "current filter revision");
// 应用退出时刷新并关闭 logs、traces 和 metrics provider。
guards.shutdown();
Ok(())
}
```
必须注意:
- 一个进程只初始化一次全局 observability。
- 应用运行期间必须持续持有 `ObservabilityGuards`。
- 应用退出时调用 `guards.shutdown()`。
- 配置文件不存在时,`init_observability_from_optional_path` 使用 `RUST_LOG` 创建最小
console 配置。
- `enabled`、endpoint、protocol、格式和文件路径修改后需要重启。
## 4. 使用 pi_config 初始化
应用已经使用 `pi_config` 管理总配置时,可以直接从合并后的配置子树初始化
observability,不需要单独读取 `observability.toml`。
### 4.1 启用 feature
```toml
[dependencies]
pi_logger = { version = "0.8", features = ["pi-config"] }
pi_config = "0.6"
```
`pi-config` 是可选 feature。未启用时,`init_observability_from_pi_config` 等适配接口不会
参与编译。
### 4.2 配置子树
应用总配置将 observability 配置放在 `observability` 子树中:
```toml
[observability.log]
format = "json"
[observability.log.filter]
default_level = "info"
[observability.log.console]
enabled = true
[observability.log.local]
enabled = false
file_dir = "logs"
file_name = "app.log"
[observability.log.remote]
enabled = true
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"
[observability.log.dynamic]
enabled = true
[observability.trace]
enabled = false
exporter = "otlp"
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"
service_name = "my-service"
[observability.metrics]
enabled = false
exporter = "otlp"
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"
service_name = "my-service"
```
传给 pi_logger 的路径是 `"observability"`。路径指向的子树内部结构与独立
`observability.toml` 相同,仍然从 `log`、`trace` 和可选 `metrics` 开始。
### 4.3 从合并配置初始化
```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");
// 应用运行期间继续持有 reload 和 guards。
let _ = reload.current_filter_revision();
guards.shutdown();
Ok(())
}
```
`pi_logger` 使用 `Config::sub_value("observability")` 一次读取合并后的完整子树。因此
TOML、CLI、环境变量、DEFAULT 和 overlay 的合并优先级仍由 `pi_config` 负责。
只读取和校验配置、不初始化全局 observability:
```rust
use pi_logger::observability::observability_config_from_pi_config;
let observability =
observability_config_from_pi_config(&mut config, "observability")?;
```
### 4.4 接收配置更新通知
配置中心或文件更新通知到达后,上层配置管理器应先生成最新的合并 `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")
}
```
更新过程会:
1. 读取完整 `observability` 子树。
2. 解析并校验全部配置和过滤规则。
3. 校验成功后更新 local、remote 和 trace 过滤规则。
4. 校验失败时返回错误,并继续使用旧过滤规则。
支持通过 `pi_config` 通知热更新:
- `[observability.log.filter]`
- `[observability.log.remote.filter]`
- `[observability.trace.filter]`
需要重启后生效:
- console/local/remote/trace/metrics 的 `enabled`
- endpoint 和 protocol
- 日志格式和本地文件路径
- metrics 配置
- remote 在“复用 local filter”和“独立 remote filter”之间切换
`log.dynamic.enabled = false` 只禁止
`reload.local().reload_*`、`reload.remote()?.reload_*` 等手动作用域 API。
`reload_observability_filters_from_pi_config` 属于配置中心通知入口,不受该开关限制。
应用的配置更新处理代码应记录 reload 错误,但不要因为一次错误更新丢弃当前有效配置。
## 5. 最小完整配置
```toml
[log]
format = "json"
[log.filter]
default_level = "info"
[log.console]
enabled = true
[log.local]
enabled = false
file_dir = "logs"
file_name = "app.log"
[log.remote]
enabled = true
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"
[log.dynamic]
enabled = true
[trace]
enabled = true
exporter = "otlp"
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"
service_name = "my-service"
service_version = "1.0.0"
[trace.filter]
default_level = "info"
[metrics]
enabled = true
exporter = "otlp"
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"
service_name = "my-service"
service_version = "1.0.0"
```
协议说明:
| `http_binary` | 4318 | 自动追加 `/v1/logs`、`/v1/traces`、`/v1/metrics` |
| `grpc` | 4317 | endpoint 原样传给 tonic,不追加 HTTP 路径 |
## 6. 新版本过滤语法
每个过滤作用域使用相同模型:
```text
default_level + overrides
```
可配置作用域:
- `[log.filter]`:console 和 local 文件日志。
- `[log.remote.filter]`:远端 OTLP logs;未配置时复用 `[log.filter]`。
- `[trace.filter]`:远端 OTLP traces。
### 6.1 默认等级
```toml
[log.filter]
default_level = "info"
```
支持 `trace`、`debug`、`info`、`warn`、`error`、`off`。
`default_level = "info"` 表示没有命中 override 时,放行 info、warn 和 error。
### 6.2 Override 覆盖规则
排查 decoder 模块时临时开启 debug:
```toml
[[log.filter.overrides]]
name = "decoder_debug"
enabled = true
level = "debug"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "prefix", pattern = "my_app::decoder" },
]
```
字段含义:
| `name` | 规则唯一名称,用于诊断、reload 和动态开关 |
| `enabled` | 是否启用,缺省为 `true` |
| `level` | 命中规则后使用的最低等级 |
| `priority` | 多条规则命中时,数值更大的规则优先 |
| `fuzzy_rules` | 选择 target 或 span,同一 override 内为 OR |
| `field_rules` | 过滤 event fields,同一 override 内为 AND |
完整执行顺序:
```text
1. 按 priority 从高到低选择第一条 fuzzy_rules 命中的 override。
2. 没有命中 override 时使用 default_level。
3. 命中 override 时使用 override.level。
4. 等级通过后检查该 override 的全部 field_rules。
5. 字段不匹配时直接拒绝,不回退默认等级或低优先级规则。
```
### 6.3 Target 和 Span 匹配
```toml
fuzzy_rules = [
{ kind = "target", match_type = "prefix", pattern = "my_app::rpc" },
{ kind = "span", match_type = "exact", pattern = "handle_request" },
]
```
支持:
| match_type | 行为 |
| `prefix` | 前缀匹配 |
| `contains` | 包含子字符串 |
| `glob` | `*`、`?` 通配符 |
| `regex` | 正则表达式 |
高频规则建议优先使用 `exact` 和 `prefix`,它们可以走 target 索引路径。
### 6.4 字段过滤
只上传 `order_id = 10001` 且状态为失败的 order 日志:
```toml
[log.remote.filter]
default_level = "off"
[[log.remote.filter.overrides]]
name = "failed_order_10001"
enabled = true
level = "debug"
priority = 200
fuzzy_rules = [
{ kind = "target", match_type = "exact", pattern = "my_app::order" },
]
field_rules = [
{ field = "order_id", op = "eq", value = 10001 },
{ field = "status", op = "in", value = ["failed", "timeout"] },
]
```
对应日志必须将字段写在 event 上:
```rust
tracing::debug!(
target: "my_app::order",
order_id = 10001_u64,
status = "failed",
"order processing failed"
);
```
支持的字段操作符:
| 相等 | `eq`、`ne` |
| 字符串 | `contains`、`starts_with`、`ends_with`、`regex` |
| 集合 | `in` |
| 存在性 | `exists`、`not_exists` |
| 数值比较 | `gt`、`gte`、`lt`、`lte` |
示例:
```toml
field_rules = [
{ field = "id", op = "eq", value = 100 },
{ field = "status", op = "in", value = ["ok", "retry"] },
{ field = "retry_count", op = "lte", value = 3 },
{ field = "module", op = "starts_with", value = "remote" },
{ field = "sampled", op = "eq", value = true },
{ field = "request_id", op = "exists" },
]
```
### 6.5 常见过滤场景
仅临时开启模块 trace:
```toml
[[log.filter.overrides]]
name = "rpc_trace"
level = "trace"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "prefix", pattern = "my_app::rpc" },
]
```
关闭本地 BI 日志,但继续通过独立 remote 规则上传:
```toml
[[log.filter.overrides]]
name = "local_bi_off"
level = "off"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "exact", pattern = "pi_serv_lib::bi" },
]
[log.remote.filter]
default_level = "off"
[[log.remote.filter.overrides]]
name = "remote_bi"
level = "info"
priority = 100
fuzzy_rules = [
{ kind = "target", match_type = "exact", pattern = "pi_serv_lib::bi" },
]
```
关闭某类普通日志不会关闭 traces。日志过滤和 trace 过滤是两套独立规则。
## 7. 常用日志写法
### 7.1 基础结构化日志
```rust
tracing::info!(
target: "my_app::user",
user_id = 10001_u64,
action = "login",
success = true,
"user login"
);
```
建议:
- target 使用稳定的模块命名,例如 `my_app::order`。
- message 描述事件,业务值放入结构化字段。
- 数字和布尔值使用原始类型,不要预先格式化为字符串。
- 不要将敏感值直接写入日志。
### 7.2 错误日志
```rust
fn load_user(user_id: u64) -> anyhow::Result<()> {
let result = do_load_user(user_id);
if let Err(error) = &result {
tracing::error!(
target: "my_app::user",
user_id,
error = %error,
"failed to load user"
);
}
result
}
```
### 7.3 标准 `log` 兼容写法
```rust
log::info!(target: "my_app::legacy", "legacy component started");
log::warn!(target: "my_app::legacy", "legacy component is slow");
```
它们会进入 observability 日志管道,并可按原始 target 过滤。新代码需要结构化字段时应使用
`tracing`。
## 8. 常用指标写法
```rust
use opentelemetry::{global, KeyValue};
use std::time::Instant;
fn record_request() {
let meter = global::meter("my_app");
let requests = meter
.u64_counter("requests_total")
.with_description("Total handled requests")
.build();
let duration = meter
.f64_histogram("request_duration_ms")
.with_unit("ms")
.build();
let in_flight = meter
.i64_up_down_counter("requests_in_flight")
.build();
let attributes = [
KeyValue::new("module", "gateway"),
KeyValue::new("method", "login"),
KeyValue::new("status", "ok"),
];
in_flight.add(1, &attributes);
let started = Instant::now();
// 处理业务请求。
requests.add(1, &attributes);
duration.record(started.elapsed().as_secs_f64() * 1000.0, &attributes);
in_flight.add(-1, &attributes);
}
```
常用类型:
- Counter:只增不减的累计次数,例如请求数。
- Histogram:数值分布,例如耗时和响应大小。
- UpDownCounter:可增可减的当前值,例如处理中请求数。
metrics 不使用日志过滤规则,也不支持动态 reload。
## 9. 常用应用跟踪写法
应用跟踪不是一条日志,而是包含父子 span 的完整调用链。
```rust
use anyhow::Result;
#[tracing::instrument(
name = "handle_request",
target = "my_app::request",
level = "info",
skip_all,
fields(request_id = request_id, module = "gateway")
)]
fn handle_request(request_id: u64) -> Result<()> {
tracing::info!(request_id, "start handling request");
decode_request(request_id)?;
persist_request(request_id)?;
Ok(())
}
#[tracing::instrument(
name = "decode_request",
target = "my_app::request",
level = "info",
skip_all,
fields(request_id = request_id, module = "decoder")
)]
fn decode_request(request_id: u64) -> Result<()> {
tracing::info!(request_id, "request decoded");
Ok(())
}
#[tracing::instrument(
name = "persist_request",
target = "my_app::request",
level = "info",
skip_all,
fields(request_id = request_id, module = "storage")
)]
fn persist_request(request_id: u64) -> Result<()> {
tracing::info!(request_id, "request persisted");
Ok(())
}
```
调用一次 `handle_request` 会产生类似调用链:
```text
handle_request
-> decode_request
-> persist_request
```
必须区分:
- `#[tracing::instrument(fields(...))]` 中的字段属于 span fields,用于调用链上下文。
- `tracing::info!(id = ...)` 中的字段属于 event fields,可以被 `field_rules` 读取。
- 开启普通日志的 `trace` 等级不会自动开启 traces。
- 开启 `[trace.filter]` 不会自动输出相同 target 的普通日志。
## 10. 动态调整过滤规则
初始化返回的 reload handle 可以按作用域修改规则:
```rust
reload.local().reload_default_level("debug")?;
reload.remote()?.reload_default_level("info")?;
reload.trace()?.reload_default_level("trace")?;
reload
.local()
.set_override_enabled("decoder_debug", false)?;
```
配置文件监听和 `pi_config` 更新也只会 reload local、remote 和 trace 过滤规则。
endpoint、protocol、enabled、格式和文件路径需要重启。
## 11. 配置检查
启动前检查配置文件:
```rust
use pi_logger::observability::validate_observability_config_path;
let config = validate_observability_config_path("observability.toml")?;
```
校验覆盖 TOML 结构、日志等级、重复 rule name、regex/glob、字段规则、exporter 和 endpoint。
## 12. JS 层日志使用
JS 层通过 `pi_pt_logger` 接入新版日志系统,最终统一写入 Rust
`pi_logger::observability` 管道,并使用相同的等级、target 和输出规则。
`pi_pt_logger` 提供两类常用接入方式:
- **接管 console**:接管 `console.log/info/debug/warn/error`,使常规 console 输出进入
底层日志系统。
- **适配 JS Logger**:通过 RustAppender 适配现有 JS Logger,保留模块来源作为日志
target,支持按模块配置过滤规则。
业务代码继续使用熟悉的 console 或 JS Logger API,无需直接调用 Rust 日志接口。JS 层
会进行前置过滤,Rust observability 负责最终过滤、格式化以及本地或远端输出。
## 13. 实践建议
- 生产默认等级使用 `info`,排障时通过 override 临时开启 `debug` 或 `trace`。
- 优先使用稳定 target,不要将动态 ID 拼入 target。
- 优先使用 `exact` 和 `prefix`,只在必要时使用 contains、glob 和 regex。
- 日志量仍然过大时,再给特定 override 增加 `field_rules`。
- field_rules 应绑定到明确 target,避免全局事件字段扫描。
- 结构化业务日志使用 `tracing::...!`,旧模块可以继续使用 `log::...!`。
- traces 用于调用链,logs 用于事件,metrics 用于聚合数值,不要互相替代。
- 多进程环境中,每个进程都需要接收配置更新并调用 reload。
- 应用退出时始终调用 `guards.shutdown()`。
可运行示例:
```text
examples/observability_demo.rs
examples/observability.toml
examples/observability_grpc.toml
examples/run_observability_demo.bat
examples/run_observability_demo_grpc.bat
```