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::http::StatusCode;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
use sea_orm::{ConnectionTrait, Statement};
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
async fn healthz() -> StatusCode {
|
async fn healthz(State(state): State<AppState>) -> StatusCode {
|
||||||
StatusCode::OK
|
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> {
|
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 health;
|
||||||
mod host_meta;
|
mod host_meta;
|
||||||
pub mod links;
|
pub mod links;
|
||||||
|
mod metrics;
|
||||||
pub mod tokens;
|
pub mod tokens;
|
||||||
mod webfinger;
|
mod webfinger;
|
||||||
|
|
||||||
|
|
@ -45,5 +46,6 @@ pub fn router(state: AppState) -> Router {
|
||||||
.merge(public_routes)
|
.merge(public_routes)
|
||||||
.merge(api_routes)
|
.merge(api_routes)
|
||||||
.merge(health::router())
|
.merge(health::router())
|
||||||
|
.merge(metrics::router())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,15 +35,36 @@ async fn webfinger(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
) -> AppResult<Response> {
|
) -> AppResult<Response> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
let (resource_opt, rels) = parse_webfinger_query(&uri);
|
let (resource_opt, rels) = parse_webfinger_query(&uri);
|
||||||
|
|
||||||
let resource = resource_opt
|
let resource = resource_opt
|
||||||
.ok_or_else(|| AppError::BadRequest("missing resource parameter".into()))?;
|
.ok_or_else(|| AppError::BadRequest("missing resource parameter".into()))?;
|
||||||
|
|
||||||
let cached = state
|
// Extract domain from resource for metrics labeling
|
||||||
.cache
|
let resource_domain = resource
|
||||||
.get(&resource)
|
.split('@')
|
||||||
.ok_or(AppError::NotFound)?;
|
.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
|
let links: Vec<serde_json::Value> = cached
|
||||||
.links
|
.links
|
||||||
|
|
@ -94,6 +115,9 @@ async fn webfinger(
|
||||||
|
|
||||||
response_body.insert("links".into(), json!(links));
|
response_body.insert("links".into(), json!(links));
|
||||||
|
|
||||||
|
let elapsed = start.elapsed().as_secs_f64();
|
||||||
|
metrics::histogram!("webfinger_query_duration_seconds").record(elapsed);
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
[
|
[
|
||||||
(header::CONTENT_TYPE, "application/jrd+json"),
|
(header::CONTENT_TYPE, "application/jrd+json"),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use metrics_exporter_prometheus::PrometheusHandle;
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -11,4 +12,5 @@ pub struct AppState {
|
||||||
pub cache: Cache,
|
pub cache: Cache,
|
||||||
pub settings: Arc<Settings>,
|
pub settings: Arc<Settings>,
|
||||||
pub challenge_verifier: Arc<dyn ChallengeVerifier>,
|
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::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection, Statement};
|
||||||
use sea_orm_migration::MigratorTrait;
|
use sea_orm_migration::MigratorTrait;
|
||||||
use std::sync::Arc;
|
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 db = setup_test_db().await;
|
||||||
let cache = Cache::new();
|
let cache = Cache::new();
|
||||||
cache.hydrate(&db).await.unwrap();
|
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 {
|
AppState {
|
||||||
db,
|
db,
|
||||||
cache,
|
cache,
|
||||||
settings: Arc::new(settings),
|
settings: Arc::new(settings),
|
||||||
challenge_verifier: Arc::new(webfingerd::challenge::MockChallengeVerifier),
|
challenge_verifier: Arc::new(webfingerd::challenge::MockChallengeVerifier),
|
||||||
|
metrics_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue