Files
qiming/qiming-rcoder/crates/rcoder-telemetry/src/lib.rs
2026-06-01 13:54:52 +08:00

353 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! RCoder 遥测模块
//!
//! 提供统一的遥测功能,包括:
//! - **OTLP Tracing**: 分布式追踪,支持 Jaeger/OTLP Collector
//! - **Prometheus Metrics**: HTTP/gRPC 请求指标、业务指标
//! - **Trace Propagation**: 跨服务 trace context 传播
//! - **Console & File Logging**: 控制台和文件日志输出
//!
//! # 快速开始
//!
//! ```no_run
//! use rcoder_telemetry::{TelemetryConfig, init};
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//! // 从环境变量初始化配置
//! let config = TelemetryConfig::from_env("my-service");
//!
//! // Initializing telemetry system包含 console 日志、OTLP 追踪、Prometheus 指标)
//! let telemetry = init(config).await?;
//!
//! // 在应用中使用 telemetry.render_metrics() 暴露 /metrics 端点
//!
//! Ok(())
//! }
//! ```
//!
//! # 环境变量
//!
//! | 变量 | 说明 | 默认值 |
//! |-----|------|-------|
//! | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP 端点 | - |
//! | `OTEL_SERVICE_NAME` | 服务名称 | 代码指定 |
//! | `OTEL_TRACES_SAMPLER_ARG` | 采样率 | `1.0` |
//! | `OTEL_EXPORTER_OTLP_PROTOCOL` | 协议 (grpc/http) | `grpc` |
//! | `TELEMETRY_PROMETHEUS_ENABLED` | 启用 Prometheus | `true` |
//! | `RUST_LOG` | 日志级别过滤 | `info` |
pub mod config;
pub mod middleware;
pub mod otlp;
pub mod prometheus;
pub mod propagation;
// Re-exports
pub use config::{FileLogConfig, OtlpConfig, PrometheusConfig, TelemetryConfig};
pub use middleware::{GrpcMetricsInterceptor, HttpMetricsLayer};
pub use prometheus::{
dec_active_tasks, inc_active_tasks, record_agent_task, record_agent_task_duration,
record_grpc_duration, record_grpc_request, record_http_duration, record_http_request,
set_active_tasks,
};
pub use propagation::{
extract_context, extract_context_http, inject_context, inject_context_http,
set_global_propagator,
};
use anyhow::Result;
use metrics_exporter_prometheus::PrometheusHandle;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing::info;
use tracing_appender::rolling::Rotation;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
/// 遥测系统 Guard
///
/// 持有遥测资源的生命周期Drop 时自动清理。
/// 同时提供 Prometheus 指标渲染功能。
pub struct TelemetryGuard {
/// OTLP TracerProvider可选
tracer_provider: Option<SdkTracerProvider>,
/// Prometheus Handle可选
prometheus_handle: Option<PrometheusHandle>,
/// 服务名称
service_name: String,
}
impl TelemetryGuard {
/// 渲染 Prometheus 指标
///
/// 返回 Prometheus 文本格式的指标数据,
/// 可直接作为 `/metrics` 端点的响应。
///
/// # Returns
///
/// 如果 Prometheus 已启用,返回 `Some(metrics_text)`
/// 否则返回 `None`。
pub fn render_metrics(&self) -> Option<String> {
self.prometheus_handle.as_ref().map(|h| h.render())
}
/// 检查 OTLP 是否已启用
pub fn is_otlp_enabled(&self) -> bool {
self.tracer_provider.is_some()
}
/// 检查 Prometheus 是否已启用
pub fn is_prometheus_enabled(&self) -> bool {
self.prometheus_handle.is_some()
}
/// 获取服务名称
pub fn service_name(&self) -> &str {
&self.service_name
}
}
impl Drop for TelemetryGuard {
fn drop(&mut self) {
if self.tracer_provider.is_some() {
otlp::shutdown_tracer_provider();
}
info!(
"[Telemetry] Telemetry system shutdown: {}",
self.service_name
);
}
}
/// 一键Initializing telemetry system
///
/// 根据配置初始化完整的遥测栈:
/// - **Console 日志**: 始终启用,输出到标准输出
/// - **OTLP Tracing**: 如果配置了 OTLP 端点,将 span 导出到 Jaeger/Collector
/// - **Prometheus Metrics**: 如果启用,提供 `/metrics` 端点数据
///
/// # Arguments
///
/// * `config` - 遥测配置
///
/// # Returns
///
/// 返回 `TelemetryGuard`,持有遥测资源的生命周期。
///
/// # Example
///
/// ```no_run
/// use rcoder_telemetry::{TelemetryConfig, init};
///
/// #[tokio::main]
/// async fn main() -> anyhow::Result<()> {
/// let config = TelemetryConfig::new("my-service")
/// .with_otlp_endpoint("http://jaeger:4317")
/// .with_prometheus();
///
/// let telemetry = init(config).await?;
///
/// // 应用逻辑...
///
/// Ok(())
/// }
/// ```
pub async fn init(config: TelemetryConfig) -> Result<TelemetryGuard> {
// 设置全局传播器(在初始化 subscriber 之前)
set_global_propagator();
// 初始化 OTLP如果配置了
let tracer_provider = if let Some(ref otlp_config) = config.otlp {
let provider = otlp::init_tracer_provider(otlp_config, &config.service_name).await?;
otlp::set_global_tracer_provider(provider.clone());
Some(provider)
} else {
None
};
// Initializing Prometheus如果配置了
let prometheus_handle = if config.prometheus.is_some() {
Some(prometheus::init_prometheus()?)
} else {
None
};
// 🆕 初始化 tracing subscriber包括控制台、文件、OpenTelemetry
init_tracing_subscriber(
&config.service_name,
tracer_provider.as_ref(),
config.file_log.as_ref(),
)?;
info!(
"[Telemetry] Initializing telemetry system: {}",
config.service_name
);
info!(
"✅ [Telemetry] Telemetry system initialization completed: OTLP={}, Prometheus={}, FileLog={}, Console=true",
tracer_provider.is_some(),
prometheus_handle.is_some(),
config.file_log.is_some()
);
Ok(TelemetryGuard {
tracer_provider,
prometheus_handle,
service_name: config.service_name,
})
}
/// 初始化 tracing subscriber
///
/// 配置以下层:
/// - EnvFilter: 基于 RUST_LOG 环境变量的日志级别过滤
/// - Console Layer: 控制台日志输出
/// - File Layer: 可选的文件日志输出JSON 格式,按天滚动)
/// - OpenTelemetry Layer: 如果提供了 TracerProvider将 span 发送到 OTLP
fn init_tracing_subscriber(
service_name: &str,
tracer_provider: Option<&SdkTracerProvider>,
file_log_config: Option<&config::FileLogConfig>,
) -> Result<()> {
use opentelemetry::trace::TracerProvider;
// 创建 EnvFilter支持 RUST_LOG 环境变量)
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
// 默认日志级别
format!(
"{}=debug,tower_http=debug,axum=info,hyper=info,tonic=info",
service_name.replace('-', "_")
)
.into()
});
// 创建控制台日志层
let console_layer = fmt::layer()
.with_target(true)
.with_ansi(true)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false);
// 创建文件日志层(如果配置了)
let file_layer = if let Some(file_config) = file_log_config {
// 创建日志目录
if !file_config.directory.exists() {
std::fs::create_dir_all(&file_config.directory)?;
}
// 创建按天滚动的 appender
let file_appender = tracing_appender::rolling::Builder::new()
.rotation(Rotation::DAILY)
.filename_prefix(&file_config.filename_prefix)
.max_log_files(file_config.max_log_files)
.build(&file_config.directory)?;
if file_config.json_format {
// JSON 格式文件日志
Some(
fmt::layer()
.json()
.with_writer(file_appender)
.with_ansi(false)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.boxed(),
)
} else {
// 纯文本格式文件日志
Some(
fmt::layer()
.with_writer(file_appender)
.with_ansi(false)
.with_target(true)
.boxed(),
)
}
} else {
None
};
// 根据是否有 TracerProvider 和 FileLayer 决定如何初始化
match (tracer_provider, file_layer) {
(Some(provider), Some(file)) => {
// 有 OTLP + 文件日志
let tracer = provider.tracer(service_name.to_string());
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(env_filter)
.with(console_layer)
.with(file)
.with(otel_layer)
.init();
}
(Some(provider), None) => {
// 只有 OTLP
let tracer = provider.tracer(service_name.to_string());
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(env_filter)
.with(console_layer)
.with(otel_layer)
.init();
}
(None, Some(file)) => {
// 只有文件日志
tracing_subscriber::registry()
.with(env_filter)
.with(console_layer)
.with(file)
.init();
}
(None, None) => {
// 仅控制台
tracing_subscriber::registry()
.with(env_filter)
.with(console_layer)
.init();
}
}
Ok(())
}
/// 仅Initializing Prometheus不初始化 OTLP 和 tracing
///
/// 适用于只需要 metrics 不需要 tracing 的场景。
/// **注意**:此函数不会初始化 tracing subscriber调用方需要自行初始化。
pub fn init_prometheus_only(service_name: impl Into<String>) -> Result<TelemetryGuard> {
let service_name = service_name.into();
info!("[Telemetry] Initializing Prometheus: {}", service_name);
let prometheus_handle = prometheus::init_prometheus()?;
Ok(TelemetryGuard {
tracer_provider: None,
prometheus_handle: Some(prometheus_handle),
service_name,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_from_env() {
let config = TelemetryConfig::from_env("test-service");
assert_eq!(config.service_name, "test-service");
}
#[test]
fn test_config_builder() {
let config = TelemetryConfig::new("my-service")
.with_otlp_endpoint("http://localhost:4317")
.with_prometheus();
assert_eq!(config.service_name, "my-service");
assert!(config.otlp.is_some());
assert!(config.prometheus.is_some());
}
}