添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

View 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"] }

View 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.0100% 采样)
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` - 是否启用 Prometheustrue/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"));
}
}

View 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());
}
}

View 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());
}
}

View 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"
);
}
}

View File

@@ -0,0 +1,9 @@
//! 遥测中间件模块
//!
//! 提供 HTTP 和 gRPC 的指标和追踪中间件。
pub mod grpc;
pub mod http;
pub use grpc::GrpcMetricsInterceptor;
pub use http::HttpMetricsLayer;

View 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);
}
}

View 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");
}
}

View 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());
}
}