# pi_logger

`pi_logger` 是一个 Rust 日志与可观测性库。当前项目同时保留旧的 `log4rs` HTTP/SLS appender 能力，并新增了基于 `tracing` 的 `observability` 统一接口。

Observability 初始化后同时支持 `tracing::...!` 和标准日志门面 `log::...!`。

新接口重点支持：

- 控制台日志；
- 本地 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
protocol = "http_binary"
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"
protocol = "http_binary"
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"
protocol = "http_binary"
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. 过滤规则模型

完整规则说明见：

[Observability 过滤规则使用说明](doc/observability_filter_rules.md)

[pi_logger Observability 程序员使用指南](doc/observability_programmer_guide.md)

核心模型：

```text
default_level + overrides
```

- 没有命中 override 时使用 `default_level`。
- 多条 override 命中时选择 `priority` 最大的一条。
- override 内的 `fuzzy_rules` 是 OR 关系。
- override 内的 `field_rules` 是 AND 关系。
- 最高优先级 override 一旦选中，字段不匹配不会回退默认等级或较低优先级规则。

## 6. default_level

支持 `trace`、`debug`、`info`、`warn`、`error`、`off`。生产环境通常使用
`info`，排查问题时通过 overrides 临时提升部分模块。

## 7. overrides

Override 按 target/span 选择一批日志并覆盖默认等级。`name` 必须唯一，
`priority` 越大优先级越高，`enabled = false` 时规则不参与匹配。

## 8. Fuzzy Rules

支持 target/span 的 `exact`、`prefix`、`contains`、`glob`、`regex` 匹配。
高流量场景优先使用 target `exact` 和 `prefix`。

## 9. Field Rules

支持 event fields 的相等、字符串、集合、存在性和数值比较。字段规则只读取
`tracing` event fields，不读取 `#[tracing::instrument(fields(...))]` 的 span fields。

## 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]`。

Observability 的 OTLP HTTP logs、traces 和 metrics 客户端统一禁用系统及环境变量代理，
不会使用 `HTTP_PROXY`、`HTTPS_PROXY` 或 `ALL_PROXY`，而是直接连接配置的 Collector
endpoint。

## 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
protocol = "http_binary"
endpoint = "http://127.0.0.1:4318"
```

实际上传地址会自动拼接为：

```text
http://127.0.0.1:4318/v1/logs
```

远端日志使用 `opentelemetry-appender-tracing` 将 `tracing` event 转换为 OpenTelemetry LogRecord。

logs、traces 和 metrics 均支持两种 OTLP transport：

```text
http_binary  OTLP Protobuf over HTTP，默认值，通常使用 4318
grpc         OTLP over gRPC，通常使用 4317
```

gRPC 配置示例：

```toml
[log.remote]
enabled = true
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"

[trace]
enabled = true
exporter = "otlp"
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"
service_name = "my-rust-service"

[metrics]
enabled = true
exporter = "otlp"
protocol = "grpc"
endpoint = "http://127.0.0.1:4317"
service_name = "my-rust-service"
```

HTTP endpoint 会自动追加对应的 `/v1/logs`、`/v1/traces` 或 `/v1/metrics`。
gRPC endpoint 原样传给 tonic，不追加 HTTP 路径。`protocol` 和 endpoint 均不支持热更新，
修改后需要重启进程。

启用任意 gRPC 输出时，`pi_logger` 会创建并由 `ObservabilityGuards` 持有专用 Tokio
runtime，因此同步 `main` 也可以初始化 gRPC exporter。应用必须持续持有 guards，并在
退出时调用 `guards.shutdown()`，确保 exporter 关闭后再释放 runtime。

## 15. OpenTelemetry traces

```toml
[trace]
enabled = true
exporter = "otlp"
protocol = "http_binary"
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(())
}
```

### 16.1 使用 log 宏

Observability 初始化会安装 `tracing-log` 兼容桥接，以下日志进入相同的
console、local 和 remote 日志层：

```rust
log::info!("application started");
log::debug!(target: "my_app::payment", "payment debug");
log::warn!(target: "my_app::payment", "payment warning");
```

`log` target 可以使用现有 fuzzy target 规则过滤。未显式指定 target 时，默认使用
Rust 模块路径。

桥接后的事件在格式化输出中顶层 tracing target 显示为 `log`，原始 target 保存在
`log.target` 字段。Observability target 过滤会自动使用原始 `log.target`。

普通 `log::...!` 主要提供等级、target 和 message，不支持 `tracing::info!` 的结构化
业务字段语法。需要按 `id`、`status` 等字段过滤时，应使用 `tracing::...!`。

Observability 会把 `log` 门面的最大等级设置为 `trace`，最终过滤交给 observability
规则，因此运行时 reload 到 debug/trace 后，`log::debug!` 和 `log::trace!` 可以立即生效。

Rust 进程只能安装一个全局 `log` logger。Observability 不能与已初始化的 log4rs、
env_logger 或其他全局 logger 同时使用；发生冲突时初始化会返回明确错误。

说明：

- `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. 导出 JavaScript 日志预过滤配置

跨语言调用场景可以在 JavaScript 层提前过滤明显不需要的普通日志，减少进入
Rust 和 tracing 层的调用成本：

```rust
let revision = reload.current_filter_revision();
let snapshot = reload.export_js_log_filter_snapshot();
let json = reload.export_js_log_filter_json()?;
```

对于不方便保存 `ObservabilityReloadHandle` 的 JavaScript/FFI 层，可以使用初始化后注册的
全局导出函数：

```rust
use pi_logger::observability::{
    current_js_log_filter_revision, export_js_log_filter_json,
    export_js_log_filter_snapshot,
};

let revision = current_js_log_filter_revision()?;
let snapshot = export_js_log_filter_snapshot()?;
let json = export_js_log_filter_json()?;
```

全局函数只能在 `init_observability*` 成功后调用。调用
`ObservabilityGuards::shutdown()` 后，全局导出状态会被清理，后续调用返回错误。实例方法
继续保留，适合 Rust 代码直接持有初始化返回的 reload handle。

导出快照只包含启动时已启用的普通日志输出：

- `local` 表示 console 和本地文件日志；
- `remote` 表示远端 OTLP logs；
- traces 和 metrics 不包含在快照中；
- `revision` 在过滤规则成功 reload 后递增；
- `schema_version` 用于识别导出协议版本。

JavaScript 必须使用保守过滤策略：只有 local 和 remote 两个有效输出都能确定拒绝时，
才能提前丢弃日志。未知 schema、未知操作符、span 规则、field 规则无法准确判断时，
必须允许日志进入 Rust，由 Rust 过滤器做最终判断。

每个作用域包含 `requires_rust_evaluation`。该值为 `true` 表示作用域中至少存在
span 规则或 field 规则。JavaScript 仍可执行能够确定的 `level + target` 判断，但在
无法确定规则是否命中时必须放行。

推荐由 JS 低成本轮询 revision，仅在 revision 变化后重新获取完整 JSON：

```text
poll current_filter_revision()
  -> revision 未变化：继续使用已有快照
  -> revision 已变化：调用 export_js_log_filter_json()
  -> 原子替换 JS 当前规则
```

`pi_logger` 只提供 Rust 结构和 JSON 字符串，不绑定具体 JavaScript/FFI 框架。

## 20. 验证方式

当前项目建议使用：

```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
```

Windows 可直接运行：

```bat
examples\run_observability_demo.bat
```

脚本会切换到仓库根目录，并默认使用 `examples\observability.toml`。调用前已设置
`PI_LOGGER_OBSERVABILITY_CONFIG` 时，脚本会保留外部配置路径。附加参数会继续传递给
`cargo run`。

使用 OTLP gRPC 配置测试 Collector `4317` 端口：

```bat
examples\run_observability_demo_grpc.bat
```

对应配置文件为 `examples\observability_grpc.toml`。该配置将 logs、traces 和 metrics
全部设置为 `protocol = "grpc"`。

不传配置路径时使用 `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`。
