- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin) - Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs) - Integrated erp-diary into workspace and erp-server - Added DiaryModule registration in main.rs - Added DiaryState FromRef in state.rs - Diary routes mounted (empty routes, ready for implementation) - Product design spec v1.2 preserved in docs/ - Implementation plan preserved in plans/ Cargo check: OK Cargo test: OK (78+ base tests passing)
127 lines
3.9 KiB
Rust
127 lines
3.9 KiB
Rust
use axum::extract::Request;
|
|
use axum::http::Method;
|
|
use axum::middleware::Next;
|
|
use axum::response::{IntoResponse, Response};
|
|
use metrics::{counter, histogram};
|
|
use std::time::Instant;
|
|
|
|
/// HTTP 请求指标中间件。
|
|
///
|
|
/// 记录两个 Prometheus 指标:
|
|
/// - `http_requests_total` — 计数器,标签: method, path, status
|
|
/// - `http_request_duration_seconds` — 直方图,标签: method, path, status
|
|
pub async fn metrics_middleware(req: Request, next: Next) -> Response {
|
|
let method = method_label(req.method());
|
|
let path = path_label(req.uri().path());
|
|
|
|
let start = Instant::now();
|
|
let resp = next.run(req).await;
|
|
let elapsed = start.elapsed();
|
|
|
|
let status = resp.status().as_u16().to_string();
|
|
|
|
let labels = [
|
|
("method", method.clone()),
|
|
("path", path.clone()),
|
|
("status", status.clone()),
|
|
];
|
|
|
|
counter!("http_requests_total", &labels).increment(1);
|
|
histogram!("http_request_duration_seconds", &labels).record(elapsed.as_secs_f64());
|
|
|
|
resp
|
|
}
|
|
|
|
fn method_label(method: &Method) -> String {
|
|
method.as_str().to_owned()
|
|
}
|
|
|
|
/// 归一化路径:将 UUID 段替换为 `:id`,避免高基数。
|
|
fn path_label(path: &str) -> String {
|
|
let parts: Vec<&str> = path
|
|
.split('/')
|
|
.filter(|s| !s.is_empty())
|
|
.map(|s| if looks_like_uuid(s) { ":id" } else { s })
|
|
.collect();
|
|
if parts.is_empty() {
|
|
"/".to_string()
|
|
} else {
|
|
format!("/{}", parts.join("/"))
|
|
}
|
|
}
|
|
|
|
fn looks_like_uuid(s: &str) -> bool {
|
|
s.len() == 36
|
|
&& s.chars().filter(|c| *c == '-').count() == 4
|
|
&& s.chars().all(|c| c.is_ascii_hexdigit() || c == '-')
|
|
}
|
|
|
|
/// 在独立端口启动 Prometheus exporter。
|
|
pub fn start_metrics_server(port: u16) {
|
|
let builder = metrics_exporter_prometheus::PrometheusBuilder::new();
|
|
let recorder = builder.build_recorder();
|
|
let handle = recorder.handle();
|
|
|
|
if let Err(e) = metrics::set_global_recorder(recorder) {
|
|
tracing::error!(error = %e, "Failed to install Prometheus recorder");
|
|
return;
|
|
}
|
|
|
|
tokio::spawn(async move {
|
|
let app = axum::Router::new()
|
|
.route(
|
|
"/metrics",
|
|
axum::routing::get(move || {
|
|
let handle = handle.clone();
|
|
async move {
|
|
let body = handle.render();
|
|
axum::response::IntoResponse::into_response((
|
|
[(
|
|
axum::http::header::CONTENT_TYPE,
|
|
"text/plain; version=0.0.4",
|
|
)],
|
|
body,
|
|
))
|
|
}
|
|
}),
|
|
)
|
|
.fallback(|| async { axum::http::StatusCode::NOT_FOUND.into_response() as Response });
|
|
|
|
let addr = format!("0.0.0.0:{port}");
|
|
match tokio::net::TcpListener::bind(&addr).await {
|
|
Ok(listener) => {
|
|
tracing::info!(addr = %addr, "Prometheus metrics server listening");
|
|
if let Err(e) = axum::serve(listener, app).await {
|
|
tracing::error!(error = %e, "Metrics server error");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(error = %e, addr = %addr, "Failed to bind metrics server");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn path_label_normalizes_uuids() {
|
|
assert_eq!(path_label("/api/v1/users"), "/api/v1/users");
|
|
assert_eq!(
|
|
path_label("/api/v1/users/01234567-89ab-cdef-0123-456789abcdef/posts"),
|
|
"/api/v1/users/:id/posts"
|
|
);
|
|
assert_eq!(path_label("/"), "/");
|
|
assert_eq!(path_label(""), "/");
|
|
}
|
|
|
|
#[test]
|
|
fn is_uuid_checks_format() {
|
|
assert!(looks_like_uuid("01234567-89ab-cdef-0123-456789abcdef"));
|
|
assert!(!looks_like_uuid("not-a-uuid"));
|
|
assert!(!looks_like_uuid("short"));
|
|
}
|
|
}
|