# Observability 手册

`rs-zero` 的 `observability` feature 提供统一 Prometheus registry、自动指标记录和 tracing helper；`otlp` feature 负责真实 OTLP trace exporter。

## 核心能力

- 一个 `MetricsRegistry` 同时记录 HTTP、gRPC、SQL、Redis、cache、resilience 指标。
- REST metrics middleware 使用 matched route 作为标签，避免 raw path 高基数。
- RPC unary helper 与 `RpcResilienceLayer` 自动记录 gRPC request metrics。
- SQL helper 和 `rzcli model gen --with-sqlx` 生成的 repository 自动记录 SQL 查询指标。
- Redis/cache adapter 可通过 `with_metrics(registry)` 自动记录命令、命中、未命中和错误事件。
- 日志初始化会记录 span close event，HTTP/gRPC span 自动携带低基数 correlation 字段：`request_id`、`traceparent`、`trace_id/span_id`、service、transport、route/method、status/code。
- `core::logging` 支持 text/json、env filter、stdout/stderr/file/rolling writer、subscriber 输出层脱敏、采样和错误聚合。
- REST/RPC 可解析 W3C `traceparent`，并在启用 OTLP 时把入站 TraceContext 设为当前 span 的 parent。
- RPC interceptor 可在启用 OTLP 时用全局 propagator 注入当前 trace context。
- `otlp` feature 下支持 OTLP gRPC/HTTP protobuf、headers、resource attributes、sampling、timeout 和 shutdown/flush handle。
- `profiling` feature 下可接入 Pyroscope + pprof-rs，默认 feature 不引入 profiling 依赖。

## 统一 registry wiring

应用应创建一个 registry，并传给所有需要观测的 adapter：

```rust
use axum::{Router, routing::get};
use rs_zero::observability::{MetricsRegistry, metrics_router};
use rs_zero::rest::{
    ApiResponse, RestConfig, RestMetricsConfig, RestMiddlewareConfig, RestServer,
};

let metrics = MetricsRegistry::new();
let router = Router::new()
    .route("/ready", get(|| async { ApiResponse::success("ok") }))
    .merge(metrics_router(metrics.clone()));

let config = RestConfig {
    metrics_registry: Some(metrics.clone()),
    middlewares: RestMiddlewareConfig {
        metrics: RestMetricsConfig { enabled: true },
        ..RestMiddlewareConfig::default()
    },
    ..RestConfig::default()
};

let app = RestServer::new(config, router).into_router();
```

`metrics_router` 暴露 Prometheus text endpoint，默认路径是 `/metrics`。

## 指标列表

| 指标 | 类型 | 标签 |
| --- | --- | --- |
| `rs_zero_http_requests_total` | counter | `method`、`route`、`status` |
| `rs_zero_http_request_duration_seconds` | histogram | `method`、`route`、`status` |
| `rs_zero_http_requests_in_flight` | gauge | 无 |
| `rs_zero_rpc_requests_total` | counter | `service`、`method`、`code` |
| `rs_zero_rpc_request_duration_seconds` | histogram | `service`、`method`、`code` |
| `rs_zero_sql_queries_total` | counter | `db_kind`、`repository`、`method`、`operation`、`result` |
| `rs_zero_sql_query_errors_total` | counter | 同 SQL labels |
| `rs_zero_sql_query_duration_seconds` | histogram | 同 SQL labels |
| `rs_zero_redis_commands_total` | counter | `command`、`shard`、`result` |
| `rs_zero_redis_command_errors_total` | counter | 同 Redis labels |
| `rs_zero_redis_command_duration_seconds` | histogram | 同 Redis labels |
| `rs_zero_cache_events_total` | counter | `component`、`operation`、`result` |
| `rs_zero_resilience_events_total` | counter | `component`、`outcome`、`transport` |

标签禁止包含 raw URL path、SQL 文本、SQL 参数、Redis key、cache key、请求体、用户 id 或其它业务高基数字段。

## SQL、Redis、cache 接入

SQL helper 适合业务自定义查询；生成 repository 会自动使用同一路径：

```rust
use rs_zero::observability::{MetricsRegistry, observe_sql_query};

async fn find_user(metrics: &MetricsRegistry) -> Result<(), sqlx::Error> {
    observe_sql_query(
        Some(metrics),
        "postgres",
        "users",
        "find_by_id",
        "select",
        async {
            // sqlx query here
            Ok(())
        },
    )
    .await
}
```

Redis 和 cache adapter 通过 `with_metrics` 接入：

