use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering::Relaxed};
const LATENCY_BUCKETS_MS: [f64; 8] = [5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0];
#[derive(Default)]
pub(super) struct ServerMetrics {
req_tts: AtomicU64,
req_hiragana: AtomicU64,
req_ruby: AtomicU64,
req_kanji: AtomicU64,
req_romaji: AtomicU64,
req_romaji_kunrei: AtomicU64,
req_analyze: AtomicU64,
latency_buckets: [AtomicU64; 9],
latency_sum_ms: AtomicU64,
latency_count: AtomicU64,
auth_failures: AtomicU64,
rate_limited: AtomicU64,
slow_requests: AtomicU64,
failed_resolution: AtomicU64,
reloads: AtomicU64,
dict_size: AtomicUsize,
}
impl ServerMetrics {
pub(super) fn record_request(&self, mode: &str, total_ms: f64) {
let counter = match mode {
"tts" => &self.req_tts,
"hiragana" => &self.req_hiragana,
"ruby" => &self.req_ruby,
"kanji" => &self.req_kanji,
"romaji" => &self.req_romaji,
"romaji-kunrei" => &self.req_romaji_kunrei,
"analyze" => &self.req_analyze,
_ => return,
};
counter.fetch_add(1, Relaxed);
for (i, b) in LATENCY_BUCKETS_MS.iter().enumerate() {
if total_ms <= *b {
self.latency_buckets[i].fetch_add(1, Relaxed);
}
}
self.latency_buckets[8].fetch_add(1, Relaxed); self.latency_sum_ms
.fetch_add(total_ms.round() as u64, Relaxed);
self.latency_count.fetch_add(1, Relaxed);
}
pub(super) fn record_auth_failure(&self) {
self.auth_failures.fetch_add(1, Relaxed);
}
pub(super) fn record_rate_limited(&self) {
self.rate_limited.fetch_add(1, Relaxed);
}
pub(super) fn record_slow_request(&self) {
self.slow_requests.fetch_add(1, Relaxed);
}
pub(super) fn record_failed_resolution(&self) {
self.failed_resolution.fetch_add(1, Relaxed);
}
pub(super) fn record_reload(&self, new_dict_size: usize) {
self.reloads.fetch_add(1, Relaxed);
self.dict_size.store(new_dict_size, Relaxed);
}
pub(super) fn set_dict_size(&self, size: usize) {
self.dict_size.store(size, Relaxed);
}
pub(super) fn render(&self) -> String {
let mut out = String::with_capacity(2048);
out.push_str("# HELP furigana_requests_total Total requests served, partitioned by mode\n");
out.push_str("# TYPE furigana_requests_total counter\n");
for (mode, c) in [
("tts", &self.req_tts),
("hiragana", &self.req_hiragana),
("ruby", &self.req_ruby),
("kanji", &self.req_kanji),
("romaji", &self.req_romaji),
("romaji-kunrei", &self.req_romaji_kunrei),
("analyze", &self.req_analyze),
] {
out.push_str(&format!(
"furigana_requests_total{{mode=\"{}\"}} {}\n",
mode,
c.load(Relaxed)
));
}
out.push_str(
"# HELP furigana_request_duration_ms Request total latency (process() 内の total_ms)\n",
);
out.push_str("# TYPE furigana_request_duration_ms histogram\n");
for (i, b) in LATENCY_BUCKETS_MS.iter().enumerate() {
out.push_str(&format!(
"furigana_request_duration_ms_bucket{{le=\"{}\"}} {}\n",
b,
self.latency_buckets[i].load(Relaxed)
));
}
out.push_str(&format!(
"furigana_request_duration_ms_bucket{{le=\"+Inf\"}} {}\n",
self.latency_buckets[8].load(Relaxed)
));
out.push_str(&format!(
"furigana_request_duration_ms_sum {}\n",
self.latency_sum_ms.load(Relaxed)
));
out.push_str(&format!(
"furigana_request_duration_ms_count {}\n",
self.latency_count.load(Relaxed)
));
for (name, help, value) in [
(
"furigana_auth_failures_total",
"Authentication failures (= token mismatch)",
self.auth_failures.load(Relaxed),
),
(
"furigana_rate_limited_total",
"Requests rejected by rate limiter (= HTTP 429)",
self.rate_limited.load(Relaxed),
),
(
"furigana_slow_requests_total",
"Requests whose total_ms exceeded slow threshold (= 100ms default)",
self.slow_requests.load(Relaxed),
),
(
"furigana_failed_resolution_total",
"Requests whose result indicated reading resolution failure",
self.failed_resolution.load(Relaxed),
),
(
"furigana_reloads_total",
"Dictionary reload events (SIGHUP / admin / auto_update / startup)",
self.reloads.load(Relaxed),
),
] {
out.push_str(&format!("# HELP {} {}\n", name, help));
out.push_str(&format!("# TYPE {} counter\n", name));
out.push_str(&format!("{} {}\n", name, value));
}
out.push_str("# HELP furigana_dict_size Current dictionary size (entries)\n");
out.push_str("# TYPE furigana_dict_size gauge\n");
out.push_str(&format!(
"furigana_dict_size {}\n",
self.dict_size.load(Relaxed)
));
out
}
}