# 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. 新旧版本差异

| 能力 | 旧版日志接口 | 新版 `observability` |
|---|---|---|
| 主要技术栈 | `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"
```

协议说明：

| protocol | 常用端口 | endpoint 行为 |
|---|---:|---|
| `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 | 行为 |
|---|---|
| `exact` | 完全相等 |
| `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"
);
```

支持的字段操作符：

| 类型 | op |
|---|---|
| 相等 | `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
```
