use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Instant;
use metrics_exporter_prometheus::PrometheusBuilder;
static METRICS_HANDLE: OnceLock<Arc<MetricsHandle>> = OnceLock::new();
pub struct MetricsHandle {
handle: metrics_exporter_prometheus::PrometheusHandle,
}
impl MetricsHandle {
pub fn render(&self) -> String {
self.handle.render()
}
}
pub fn init_metrics() -> Arc<MetricsHandle> {
METRICS_HANDLE
.get_or_init(|| {
let recorder = PrometheusBuilder::new().build_recorder();
let handle = recorder.handle();
metrics::set_global_recorder(recorder)
.expect("failed to install Prometheus metrics recorder");
describe_metrics();
Arc::new(MetricsHandle { handle })
})
.clone()
}
fn describe_metrics() {
metrics::describe_counter!(
"episteme_smells_detected_total",
metrics::Unit::Count,
"Total number of code smells detected by the analyze endpoint"
);
metrics::describe_counter!(
"episteme_searches_total",
metrics::Unit::Count,
"Total number of search queries executed"
);
metrics::describe_counter!(
"episteme_refactoring_suggestions_total",
metrics::Unit::Count,
"Total number of refactoring suggestions returned"
);
metrics::describe_histogram!(
"episteme_analysis_duration_seconds",
metrics::Unit::Seconds,
"Duration of code analysis requests"
);
metrics::describe_histogram!(
"episteme_search_duration_seconds",
metrics::Unit::Seconds,
"Duration of search requests"
);
metrics::describe_counter!(
"episteme_rate_limit_rejected_total",
metrics::Unit::Count,
"Total number of requests rejected by the rate limiter"
);
}
pub fn track_smell_detection(smell_id: &str, smell_name: &str) {
metrics::counter!(
"episteme_smells_detected_total",
"smell_id" => smell_id.to_owned(),
"smell_name" => smell_name.to_owned(),
)
.increment(1);
}
pub fn track_search(entity_type: &str, has_filter: bool) {
metrics::counter!(
"episteme_searches_total",
"entity_type" => entity_type.to_owned(),
"has_filter" => has_filter.to_string(),
)
.increment(1);
}
pub fn track_refactoring_suggestion(refactoring_id: &str) {
metrics::counter!(
"episteme_refactoring_suggestions_total",
"refactoring_id" => refactoring_id.to_owned(),
)
.increment(1);
}
pub fn track_rate_limit_rejected(path: &str) {
metrics::counter!(
"episteme_rate_limit_rejected_total",
"path" => path.to_owned(),
)
.increment(1);
}
pub struct AnalysisTimer {
start: Instant,
}
impl Default for AnalysisTimer {
fn default() -> Self {
Self::new()
}
}
impl AnalysisTimer {
pub fn new() -> Self {
Self {
start: Instant::now(),
}
}
}
impl Drop for AnalysisTimer {
fn drop(&mut self) {
let elapsed = self.start.elapsed().as_secs_f64();
metrics::histogram!("episteme_analysis_duration_seconds").record(elapsed);
}
}
pub struct SearchTimer {
start: Instant,
}
impl Default for SearchTimer {
fn default() -> Self {
Self::new()
}
}
impl SearchTimer {
pub fn new() -> Self {
Self {
start: Instant::now(),
}
}
}
impl Drop for SearchTimer {
fn drop(&mut self) {
let elapsed = self.start.elapsed().as_secs_f64();
metrics::histogram!("episteme_search_duration_seconds").record(elapsed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn init_metrics_does_not_panic() {
let _ = init_metrics();
}
#[test]
fn track_smell_detection_increments_without_panic() {
let _ = init_metrics();
track_smell_detection("SMELL-001", "god_class");
}
#[test]
fn track_search_increments_without_panic() {
let _ = init_metrics();
track_search("pattern", true);
}
#[test]
fn track_refactoring_suggestion_increments_without_panic() {
let _ = init_metrics();
track_refactoring_suggestion("RF-001");
}
#[test]
fn analysis_timer_records_on_drop() {
let _ = init_metrics();
{
let _timer = AnalysisTimer::new();
}
}
#[test]
fn search_timer_records_on_drop() {
let _ = init_metrics();
{
let _timer = SearchTimer::new();
}
}
}