Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 116 additions & 4 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
tokio = { version = "1.0", features = ["full"] }
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
axum = { version = "0.8", features = ["json", "multipart", "macros", "ws"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["cors", "trace", "request-id", "util"] }
Expand All @@ -28,4 +29,5 @@ jsonwebtoken = "9.0"
base64 = "0.21"
stellar-strkey = "0.0.8"
ed25519-dalek = { version = "2.1", features = ["pkcs8", "rand_core"] }
redis = { version = "0.27", features = ["tokio-comp"] }
prometheus = { version = "0.13", features = ["process"] }
once_cell = "1.19"
3 changes: 3 additions & 0 deletions backend/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use uuid::Uuid;
use crate::auth::signature_auth_middleware;
use crate::cache::PlanCache;
use crate::kyc_webhook::kyc_webhook_handler;
use crate::metrics::{latency_middleware, metrics_handler};
use crate::stellar_anchor::AnchorRegistry;
use crate::ws::{ws_handler, KycUpdateEvent};
use crate::yield_calculator;
Expand Down Expand Up @@ -136,6 +137,8 @@ pub fn create_router(state: Arc<AppState>) -> Router {
Router::new()
.merge(user_routes)
.merge(public_routes)
.route("/metrics", get(metrics_handler))
.layer(from_fn(latency_middleware))
.layer(cors)
.with_state(state)
}
Expand Down
1 change: 1 addition & 0 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod config;
pub mod db;
pub mod inactivity_watchdog;
pub mod kyc_webhook;
pub mod metrics;
pub mod stellar_anchor;
pub mod telemetry;
pub mod ws;
Expand Down
17 changes: 16 additions & 1 deletion backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use inheritx_backend::{
create_router, telemetry, AppState, Config, DbManager, InactivityWatchdogConfig,
create_router, metrics, telemetry, AppState, Config, DbManager, InactivityWatchdogConfig,
InactivityWatchdogService,
};
use std::net::SocketAddr;
Expand All @@ -11,6 +11,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing logging
telemetry::init_tracing()?;

// Initialize Prometheus metrics
metrics::init();

//loading the .env
dotenvy::dotenv().ok();

Expand Down Expand Up @@ -65,6 +68,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
));
inactivity_watchdog.start();

// Periodically refresh DB pool metrics
{
let pool = db_pool.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(15));
loop {
interval.tick().await;
metrics::update_db_pool_metrics(&pool);
}
});
}

// Create Axum application
let app = create_router(state);

Expand Down
104 changes: 104 additions & 0 deletions backend/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use axum::{extract::Request, http::StatusCode, middleware::Next, response::IntoResponse};
use once_cell::sync::Lazy;
use prometheus::{
histogram_opts, opts, register_gauge, register_histogram_vec, Encoder, Gauge, HistogramVec,
TextEncoder,
};
use std::time::Instant;

/// Tracks number of in-flight HTTP connections.
pub static ACTIVE_CONNECTIONS: Lazy<Gauge> = Lazy::new(|| {
register_gauge!(opts!(
"inheritx_active_connections",
"Number of currently active HTTP connections"
))
.expect("failed to register active_connections gauge")
});

/// Per-route request latency histogram (seconds).
/// Labels: method, path, status
pub static REQUEST_LATENCY: Lazy<HistogramVec> = Lazy::new(|| {
register_histogram_vec!(
histogram_opts!(
"inheritx_http_request_duration_seconds",
"HTTP request latency in seconds",
// Buckets tuned for low-latency API; p95/p99 computed by Prometheus from these.
vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
),
&["method", "path", "status"]
)
.expect("failed to register request_latency histogram")
});

/// DB pool size (total connections in the pool).
pub static DB_POOL_SIZE: Lazy<Gauge> = Lazy::new(|| {
register_gauge!(opts!(
"inheritx_db_pool_size",
"Total connections in the DB pool"
))
.expect("failed to register db_pool_size gauge")
});

/// DB pool idle connections.
pub static DB_POOL_IDLE: Lazy<Gauge> = Lazy::new(|| {
register_gauge!(opts!(
"inheritx_db_pool_idle",
"Idle connections in the DB pool"
))
.expect("failed to register db_pool_idle gauge")
});

/// Call once at startup to force lazy initialization of all metrics.
pub fn init() {
Lazy::force(&ACTIVE_CONNECTIONS);
Lazy::force(&REQUEST_LATENCY);
Lazy::force(&DB_POOL_SIZE);
Lazy::force(&DB_POOL_IDLE);
}

/// Updates DB pool gauges from the current sqlx pool state.
pub fn update_db_pool_metrics(pool: &sqlx::PgPool) {
DB_POOL_SIZE.set(pool.size() as f64);
DB_POOL_IDLE.set(pool.num_idle() as f64);
}

/// GET /metrics — Prometheus text exposition.
pub async fn metrics_handler() -> impl IntoResponse {
let encoder = TextEncoder::new();
let metric_families = prometheus::gather();
let mut buf = Vec::new();
if encoder.encode(&metric_families, &mut buf).is_err() {
return (StatusCode::INTERNAL_SERVER_ERROR, "encoding error").into_response();
}
(
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, encoder.format_type())],
buf,
)
.into_response()
}

/// Axum middleware: tracks active connections and records per-route latency.
pub async fn latency_middleware(req: Request, next: Next) -> impl IntoResponse {
ACTIVE_CONNECTIONS.inc();

let method = req.method().to_string();
// Use the matched path pattern (e.g. "/api/plans") to avoid high cardinality.
let path = req
.extensions()
.get::<axum::extract::MatchedPath>()
.map(|p| p.as_str().to_owned())
.unwrap_or_else(|| req.uri().path().to_owned());

let start = Instant::now();
let response = next.run(req).await;
let elapsed = start.elapsed().as_secs_f64();

let status = response.status().as_u16().to_string();
REQUEST_LATENCY
.with_label_values(&[&method, &path, &status])
.observe(elapsed);

ACTIVE_CONNECTIONS.dec();
response
}
Loading