```rust
use rs_zero::cache::{CacheAside, CacheAsideConfig, LruCacheStore, MemoryCacheStore, TwoLevelCacheStore};
use rs_zero::observability::MetricsRegistry;

let metrics = MetricsRegistry::new();
let l1 = LruCacheStore::new(1024)?.with_metrics(metrics.clone());
let l2 = MemoryCacheStore::new();
let store = TwoLevelCacheStore::new(l1, l2).with_metrics(metrics.clone());
let cache = CacheAside::new(store, CacheAsideConfig::default()).with_metrics(metrics);
```

`cache-redis` feature 下，`RedisCacheStore::with_metrics` 与 `RedisShardedCacheStore::with_metrics` 会记录 Redis command 指标，shard 标签使用配置中的 shard name。

## trace id 与日志关联

REST 默认 request id header 是 `x-request-id`，gRPC 使用同名 metadata。`CorrelationContext` 会统一构造日志和 span 使用的低基数字段：

- `request_id`
- 入站 `traceparent`
- `trace_id` / `span_id`（来自当前 OTLP span，或合法入站 `traceparent`）
- `service`、`transport`、`route` / `method`
- `status` / `code`

REST 只使用 axum matched route，例如 `/users/:id`；缺少 route pattern 时写入 `unknown`，不会把 raw path、query、用户 id 或 token 写入 correlation 字段。RPC 使用 service 和 method pattern，不写 metadata 中的业务参数。`RpcResilienceLayer::run_unary` 和 `observe_rpc_unary` 会输出 INFO 事件 `rpc unary observed`，字段包含 `rpc.service`、`rpc.method`、`route`、`request_id`、`traceparent`、`trace_id`、`span_id` 和 `code`，其中 `route` 与 RPC method 保持一致。若 server 侧持有 `tonic::Request<T>`，应优先使用 `run_unary_with_metadata` 或 `observe_rpc_unary_with_metadata`，否则提前 `into_inner()` 会丢掉 `x-request-id` 和 `traceparent`。`core::init_tracing` 和 OTLP tracing 初始化也会输出 span close event，方便从日志中看到 span 字段。未启用 `otlp` 时不会伪造 trace id，仍可使用 request id 或入站 `traceparent` 关联日志与请求。

如果当前服务作为 RPC client 调用下游，`request_id_interceptor` 会优先使用 tonic `Request` extensions、REST metrics middleware 写入的当前 request id，或 `with_rpc_request_id` task-local 上下文；都不存在时才生成新 id。需要 trace 时，可使用 `trace_context_interceptor` 注入当前 OTLP trace context。没有活跃 OTLP context 时不会写入伪 trace：

```rust
use rs_zero::rpc::trace_context_interceptor;

let interceptor = trace_context_interceptor();
```

启用 `otlp` 后，`install_otlp_tracing` 会安装 W3C TraceContext propagator。REST metrics middleware 会从 HTTP header 提取 parent context；RPC helper 可通过 `observe_rpc_unary_with_context` 传入入站 `traceparent`；需要手写适配器时可使用：

```rust
use rs_zero::observability::{
    opentelemetry_context_from_headers,
    opentelemetry_context_from_traceparent,
};
```

这些 helper 只接受合法 W3C `traceparent`，不会在缺少 OTLP context 时伪造 trace id。

## 结构化日志、脱敏与采样

`LogConfig::from_env` 会读取 `RUST_LOG`，适合 CLI 生成的服务默认入口。生产建议使用 JSON 输出，便于日志平台解析：

```rust
use rs_zero::core::logging::{LogConfig, LogFormat, LogWriterConfig, init_tracing};

let config = LogConfig::from_env()
    .with_format(LogFormat::Json)
    .with_service("orders")
    .with_writer(LogWriterConfig::Stdout);

init_tracing(config)?;
```

需要写入文件时使用显式 writer。`RollingFile` 支持运行期按大小轮转；写入路径会自动创建父目录，超过 `max_bytes` 前会轮转，并按 `max_files` 保留历史文件。`max_age` 只作为平台策略说明，当前不在进程内按时间删除文件；如需按时间或压缩归档，仍应接入 logrotate、journald、容器日志驱动或托管日志平台：

```rust
use std::time::Duration;
use rs_zero::core::logging::{LogWriterConfig, RollingFileConfig};

let writer = LogWriterConfig::RollingFile(RollingFileConfig {
    path: "logs/service.log".into(),
    max_bytes: Some(128 * 1024 * 1024),
    max_files: 5,
    max_age: Some(Duration::from_secs(7 * 24 * 60 * 60)),
});
```

低基数字段可通过 `LogFields` 或 `CorrelationContext` 统一构造：

