mirror of
https://github.com/CloudNebulaProject/webfingerd.git
synced 2026-04-10 13:10:41 +00:00
feat: add Prometheus metrics endpoint and query instrumentation
This commit is contained in:
parent
df3cc1eb91
commit
820a6410c4
6 changed files with 70 additions and 6 deletions
|
|
@ -1,11 +1,23 @@
|
|||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use sea_orm::{ConnectionTrait, Statement};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
async fn healthz() -> StatusCode {
|
||||
StatusCode::OK
|
||||
async fn healthz(State(state): State<AppState>) -> StatusCode {
|
||||
match state
|
||||
.db
|
||||
.execute(Statement::from_string(
|
||||
sea_orm::DatabaseBackend::Sqlite,
|
||||
"SELECT 1".to_string(),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(_) => StatusCode::OK,
|
||||
Err(_) => StatusCode::SERVICE_UNAVAILABLE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
|
|
|
|||
13
src/handler/metrics.rs
Normal file
13
src/handler/metrics.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use axum::extract::State;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
async fn metrics(State(state): State<AppState>) -> String {
|
||||
state.metrics_handle.render()
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new().route("/metrics", get(metrics))
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ pub mod domains;
|
|||
mod health;
|
||||
mod host_meta;
|
||||
pub mod links;
|
||||
mod metrics;
|
||||
pub mod tokens;
|
||||
mod webfinger;
|
||||
|
||||
|
|
@ -45,5 +46,6 @@ pub fn router(state: AppState) -> Router {
|
|||
.merge(public_routes)
|
||||
.merge(api_routes)
|
||||
.merge(health::router())
|
||||
.merge(metrics::router())
|
||||
.with_state(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,15 +35,36 @@ async fn webfinger(
|
|||
State(state): State<AppState>,
|
||||
uri: Uri,
|
||||
) -> AppResult<Response> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let (resource_opt, rels) = parse_webfinger_query(&uri);
|
||||
|
||||
let resource = resource_opt
|
||||
.ok_or_else(|| AppError::BadRequest("missing resource parameter".into()))?;
|
||||
|
||||
let cached = state
|
||||
.cache
|
||||
.get(&resource)
|
||||
.ok_or(AppError::NotFound)?;
|
||||
// Extract domain from resource for metrics labeling
|
||||
let resource_domain = resource
|
||||
.split('@')
|
||||
.nth(1)
|
||||
.or_else(|| {
|
||||
// Handle URI-style resources like https://domain/path
|
||||
resource.split("://").nth(1).and_then(|s| s.split('/').next())
|
||||
})
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let cached = match state.cache.get(&resource) {
|
||||
Some(c) => {
|
||||
metrics::counter!("webfinger_queries_total", "domain" => resource_domain.clone(), "status" => "hit").increment(1);
|
||||
c
|
||||
}
|
||||
None => {
|
||||
metrics::counter!("webfinger_queries_total", "domain" => resource_domain.clone(), "status" => "miss").increment(1);
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
metrics::histogram!("webfinger_query_duration_seconds").record(elapsed);
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
};
|
||||
|
||||
let links: Vec<serde_json::Value> = cached
|
||||
.links
|
||||
|
|
@ -94,6 +115,9 @@ async fn webfinger(
|
|||
|
||||
response_body.insert("links".into(), json!(links));
|
||||
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
metrics::histogram!("webfinger_query_duration_seconds").record(elapsed);
|
||||
|
||||
Ok((
|
||||
[
|
||||
(header::CONTENT_TYPE, "application/jrd+json"),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use metrics_exporter_prometheus::PrometheusHandle;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -11,4 +12,5 @@ pub struct AppState {
|
|||
pub cache: Cache,
|
||||
pub settings: Arc<Settings>,
|
||||
pub challenge_verifier: Arc<dyn ChallengeVerifier>,
|
||||
pub metrics_handle: PrometheusHandle,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use metrics_exporter_prometheus::PrometheusBuilder;
|
||||
use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection, Statement};
|
||||
use sea_orm_migration::MigratorTrait;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -57,10 +58,20 @@ pub async fn test_state_with_settings(settings: Settings) -> AppState {
|
|||
let db = setup_test_db().await;
|
||||
let cache = Cache::new();
|
||||
cache.hydrate(&db).await.unwrap();
|
||||
// Each test gets its own metrics recorder. If install_recorder fails because
|
||||
// another test already installed one, build a standalone recorder and grab its handle.
|
||||
let metrics_handle = PrometheusBuilder::new()
|
||||
.install_recorder()
|
||||
.unwrap_or_else(|_| {
|
||||
let recorder = PrometheusBuilder::new().build_recorder();
|
||||
recorder.handle()
|
||||
});
|
||||
|
||||
AppState {
|
||||
db,
|
||||
cache,
|
||||
settings: Arc::new(settings),
|
||||
challenge_verifier: Arc::new(webfingerd::challenge::MockChallengeVerifier),
|
||||
metrics_handle,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue