添加qiming-rcoder模块
This commit is contained in:
46
qiming-rcoder/crates/rcoder-telemetry/Cargo.toml
Normal file
46
qiming-rcoder/crates/rcoder-telemetry/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "rcoder-telemetry"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Shared telemetry module for rcoder services (Prometheus + OTLP)"
|
||||
authors = ["soddy"]
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
# OpenTelemetry
|
||||
opentelemetry = { workspace = true }
|
||||
opentelemetry_sdk = { workspace = true }
|
||||
opentelemetry-otlp = { workspace = true }
|
||||
tracing-opentelemetry = { workspace = true }
|
||||
|
||||
# Prometheus
|
||||
metrics = { workspace = true }
|
||||
metrics-exporter-prometheus = { workspace = true }
|
||||
|
||||
# Axum 中间件
|
||||
axum = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
http = { workspace = true }
|
||||
http-body = { workspace = true }
|
||||
|
||||
# gRPC (Tonic)
|
||||
tonic = { workspace = true }
|
||||
|
||||
# 其他
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }
|
||||
tracing-appender = "0.2"
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
pin-project-lite = "0.2"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# 启用完整的 OTLP 功能
|
||||
full = []
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full", "test-util"] }
|
||||
274
qiming-rcoder/crates/rcoder-telemetry/src/config.rs
Normal file
274
qiming-rcoder/crates/rcoder-telemetry/src/config.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
//! 遥测配置模块
|
||||
//!
|
||||
//! 提供统一的遥测配置,支持从环境变量读取。
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 遥测系统统一配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TelemetryConfig {
|
||||
/// 服务名称(用于 trace 和 metrics 标识)
|
||||
pub service_name: String,
|
||||
/// OTLP 配置(可选)
|
||||
pub otlp: Option<OtlpConfig>,
|
||||
/// Prometheus 配置(可选)
|
||||
pub prometheus: Option<PrometheusConfig>,
|
||||
/// 文件日志配置(可选)
|
||||
pub file_log: Option<FileLogConfig>,
|
||||
}
|
||||
|
||||
/// OTLP 导出器配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OtlpConfig {
|
||||
/// OTLP 端点地址
|
||||
/// 默认: `http://localhost:4317`(gRPC)或 `http://localhost:4318`(HTTP)
|
||||
pub endpoint: String,
|
||||
/// 采样率 (0.0 - 1.0)
|
||||
/// 默认: 1.0(100% 采样)
|
||||
pub sample_rate: f64,
|
||||
/// 是否使用 gRPC 协议
|
||||
/// 默认: true
|
||||
pub use_grpc: bool,
|
||||
}
|
||||
|
||||
/// Prometheus 指标配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrometheusConfig {
|
||||
/// 是否启用 Prometheus 指标
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// 文件日志配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileLogConfig {
|
||||
/// 日志目录
|
||||
pub directory: PathBuf,
|
||||
/// 文件名前缀
|
||||
pub filename_prefix: String,
|
||||
/// 保留的日志文件数量
|
||||
pub max_log_files: usize,
|
||||
/// 使用 JSON 格式
|
||||
pub json_format: bool,
|
||||
}
|
||||
|
||||
impl Default for TelemetryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
service_name: "unknown-service".to_string(),
|
||||
otlp: None,
|
||||
prometheus: Some(PrometheusConfig::default()),
|
||||
file_log: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OtlpConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
endpoint: "http://localhost:4317".to_string(),
|
||||
sample_rate: 1.0,
|
||||
use_grpc: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrometheusConfig {
|
||||
fn default() -> Self {
|
||||
Self { enabled: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileLogConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
directory: PathBuf::from("logs"),
|
||||
filename_prefix: "app".to_string(),
|
||||
max_log_files: 5,
|
||||
json_format: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileLogConfig {
|
||||
/// 创建新的文件日志配置
|
||||
pub fn new(directory: impl Into<PathBuf>, filename_prefix: impl Into<String>) -> Self {
|
||||
Self {
|
||||
directory: directory.into(),
|
||||
filename_prefix: filename_prefix.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置保留的日志文件数量
|
||||
pub fn with_max_files(mut self, max_files: usize) -> Self {
|
||||
self.max_log_files = max_files;
|
||||
self
|
||||
}
|
||||
|
||||
/// 禁用 JSON 格式(使用纯文本)
|
||||
pub fn with_text_format(mut self) -> Self {
|
||||
self.json_format = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TelemetryConfig {
|
||||
/// 创建新的配置
|
||||
pub fn new(service_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
service_name: service_name.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 从环境变量读取配置
|
||||
///
|
||||
/// 支持的环境变量:
|
||||
/// - `OTEL_SERVICE_NAME` - 服务名称(如果未指定则使用参数值)
|
||||
/// - `OTEL_EXPORTER_OTLP_ENDPOINT` - OTLP 端点
|
||||
/// - `OTEL_TRACES_SAMPLER_ARG` - 采样率
|
||||
/// - `OTEL_EXPORTER_OTLP_PROTOCOL` - 协议(grpc/http)
|
||||
/// - `TELEMETRY_PROMETHEUS_ENABLED` - 是否启用 Prometheus(true/false)
|
||||
pub fn from_env(default_service_name: impl Into<String>) -> Self {
|
||||
let service_name =
|
||||
env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| default_service_name.into());
|
||||
|
||||
// OTLP 配置
|
||||
let otlp_endpoint = env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
|
||||
let otlp = otlp_endpoint.map(|endpoint| {
|
||||
let sample_rate = env::var("OTEL_TRACES_SAMPLER_ARG")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1.0);
|
||||
|
||||
let use_grpc = env::var("OTEL_EXPORTER_OTLP_PROTOCOL")
|
||||
.map(|p| p.to_lowercase() != "http")
|
||||
.unwrap_or(true);
|
||||
|
||||
OtlpConfig {
|
||||
endpoint,
|
||||
sample_rate,
|
||||
use_grpc,
|
||||
}
|
||||
});
|
||||
|
||||
// Prometheus 配置
|
||||
let prometheus_enabled = env::var("TELEMETRY_PROMETHEUS_ENABLED")
|
||||
.map(|v| v.to_lowercase() != "false" && v != "0")
|
||||
.unwrap_or(true);
|
||||
|
||||
let prometheus = if prometheus_enabled {
|
||||
Some(PrometheusConfig { enabled: true })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
service_name,
|
||||
otlp,
|
||||
prometheus,
|
||||
file_log: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 启用 OTLP(使用默认配置)
|
||||
pub fn with_otlp(mut self) -> Self {
|
||||
self.otlp = Some(OtlpConfig::default());
|
||||
self
|
||||
}
|
||||
|
||||
/// 启用 OTLP(使用指定端点)
|
||||
pub fn with_otlp_endpoint(mut self, endpoint: impl Into<String>) -> Self {
|
||||
self.otlp = Some(OtlpConfig {
|
||||
endpoint: endpoint.into(),
|
||||
..Default::default()
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置 OTLP 配置
|
||||
pub fn with_otlp_config(mut self, config: OtlpConfig) -> Self {
|
||||
self.otlp = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// 禁用 Prometheus
|
||||
pub fn without_prometheus(mut self) -> Self {
|
||||
self.prometheus = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// 启用 Prometheus
|
||||
pub fn with_prometheus(mut self) -> Self {
|
||||
self.prometheus = Some(PrometheusConfig::default());
|
||||
self
|
||||
}
|
||||
|
||||
/// 启用文件日志(使用默认配置)
|
||||
pub fn with_file_log(mut self, filename_prefix: impl Into<String>) -> Self {
|
||||
self.file_log = Some(FileLogConfig {
|
||||
filename_prefix: filename_prefix.into(),
|
||||
..Default::default()
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// 启用文件日志(使用自定义配置)
|
||||
pub fn with_file_log_config(mut self, config: FileLogConfig) -> Self {
|
||||
self.file_log = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// 禁用文件日志
|
||||
pub fn without_file_log(mut self) -> Self {
|
||||
self.file_log = None;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = TelemetryConfig::default();
|
||||
assert_eq!(config.service_name, "unknown-service");
|
||||
assert!(config.otlp.is_none());
|
||||
assert!(config.prometheus.is_some());
|
||||
assert!(config.file_log.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_config() {
|
||||
let config = TelemetryConfig::new("my-service");
|
||||
assert_eq!(config.service_name, "my-service");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_otlp() {
|
||||
let config = TelemetryConfig::new("test").with_otlp_endpoint("http://jaeger:4317");
|
||||
|
||||
assert!(config.otlp.is_some());
|
||||
let otlp = config.otlp.unwrap();
|
||||
assert_eq!(otlp.endpoint, "http://jaeger:4317");
|
||||
assert!(otlp.use_grpc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_without_prometheus() {
|
||||
let config = TelemetryConfig::new("test").without_prometheus();
|
||||
assert!(config.prometheus.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_file_log() {
|
||||
let config = TelemetryConfig::new("test").with_file_log("my-service");
|
||||
|
||||
assert!(config.file_log.is_some());
|
||||
let file_log = config.file_log.unwrap();
|
||||
assert_eq!(file_log.filename_prefix, "my-service");
|
||||
assert_eq!(file_log.directory, PathBuf::from("logs"));
|
||||
}
|
||||
}
|
||||
352
qiming-rcoder/crates/rcoder-telemetry/src/lib.rs
Normal file
352
qiming-rcoder/crates/rcoder-telemetry/src/lib.rs
Normal file
@@ -0,0 +1,352 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
152
qiming-rcoder/crates/rcoder-telemetry/src/middleware/grpc.rs
Normal file
152
qiming-rcoder/crates/rcoder-telemetry/src/middleware/grpc.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
//! gRPC 指标拦截器
|
||||
//!
|
||||
//! 为 Tonic gRPC 服务提供指标收集功能。
|
||||
|
||||
use std::time::Instant;
|
||||
use tonic::{Request, Status};
|
||||
|
||||
use crate::prometheus::{record_grpc_duration, record_grpc_request};
|
||||
|
||||
/// gRPC 指标拦截器
|
||||
///
|
||||
/// 用于记录 gRPC 请求的指标。
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use rcoder_telemetry::middleware::GrpcMetricsInterceptor;
|
||||
/// use tonic::transport::Server;
|
||||
///
|
||||
/// // 创建拦截器
|
||||
/// let interceptor = GrpcMetricsInterceptor::new();
|
||||
///
|
||||
/// // 在服务中使用
|
||||
/// // Server::builder()
|
||||
/// // .add_service(MyServiceServer::with_interceptor(service, interceptor.intercept()))
|
||||
/// // .serve(addr)
|
||||
/// // .await?;
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GrpcMetricsInterceptor;
|
||||
|
||||
impl GrpcMetricsInterceptor {
|
||||
/// 创建新的 gRPC 指标拦截器
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// 获取拦截器函数
|
||||
///
|
||||
/// 返回一个可以传递给 `with_interceptor` 的函数。
|
||||
pub fn intercept(&self) -> impl Fn(Request<()>) -> Result<Request<()>, Status> + Clone {
|
||||
move |req: Request<()>| {
|
||||
// 拦截器只能记录请求开始,响应指标需要在服务层处理
|
||||
// 这里主要用于注入 trace context
|
||||
Ok(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// gRPC 请求计时器
|
||||
///
|
||||
/// 用于在服务方法中记录请求耗时。
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use rcoder_telemetry::middleware::grpc::GrpcRequestTimer;
|
||||
///
|
||||
/// async fn my_grpc_method() {
|
||||
/// let timer = GrpcRequestTimer::new("MyService/MyMethod");
|
||||
///
|
||||
/// // ... 处理请求 ...
|
||||
///
|
||||
/// timer.record_success();
|
||||
/// // 或者
|
||||
/// // timer.record_error();
|
||||
/// }
|
||||
/// ```
|
||||
pub struct GrpcRequestTimer {
|
||||
method: String,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl GrpcRequestTimer {
|
||||
/// 创建新的请求计时器
|
||||
pub fn new(method: impl Into<String>) -> Self {
|
||||
Self {
|
||||
method: method.into(),
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录成功请求
|
||||
pub fn record_success(self) {
|
||||
let duration = self.start.elapsed();
|
||||
record_grpc_request(&self.method, "ok");
|
||||
record_grpc_duration(&self.method, duration);
|
||||
}
|
||||
|
||||
/// 记录失败请求
|
||||
pub fn record_error(self) {
|
||||
let duration = self.start.elapsed();
|
||||
record_grpc_request(&self.method, "error");
|
||||
record_grpc_duration(&self.method, duration);
|
||||
}
|
||||
|
||||
/// 记录请求(指定状态)
|
||||
pub fn record(self, status: &str) {
|
||||
let duration = self.start.elapsed();
|
||||
record_grpc_request(&self.method, status);
|
||||
record_grpc_duration(&self.method, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 gRPC 请求中提取方法名
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - 请求 URI(如 `/package.Service/Method`)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 返回方法名(如 `Service/Method`)
|
||||
pub fn extract_method_name(uri: &str) -> String {
|
||||
// gRPC URI 格式: /package.Service/Method
|
||||
// 提取 Service/Method 部分
|
||||
if let Some(pos) = uri.rfind('.') {
|
||||
uri[pos + 1..].to_string()
|
||||
} else if let Some(stripped) = uri.strip_prefix('/') {
|
||||
stripped.to_string()
|
||||
} else {
|
||||
uri.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_method_name() {
|
||||
assert_eq!(
|
||||
extract_method_name("/agent.AgentService/Chat"),
|
||||
"AgentService/Chat"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_method_name("/MyService/MyMethod"),
|
||||
"MyService/MyMethod"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grpc_metrics_interceptor() {
|
||||
let interceptor = GrpcMetricsInterceptor::new();
|
||||
let intercept_fn = interceptor.intercept();
|
||||
|
||||
// 测试拦截器不会修改请求
|
||||
let req = Request::new(());
|
||||
let result = intercept_fn(req);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
194
qiming-rcoder/crates/rcoder-telemetry/src/middleware/http.rs
Normal file
194
qiming-rcoder/crates/rcoder-telemetry/src/middleware/http.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! HTTP 指标中间件
|
||||
//!
|
||||
//! 为 Axum 提供 HTTP 请求指标收集功能。
|
||||
|
||||
use axum::http::{Request, Response};
|
||||
use pin_project_lite::pin_project;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Instant,
|
||||
};
|
||||
use tower::{Layer, Service};
|
||||
|
||||
use crate::prometheus::{record_http_duration, record_http_request};
|
||||
|
||||
/// HTTP 指标中间件层
|
||||
///
|
||||
/// 自动收集 HTTP 请求的指标:
|
||||
/// - 请求计数(按 method, path, status 分组)
|
||||
/// - 请求耗时(按 method, path 分组)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use axum::Router;
|
||||
/// use rcoder_telemetry::middleware::HttpMetricsLayer;
|
||||
///
|
||||
/// let app: Router<()> = Router::new()
|
||||
/// // ... routes
|
||||
/// .layer(HttpMetricsLayer::new());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HttpMetricsLayer;
|
||||
|
||||
impl HttpMetricsLayer {
|
||||
/// 创建新的 HTTP 指标层
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for HttpMetricsLayer {
|
||||
type Service = HttpMetricsService<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
HttpMetricsService { inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP 指标服务
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HttpMetricsService<S> {
|
||||
inner: S,
|
||||
}
|
||||
|
||||
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for HttpMetricsService<S>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
|
||||
S::Future: Send,
|
||||
ReqBody: Send + 'static,
|
||||
ResBody: Send + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = HttpMetricsFuture<S::Future>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
let method = req.method().to_string();
|
||||
let path = normalize_path(req.uri().path());
|
||||
let start = Instant::now();
|
||||
|
||||
let future = self.inner.call(req);
|
||||
|
||||
HttpMetricsFuture {
|
||||
inner: future,
|
||||
method,
|
||||
path,
|
||||
start,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
/// HTTP 指标 Future
|
||||
pub struct HttpMetricsFuture<F> {
|
||||
#[pin]
|
||||
inner: F,
|
||||
method: String,
|
||||
path: String,
|
||||
start: Instant,
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, ResBody, E> Future for HttpMetricsFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<Response<ResBody>, E>>,
|
||||
{
|
||||
type Output = F::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
|
||||
match this.inner.poll(cx) {
|
||||
Poll::Ready(result) => {
|
||||
let duration = this.start.elapsed();
|
||||
|
||||
// 记录指标
|
||||
if let Ok(ref response) = result {
|
||||
let status = response.status().as_u16();
|
||||
record_http_request(this.method, this.path, status);
|
||||
}
|
||||
record_http_duration(this.method, this.path, duration);
|
||||
|
||||
Poll::Ready(result)
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 标准化路径
|
||||
///
|
||||
/// 移除路径参数中的具体值,保留路径模式。
|
||||
/// 例如:`/api/users/123` -> `/api/users/:id`
|
||||
fn normalize_path(path: &str) -> String {
|
||||
// 简单实现:直接返回路径
|
||||
// 可以根据需要扩展为更复杂的路径模式匹配
|
||||
let parts: Vec<&str> = path.split('/').collect();
|
||||
let normalized: Vec<String> = parts
|
||||
.iter()
|
||||
.map(|part| {
|
||||
// 如果是纯数字或 UUID 格式,替换为 :id
|
||||
if part.chars().all(|c| c.is_ascii_digit()) && !part.is_empty() {
|
||||
":id".to_string()
|
||||
} else if is_uuid(part) {
|
||||
":uuid".to_string()
|
||||
} else {
|
||||
part.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
normalized.join("/")
|
||||
}
|
||||
|
||||
/// 检查是否是 UUID 格式
|
||||
fn is_uuid(s: &str) -> bool {
|
||||
if s.len() != 36 {
|
||||
return false;
|
||||
}
|
||||
// 简单检查 UUID 格式:8-4-4-4-12
|
||||
let parts: Vec<&str> = s.split('-').collect();
|
||||
parts.len() == 5
|
||||
&& parts[0].len() == 8
|
||||
&& parts[1].len() == 4
|
||||
&& parts[2].len() == 4
|
||||
&& parts[3].len() == 4
|
||||
&& parts[4].len() == 12
|
||||
&& parts
|
||||
.iter()
|
||||
.all(|p| p.chars().all(|c| c.is_ascii_hexdigit()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path() {
|
||||
assert_eq!(normalize_path("/api/users/123"), "/api/users/:id");
|
||||
assert_eq!(normalize_path("/health"), "/health");
|
||||
assert_eq!(normalize_path("/api/v1/projects"), "/api/v1/projects");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_uuid() {
|
||||
assert!(is_uuid("550e8400-e29b-41d4-a716-446655440000"));
|
||||
assert!(!is_uuid("not-a-uuid"));
|
||||
assert!(!is_uuid("123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path_with_uuid() {
|
||||
assert_eq!(
|
||||
normalize_path("/api/sessions/550e8400-e29b-41d4-a716-446655440000/progress"),
|
||||
"/api/sessions/:uuid/progress"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! 遥测中间件模块
|
||||
//!
|
||||
//! 提供 HTTP 和 gRPC 的指标和追踪中间件。
|
||||
|
||||
pub mod grpc;
|
||||
pub mod http;
|
||||
|
||||
pub use grpc::GrpcMetricsInterceptor;
|
||||
pub use http::HttpMetricsLayer;
|
||||
175
qiming-rcoder/crates/rcoder-telemetry/src/otlp.rs
Normal file
175
qiming-rcoder/crates/rcoder-telemetry/src/otlp.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
//! OTLP TracerProvider 初始化模块
|
||||
//!
|
||||
//! 提供 OpenTelemetry OTLP 导出器的初始化功能,支持 gRPC 和 HTTP 协议。
|
||||
|
||||
use crate::config::OtlpConfig;
|
||||
use anyhow::Result;
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
|
||||
use tracing::info;
|
||||
|
||||
/// 初始化 OTLP TracerProvider
|
||||
///
|
||||
/// 根据配置创建 OTLP 导出器并构建 TracerProvider。
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `config` - OTLP 配置
|
||||
/// * `service_name` - 服务名称(用于 resource 标识)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 返回初始化后的 `SdkTracerProvider`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use rcoder_telemetry::otlp::init_tracer_provider;
|
||||
/// use rcoder_telemetry::config::OtlpConfig;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> anyhow::Result<()> {
|
||||
/// let config = OtlpConfig::default();
|
||||
/// let provider = init_tracer_provider(&config, "my-service").await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn init_tracer_provider(
|
||||
config: &OtlpConfig,
|
||||
service_name: &str,
|
||||
) -> Result<SdkTracerProvider> {
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry_sdk::Resource;
|
||||
|
||||
info!(
|
||||
"🔧 [OTLP] Initializing TracerProvider: endpoint={}, grpc={}, sample_rate={}",
|
||||
config.endpoint, config.use_grpc, config.sample_rate
|
||||
);
|
||||
|
||||
// 创建 Resource(标识服务)
|
||||
// 使用 service.name 语义约定
|
||||
let resource = Resource::builder()
|
||||
.with_attributes([KeyValue::new("service.name", service_name.to_string())])
|
||||
.build();
|
||||
|
||||
// 创建采样器
|
||||
let sampler = if config.sample_rate >= 1.0 {
|
||||
Sampler::AlwaysOn
|
||||
} else if config.sample_rate <= 0.0 {
|
||||
Sampler::AlwaysOff
|
||||
} else {
|
||||
Sampler::TraceIdRatioBased(config.sample_rate)
|
||||
};
|
||||
|
||||
// 创建 OTLP 导出器
|
||||
let tracer_provider = if config.use_grpc {
|
||||
init_grpc_provider(&config.endpoint, resource, sampler).await?
|
||||
} else {
|
||||
init_http_provider(&config.endpoint, resource, sampler).await?
|
||||
};
|
||||
|
||||
info!("[OTLP] TracerProvider initialization completed");
|
||||
|
||||
Ok(tracer_provider)
|
||||
}
|
||||
|
||||
/// 初始化 gRPC OTLP 导出器
|
||||
async fn init_grpc_provider(
|
||||
endpoint: &str,
|
||||
resource: opentelemetry_sdk::Resource,
|
||||
sampler: Sampler,
|
||||
) -> Result<SdkTracerProvider> {
|
||||
use opentelemetry_otlp::SpanExporter;
|
||||
|
||||
// 创建 gRPC 导出器
|
||||
let exporter = SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.build()?;
|
||||
|
||||
// 构建 TracerProvider
|
||||
let provider = SdkTracerProvider::builder()
|
||||
.with_batch_exporter(exporter)
|
||||
.with_sampler(sampler)
|
||||
.with_resource(resource)
|
||||
.build();
|
||||
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
/// 初始化 HTTP OTLP 导出器
|
||||
async fn init_http_provider(
|
||||
endpoint: &str,
|
||||
resource: opentelemetry_sdk::Resource,
|
||||
sampler: Sampler,
|
||||
) -> Result<SdkTracerProvider> {
|
||||
use opentelemetry_otlp::Protocol;
|
||||
use opentelemetry_otlp::SpanExporter;
|
||||
|
||||
// 创建 HTTP 导出器
|
||||
let exporter = SpanExporter::builder()
|
||||
.with_http()
|
||||
.with_endpoint(endpoint)
|
||||
.with_protocol(Protocol::HttpBinary)
|
||||
.build()?;
|
||||
|
||||
// 构建 TracerProvider
|
||||
let provider = SdkTracerProvider::builder()
|
||||
.with_batch_exporter(exporter)
|
||||
.with_sampler(sampler)
|
||||
.with_resource(resource)
|
||||
.build();
|
||||
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
/// 设置全局 TracerProvider
|
||||
///
|
||||
/// 将 TracerProvider 设置为全局 provider,并获取 tracer。
|
||||
pub fn set_global_tracer_provider(provider: SdkTracerProvider) {
|
||||
opentelemetry::global::set_tracer_provider(provider);
|
||||
info!("[OTLP] Global TracerProvider set");
|
||||
}
|
||||
|
||||
/// 关闭 TracerProvider
|
||||
///
|
||||
/// 在应用退出前调用,确保所有 span 被导出。
|
||||
/// 注意:在 OpenTelemetry 0.31+ 中,shutdown 需要在 TracerProvider 实例上调用。
|
||||
/// 此函数仅用于日志记录,实际 shutdown 由 TelemetryGuard 的 Drop 处理。
|
||||
pub fn shutdown_tracer_provider() {
|
||||
info!("[OTLP] TracerProvider shutdown request logged");
|
||||
// 注意:在 OpenTelemetry 0.31+ 中,全局 shutdown 函数已移除
|
||||
// TracerProvider 的 Drop 会自动处理清理
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sampler_always_on() {
|
||||
let config = OtlpConfig {
|
||||
sample_rate: 1.0,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(config.sample_rate >= 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sampler_always_off() {
|
||||
let config = OtlpConfig {
|
||||
sample_rate: 0.0,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(config.sample_rate <= 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sampler_ratio() {
|
||||
let config = OtlpConfig {
|
||||
sample_rate: 0.5,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(config.sample_rate > 0.0 && config.sample_rate < 1.0);
|
||||
}
|
||||
}
|
||||
239
qiming-rcoder/crates/rcoder-telemetry/src/prometheus.rs
Normal file
239
qiming-rcoder/crates/rcoder-telemetry/src/prometheus.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
//! Prometheus 指标模块
|
||||
//!
|
||||
//! 提供 Prometheus 指标的定义、记录和导出功能。
|
||||
//! 使用 `metrics` crate 作为 facade,`metrics-exporter-prometheus` 作为后端。
|
||||
|
||||
use metrics::{
|
||||
counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram, Unit,
|
||||
};
|
||||
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
|
||||
use std::time::Duration;
|
||||
use tracing::info;
|
||||
|
||||
// ============= 指标名称常量 =============
|
||||
|
||||
/// HTTP 请求总数
|
||||
pub const HTTP_REQUESTS_TOTAL: &str = "http_requests_total";
|
||||
/// HTTP 请求耗时
|
||||
pub const HTTP_REQUEST_DURATION_SECONDS: &str = "http_request_duration_seconds";
|
||||
|
||||
/// gRPC 请求总数
|
||||
pub const GRPC_REQUESTS_TOTAL: &str = "grpc_requests_total";
|
||||
/// gRPC 请求耗时
|
||||
pub const GRPC_REQUEST_DURATION_SECONDS: &str = "grpc_request_duration_seconds";
|
||||
|
||||
/// Agent 任务总数
|
||||
pub const AGENT_TASKS_TOTAL: &str = "agent_tasks_total";
|
||||
/// Agent 任务耗时
|
||||
pub const AGENT_TASK_DURATION_SECONDS: &str = "agent_task_duration_seconds";
|
||||
/// 活跃任务数
|
||||
pub const AGENT_ACTIVE_TASKS: &str = "agent_active_tasks";
|
||||
|
||||
// ============= 初始化 =============
|
||||
|
||||
/// Initializing Prometheus 指标系统
|
||||
///
|
||||
/// 安装 Prometheus recorder 并返回 handle,用于渲染指标。
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 返回 `PrometheusHandle`,可通过 `render()` 方法获取 Prometheus 格式的指标文本。
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use rcoder_telemetry::prometheus::init_prometheus;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let handle = init_prometheus().expect("Failed to init prometheus");
|
||||
/// let metrics_text = handle.render();
|
||||
/// println!("{}", metrics_text);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn init_prometheus() -> anyhow::Result<PrometheusHandle> {
|
||||
let handle = PrometheusBuilder::new()
|
||||
.install_recorder()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to install Prometheus recorder: {}", e))?;
|
||||
|
||||
// 注册指标描述
|
||||
register_metric_descriptions();
|
||||
|
||||
info!("[Prometheus] Metrics system initialization completed");
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// 注册指标描述(元数据)
|
||||
fn register_metric_descriptions() {
|
||||
// HTTP 指标
|
||||
describe_counter!(
|
||||
HTTP_REQUESTS_TOTAL,
|
||||
Unit::Count,
|
||||
"Total number of HTTP requests"
|
||||
);
|
||||
describe_histogram!(
|
||||
HTTP_REQUEST_DURATION_SECONDS,
|
||||
Unit::Seconds,
|
||||
"HTTP request duration in seconds"
|
||||
);
|
||||
|
||||
// gRPC 指标
|
||||
describe_counter!(
|
||||
GRPC_REQUESTS_TOTAL,
|
||||
Unit::Count,
|
||||
"Total number of gRPC requests"
|
||||
);
|
||||
describe_histogram!(
|
||||
GRPC_REQUEST_DURATION_SECONDS,
|
||||
Unit::Seconds,
|
||||
"gRPC request duration in seconds"
|
||||
);
|
||||
|
||||
// Agent 指标
|
||||
describe_counter!(
|
||||
AGENT_TASKS_TOTAL,
|
||||
Unit::Count,
|
||||
"Total number of agent tasks"
|
||||
);
|
||||
describe_histogram!(
|
||||
AGENT_TASK_DURATION_SECONDS,
|
||||
Unit::Seconds,
|
||||
"Agent task duration in seconds"
|
||||
);
|
||||
describe_gauge!(
|
||||
AGENT_ACTIVE_TASKS,
|
||||
Unit::Count,
|
||||
"Current number of active agent tasks"
|
||||
);
|
||||
}
|
||||
|
||||
// ============= HTTP 指标 =============
|
||||
|
||||
/// 记录 HTTP 请求
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `method` - HTTP 方法(GET, POST 等)
|
||||
/// * `path` - 请求路径
|
||||
/// * `status` - 响应状态码
|
||||
pub fn record_http_request(method: &str, path: &str, status: u16) {
|
||||
counter!(
|
||||
HTTP_REQUESTS_TOTAL,
|
||||
"method" => method.to_string(),
|
||||
"path" => path.to_string(),
|
||||
"status" => status.to_string()
|
||||
)
|
||||
.increment(1);
|
||||
}
|
||||
|
||||
/// 记录 HTTP 请求耗时
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `method` - HTTP 方法
|
||||
/// * `path` - 请求路径
|
||||
/// * `duration` - 请求耗时
|
||||
pub fn record_http_duration(method: &str, path: &str, duration: Duration) {
|
||||
histogram!(
|
||||
HTTP_REQUEST_DURATION_SECONDS,
|
||||
"method" => method.to_string(),
|
||||
"path" => path.to_string()
|
||||
)
|
||||
.record(duration.as_secs_f64());
|
||||
}
|
||||
|
||||
// ============= gRPC 指标 =============
|
||||
|
||||
/// 记录 gRPC 请求
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `method` - gRPC 方法名(如 "Chat", "SubscribeProgress")
|
||||
/// * `status` - 状态("ok", "error")
|
||||
pub fn record_grpc_request(method: &str, status: &str) {
|
||||
counter!(
|
||||
GRPC_REQUESTS_TOTAL,
|
||||
"method" => method.to_string(),
|
||||
"status" => status.to_string()
|
||||
)
|
||||
.increment(1);
|
||||
}
|
||||
|
||||
/// 记录 gRPC 请求耗时
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `method` - gRPC 方法名
|
||||
/// * `duration` - 请求耗时
|
||||
pub fn record_grpc_duration(method: &str, duration: Duration) {
|
||||
histogram!(
|
||||
GRPC_REQUEST_DURATION_SECONDS,
|
||||
"method" => method.to_string()
|
||||
)
|
||||
.record(duration.as_secs_f64());
|
||||
}
|
||||
|
||||
// ============= Agent 指标 =============
|
||||
|
||||
/// 记录 Agent 任务
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_id` - 项目 ID
|
||||
/// * `status` - 任务状态("success", "error", "timeout")
|
||||
pub fn record_agent_task(project_id: &str, status: &str) {
|
||||
counter!(
|
||||
AGENT_TASKS_TOTAL,
|
||||
"project_id" => project_id.to_string(),
|
||||
"status" => status.to_string()
|
||||
)
|
||||
.increment(1);
|
||||
}
|
||||
|
||||
/// 记录 Agent 任务耗时
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_id` - 项目 ID
|
||||
/// * `duration` - 任务耗时
|
||||
pub fn record_agent_task_duration(project_id: &str, duration: Duration) {
|
||||
histogram!(
|
||||
AGENT_TASK_DURATION_SECONDS,
|
||||
"project_id" => project_id.to_string()
|
||||
)
|
||||
.record(duration.as_secs_f64());
|
||||
}
|
||||
|
||||
/// 设置活跃任务数
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `count` - 当前活跃任务数
|
||||
pub fn set_active_tasks(count: u64) {
|
||||
gauge!(AGENT_ACTIVE_TASKS).set(count as f64);
|
||||
}
|
||||
|
||||
/// 增加活跃任务数
|
||||
pub fn inc_active_tasks() {
|
||||
gauge!(AGENT_ACTIVE_TASKS).increment(1.0);
|
||||
}
|
||||
|
||||
/// 减少活跃任务数
|
||||
pub fn dec_active_tasks() {
|
||||
gauge!(AGENT_ACTIVE_TASKS).decrement(1.0);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// 注意:这些测试需要先初始化 metrics recorder
|
||||
// 在实际测试中,可能需要使用 mock recorder
|
||||
|
||||
#[test]
|
||||
fn test_metric_names() {
|
||||
assert_eq!(HTTP_REQUESTS_TOTAL, "http_requests_total");
|
||||
assert_eq!(GRPC_REQUESTS_TOTAL, "grpc_requests_total");
|
||||
assert_eq!(AGENT_ACTIVE_TASKS, "agent_active_tasks");
|
||||
}
|
||||
}
|
||||
212
qiming-rcoder/crates/rcoder-telemetry/src/propagation.rs
Normal file
212
qiming-rcoder/crates/rcoder-telemetry/src/propagation.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
//! Trace Context 传播模块
|
||||
//!
|
||||
//! 提供跨服务的 trace context 传播功能,支持 gRPC 和 HTTP。
|
||||
|
||||
use opentelemetry::propagation::{Extractor, Injector, TextMapPropagator};
|
||||
use opentelemetry::Context;
|
||||
use opentelemetry_sdk::propagation::TraceContextPropagator;
|
||||
use tonic::metadata::{MetadataKey, MetadataMap, MetadataValue};
|
||||
use tracing::debug;
|
||||
|
||||
/// gRPC MetadataMap 的 Injector 实现
|
||||
struct MetadataMapInjector<'a>(&'a mut MetadataMap);
|
||||
|
||||
impl Injector for MetadataMapInjector<'_> {
|
||||
fn set(&mut self, key: &str, value: String) {
|
||||
if let Ok(key) = MetadataKey::from_bytes(key.as_bytes()) {
|
||||
if let Ok(value) = MetadataValue::try_from(&value) {
|
||||
self.0.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// gRPC MetadataMap 的 Extractor 实现
|
||||
struct MetadataMapExtractor<'a>(&'a MetadataMap);
|
||||
|
||||
impl Extractor for MetadataMapExtractor<'_> {
|
||||
fn get(&self, key: &str) -> Option<&str> {
|
||||
self.0.get(key).and_then(|value| value.to_str().ok())
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<&str> {
|
||||
self.0
|
||||
.keys()
|
||||
.filter_map(|key| {
|
||||
if let tonic::metadata::KeyRef::Ascii(k) = key {
|
||||
Some(k.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// 注入 trace context 到 gRPC metadata
|
||||
///
|
||||
/// 将当前 span 的 trace context 注入到 gRPC metadata 中,
|
||||
/// 用于跨服务传播。
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `metadata` - gRPC metadata
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use tonic::metadata::MetadataMap;
|
||||
/// use rcoder_telemetry::propagation::inject_context;
|
||||
///
|
||||
/// let mut metadata = MetadataMap::new();
|
||||
/// inject_context(&mut metadata);
|
||||
/// // 现在 metadata 包含 traceparent 和 tracestate headers
|
||||
/// ```
|
||||
pub fn inject_context(metadata: &mut MetadataMap) {
|
||||
let propagator = TraceContextPropagator::new();
|
||||
let cx = Context::current();
|
||||
let mut injector = MetadataMapInjector(metadata);
|
||||
propagator.inject_context(&cx, &mut injector);
|
||||
|
||||
debug!("[Propagation] Trace context injected into gRPC metadata");
|
||||
}
|
||||
|
||||
/// Extracting trace context from gRPC metadata
|
||||
///
|
||||
/// 从 gRPC metadata 中提取 trace context,
|
||||
/// 用于继续跨服务的 trace。
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `metadata` - gRPC metadata
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 返回提取的 `Context`,如果没有找到则返回当前 context。
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use tonic::metadata::MetadataMap;
|
||||
/// use rcoder_telemetry::propagation::extract_context;
|
||||
///
|
||||
/// let metadata = MetadataMap::new();
|
||||
/// let context = extract_context(&metadata);
|
||||
/// // 使用 context 创建新的 span
|
||||
/// ```
|
||||
pub fn extract_context(metadata: &MetadataMap) -> Context {
|
||||
let propagator = TraceContextPropagator::new();
|
||||
let extractor = MetadataMapExtractor(metadata);
|
||||
let cx = propagator.extract(&extractor);
|
||||
|
||||
debug!("[Propagation] Extracting trace context from gRPC metadata");
|
||||
|
||||
cx
|
||||
}
|
||||
|
||||
/// HTTP Headers 的 Injector 实现
|
||||
pub struct HttpHeaderInjector<'a>(pub &'a mut http::HeaderMap);
|
||||
|
||||
impl Injector for HttpHeaderInjector<'_> {
|
||||
fn set(&mut self, key: &str, value: String) {
|
||||
if let Ok(name) = http::header::HeaderName::from_bytes(key.as_bytes()) {
|
||||
if let Ok(value) = http::header::HeaderValue::from_str(&value) {
|
||||
self.0.insert(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP Headers 的 Extractor 实现
|
||||
pub struct HttpHeaderExtractor<'a>(pub &'a http::HeaderMap);
|
||||
|
||||
impl Extractor for HttpHeaderExtractor<'_> {
|
||||
fn get(&self, key: &str) -> Option<&str> {
|
||||
self.0.get(key).and_then(|value| value.to_str().ok())
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<&str> {
|
||||
self.0.keys().map(|key| key.as_str()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// 注入 trace context 到 HTTP headers
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `headers` - HTTP headers
|
||||
pub fn inject_context_http(headers: &mut http::HeaderMap) {
|
||||
let propagator = TraceContextPropagator::new();
|
||||
let cx = Context::current();
|
||||
let mut injector = HttpHeaderInjector(headers);
|
||||
propagator.inject_context(&cx, &mut injector);
|
||||
|
||||
debug!("[Propagation] Trace context injected into HTTP headers");
|
||||
}
|
||||
|
||||
/// Extracting trace context from HTTP headers
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `headers` - HTTP headers
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// 返回提取的 `Context`
|
||||
pub fn extract_context_http(headers: &http::HeaderMap) -> Context {
|
||||
let propagator = TraceContextPropagator::new();
|
||||
let extractor = HttpHeaderExtractor(headers);
|
||||
let cx = propagator.extract(&extractor);
|
||||
|
||||
debug!("[Propagation] Extracting trace context from HTTP headers");
|
||||
|
||||
cx
|
||||
}
|
||||
|
||||
/// 设置全局 text map 传播器
|
||||
///
|
||||
/// 应该在应用启动时调用一次。
|
||||
pub fn set_global_propagator() {
|
||||
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
|
||||
debug!("[Propagation] Global TraceContextPropagator set");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use opentelemetry::trace::TraceContextExt;
|
||||
|
||||
#[test]
|
||||
fn test_metadata_injector_extractor() {
|
||||
let mut metadata = MetadataMap::new();
|
||||
|
||||
// 手动设置一些 metadata
|
||||
metadata.insert(
|
||||
"traceparent",
|
||||
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// 提取 context
|
||||
let cx = extract_context(&metadata);
|
||||
assert!(!cx.span().span_context().trace_id().to_string().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_header_injector_extractor() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
|
||||
// 手动设置 traceparent header
|
||||
headers.insert(
|
||||
"traceparent",
|
||||
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// 提取 context
|
||||
let cx = extract_context_http(&headers);
|
||||
assert!(!cx.span().span_context().trace_id().to_string().is_empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user