```rust
use rs_zero::core::logging::LogFields;

let fields = LogFields::new("orders")
    .with_transport("grpc")
    .with_route("/orders/:id")
    .with_request_id("req-1")
    .with_status("Ok");
```

敏感信息会在 subscriber 最终输出前脱敏，覆盖 text、JSON、span fields、event fields 和错误文本。默认规则覆盖 `authorization`、`cookie`、`token`、`password`、`secret`、`api_key` 等常见字段；业务字段可通过 `LogConfig::with_redaction` 扩展：

```rust
use rs_zero::core::logging::{LogConfig, RedactionConfig, redact_text};

let config = LogConfig::from_env().with_redaction(RedactionConfig::default());
let safe = redact_text(
    "Authorization: Bearer sample-value api_key sample-value",
    &RedactionConfig::default(),
);
```

高频重复日志可用 `LogSampler` 限制输出，错误聚合可用 `ErrorAggregator` 按低基数 fingerprint 统计：

```rust
use rs_zero::core::logging::{ErrorAggregator, LogSampler, SamplingConfig};

let sampler = LogSampler::new(SamplingConfig {
    enabled: true,
    first_n: 3,
    thereafter: 100,
});

if sampler.should_log("redis-timeout") {
    // emit log
}

let aggregator = ErrorAggregator::new();
aggregator.record("user 42 failed with token abc123");
let snapshot = aggregator.snapshot();
```

## OTLP tracing

```rust
use rs_zero::observability::{OtlpTraceConfig, install_otlp_tracing};

let handle = install_otlp_tracing(
    OtlpTraceConfig::default(),
    "info,tower_http=debug".to_string(),
)?;
handle.flush()?;
handle.shutdown()?;
```

`init_opentelemetry_tracing_with_handle` 适合从统一配置初始化；如果只需要兼容旧入口，可以继续使用 `init_opentelemetry_tracing`。

## Profiling 与 Pyroscope

Profiling 是独立、实验性、opt-in feature，默认不会进入运行时依赖。它适合压测、容量评估和受控生产试点；不要在未评估开销的关键路径默认启用。

```toml
rs-zero = { version = "0.1", features = ["profiling"] }
```

启动 Pyroscope agent：

```rust
use rs_zero::profiling::{ProfilingConfig, PyroscopeConfig, start_profiling};

let handle = start_profiling(ProfilingConfig::pyroscope(PyroscopeConfig {
    endpoint: "http://127.0.0.1:4040".to_string(),
    service_name: "orders".to_string(),
    ..PyroscopeConfig::default()
}))?;

// shutdown path:
handle.shutdown()?;
```

生产使用前确认：

- Pyroscope server 或 Grafana Cloud Profiles endpoint 可达。
- `service_name` 和 tags 使用低基数字段，例如 `env`、`region`、`version`。
- `sample_rate` 不设为过高；`0` 会归一化为 `100`。
- pprof-rs 采样会影响进程，压测环境先验证开销。
- 默认 CI 不连接真实 Pyroscope；真实验证使用 `scripts/external-integration.sh pyroscope` 或 GitHub Actions external integrations 手动 target。
- 启用后必须在 shutdown 路径调用 `ProfilingHandle::shutdown()`，避免进程退出时丢失 profile 上报。

## 使用建议

- 所有服务都应启用 request id，便于日志和 trace 串联。
- 所有 adapter 复用同一个 `MetricsRegistry`，避免 `/metrics` 输出分散。
- 结构化日志字段保持低基数，不写请求体、SQL 参数、Redis key、cache key 或用户标识。
- 默认 subscriber 会在输出前脱敏；业务自定义敏感字段应加入 `RedactionConfig`，仍不要主动记录请求体、SQL 参数、Redis key 或 token。
- 默认测试不连接真实 collector；真实 OTLP collector 测试使用 ignored/external 路径。
- 初始化 tracing 时要避免重复安装 subscriber；重复初始化会返回明确错误。测试或嵌入场景可使用 `with_scoped_tracing` 安装作用域 subscriber。
- 指标标签只使用 route pattern、service、method、operation、status/code/result 等低基数维度。

## 相关文档与测试

- [Observability](../observability.md)
- `examples/observability-hello/README.md`
- `examples/production-adapters/otlp-tracing.rs`
- `cargo test --test observability_integration`
- `cargo test --test observability_trace_correlation`
- `cargo test --test observability_otlp`
- `cargo test --test logging_integration`
- `cargo test --test profiling_integration --features profiling`
- `scripts/external-integration.sh otlp pyroscope`
- `cargo check -p rs-zero --no-default-features --features otlp`
