use crate::console::ConsoleState;
use crate::console::middleware::AuthRequired;
use axum::{Json, extract::State, http::HeaderMap, response::Response};
use serde_json::json;
use std::sync::Arc;
pub async fn metrics_page(
State(state): State<Arc<ConsoleState>>,
headers: HeaderMap,
AuthRequired(_): AuthRequired,
) -> Response {
let runtime_metrics = collect_runtime_metrics(&state);
let prometheus_config = get_prometheus_config(&state);
state.render_with_headers(
"console/metrics.html",
json!({
"nav_active": "metrics",
"metrics": runtime_metrics,
"prometheus": prometheus_config,
}),
&headers,
)
}
#[derive(Clone, serde::Serialize)]
struct PrometheusConfig {
pub enabled: bool,
pub path: String,
pub auth_required: bool,
}
fn get_prometheus_config(state: &ConsoleState) -> PrometheusConfig {
if let Some(app_state) = state.app_state() {
let cfg = app_state.config().metrics.clone().unwrap_or_default();
return PrometheusConfig {
enabled: cfg.enabled,
path: cfg.path,
auth_required: cfg.token.is_some(),
};
}
PrometheusConfig {
enabled: false,
path: "/metrics".to_string(),
auth_required: false,
}
}
pub async fn metrics_data(
State(state): State<Arc<ConsoleState>>,
AuthRequired(_): AuthRequired,
) -> Json<RuntimeMetrics> {
Json(collect_runtime_metrics(&state))
}
#[derive(Clone, serde::Serialize)]
pub struct RuntimeMetrics {
pub system: SystemMetrics,
pub sip: SipMetrics,
pub calls: CallMetrics,
pub media: MediaMetrics,
pub voicemail: VoicemailMetrics,
pub collected_at: String,
}
#[derive(Clone, serde::Serialize)]
pub struct SystemMetrics {
pub uptime_seconds: i64,
pub version: String,
pub edition: String,
}
#[derive(Clone, serde::Serialize, Default)]
pub struct SipMetrics {
pub registrations_active: u32,
pub registrations_total: u64,
pub registrations_succeeded: u64,
pub registrations_failed: u64,
pub dialogs_active: u32,
}
#[derive(Clone, serde::Serialize, Default)]
pub struct CallMetrics {
pub active: u32,
pub capacity: u32,
pub utilization: u32,
}
#[derive(Clone, serde::Serialize, Default)]
pub struct MediaMetrics {
pub webrtc_connections: u64,
}
#[derive(Clone, serde::Serialize, Default)]
pub struct VoicemailMetrics {
pub messages_today: u64,
pub active_mailboxes: u32,
}
fn collect_runtime_metrics(state: &ConsoleState) -> RuntimeMetrics {
let system = collect_system_metrics(state);
let sip = collect_sip_metrics(state);
let calls = collect_call_metrics(state);
let media = MediaMetrics::default();
let voicemail = VoicemailMetrics::default();
RuntimeMetrics {
system,
sip,
calls,
media,
voicemail,
collected_at: chrono::Utc::now().to_rfc3339(),
}
}
fn collect_system_metrics(state: &ConsoleState) -> SystemMetrics {
let uptime_seconds = state
.app_state()
.map(|s| (chrono::Utc::now() - s.uptime).num_seconds())
.unwrap_or(0);
let version = crate::version::get_short_version().to_string();
let edition = if cfg!(feature = "commerce") {
"commerce".to_string()
} else {
"community".to_string()
};
SystemMetrics {
uptime_seconds,
version,
edition,
}
}
fn collect_sip_metrics(state: &ConsoleState) -> SipMetrics {
let mut metrics = SipMetrics::default();
if let Some(server) = state.sip_server() {
metrics.dialogs_active = server.active_call_registry.count() as u32;
}
metrics
}
fn collect_call_metrics(state: &ConsoleState) -> CallMetrics {
let mut metrics = CallMetrics::default();
if let Some(server) = state.sip_server() {
metrics.active = server.active_call_registry.count() as u32;
metrics.capacity = server.proxy_config.max_concurrency.unwrap_or(0) as u32;
}
metrics.utilization = if metrics.capacity > 0 {
((metrics.active as f64 / metrics.capacity as f64) * 100.0).round() as u32
} else {
0
};
metrics
}
pub fn urls() -> axum::Router<Arc<ConsoleState>> {
use axum::routing::get;
axum::Router::new()
.route("/metrics/runtime", get(metrics_page))
.route("/metrics/runtime/data", get(metrics_data))
}