use std::sync::OnceLock;
use prometheus::{HistogramVec, register_histogram_vec};
use crate::error::LlmErrorKind;
fn call_duration() -> &'static HistogramVec {
static V: OnceLock<HistogramVec> = OnceLock::new();
V.get_or_init(|| {
register_histogram_vec!(
"polychrome_llm_call_duration_seconds",
"LLM provider call latency (dispatch to stream-open or failure), by outcome.",
&["outcome"]
)
.expect("register polychrome_llm_call_duration_seconds")
})
}
pub(crate) fn record_call(outcome: Result<(), LlmErrorKind>, elapsed: std::time::Duration) {
let label = match outcome {
Ok(()) => "ok",
Err(kind) => kind_label(kind),
};
call_duration()
.with_label_values(&[label])
.observe(elapsed.as_secs_f64());
}
pub(crate) fn force() {
for outcome in [
"ok",
"rate_limit",
"timeout",
"unavailable",
"auth",
"bad_request",
"ambiguous",
"other",
] {
call_duration().with_label_values(&[outcome]);
}
}
const fn kind_label(kind: LlmErrorKind) -> &'static str {
match kind {
LlmErrorKind::RateLimit => "rate_limit",
LlmErrorKind::Timeout => "timeout",
LlmErrorKind::Unavailable => "unavailable",
LlmErrorKind::Auth => "auth",
LlmErrorKind::BadRequest => "bad_request",
LlmErrorKind::Ambiguous => "ambiguous",
LlmErrorKind::Other => "other",
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use std::time::Duration;
use prometheus::{Encoder as _, TextEncoder};
use super::record_call;
use crate::error::LlmErrorKind;
#[test]
fn record_call_is_visible_in_a_registry_scrape() {
record_call(Ok(()), Duration::from_millis(5));
record_call(Err(LlmErrorKind::RateLimit), Duration::from_millis(1));
let mut buf = Vec::new();
TextEncoder::new()
.encode(&prometheus::default_registry().gather(), &mut buf)
.expect("encode");
let text = String::from_utf8(buf).expect("utf8");
assert!(
text.contains("polychrome_llm_call_duration_seconds_bucket"),
"missing histogram buckets in scrape:\n{text}"
);
assert!(text.contains("outcome=\"ok\""));
assert!(text.contains("outcome=\"rate_limit\""));
}
}