use std::sync::OnceLock;
use prometheus::{
Counter, CounterVec, Encoder, Gauge, Histogram, HistogramOpts, HistogramVec, Opts, Registry,
TextEncoder,
};
static REGISTRY: OnceLock<Registry> = OnceLock::new();
static METRICS: OnceLock<ExpectMetrics> = OnceLock::new();
fn registry() -> &'static Registry {
REGISTRY.get_or_init(Registry::new)
}
#[derive(Debug, Clone)]
pub struct ExpectMetrics {
sessions_active: Gauge,
sessions_total: Counter,
session_duration: Histogram,
bytes_sent_total: Counter,
bytes_received_total: Counter,
expect_total: CounterVec,
expect_duration: HistogramVec,
expect_timeouts: Counter,
commands_total: Counter,
command_duration: Histogram,
errors_total: CounterVec,
dialogs_total: Counter,
dialog_duration: Histogram,
dialog_steps: Histogram,
}
impl ExpectMetrics {
pub fn global() -> &'static Self {
METRICS.get_or_init(|| Self::new(registry()).expect("Failed to register metrics"))
}
pub fn new(registry: &Registry) -> Result<Self, prometheus::Error> {
let sessions_active = Gauge::with_opts(Opts::new(
"expect_sessions_active",
"Number of currently active expect sessions",
))?;
registry.register(Box::new(sessions_active.clone()))?;
let sessions_total = Counter::with_opts(Opts::new(
"expect_sessions_total",
"Total number of expect sessions started",
))?;
registry.register(Box::new(sessions_total.clone()))?;
let session_duration = Histogram::with_opts(
HistogramOpts::new(
"expect_session_duration_seconds",
"Duration of expect sessions in seconds",
)
.buckets(vec![0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0, 600.0]),
)?;
registry.register(Box::new(session_duration.clone()))?;
let bytes_sent_total = Counter::with_opts(Opts::new(
"expect_bytes_sent_total",
"Total bytes sent to sessions",
))?;
registry.register(Box::new(bytes_sent_total.clone()))?;
let bytes_received_total = Counter::with_opts(Opts::new(
"expect_bytes_received_total",
"Total bytes received from sessions",
))?;
registry.register(Box::new(bytes_received_total.clone()))?;
let expect_total = CounterVec::new(
Opts::new("expect_operations_total", "Total expect operations"),
&["status"],
)?;
registry.register(Box::new(expect_total.clone()))?;
let expect_duration = HistogramVec::new(
HistogramOpts::new(
"expect_operation_duration_seconds",
"Duration of expect operations in seconds",
)
.buckets(vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0]),
&["pattern_type"],
)?;
registry.register(Box::new(expect_duration.clone()))?;
let expect_timeouts = Counter::with_opts(Opts::new(
"expect_timeouts_total",
"Total number of expect timeout errors",
))?;
registry.register(Box::new(expect_timeouts.clone()))?;
let commands_total = Counter::with_opts(Opts::new(
"expect_commands_total",
"Total commands sent to sessions",
))?;
registry.register(Box::new(commands_total.clone()))?;
let command_duration = Histogram::with_opts(
HistogramOpts::new(
"expect_command_duration_seconds",
"Duration of command execution in seconds",
)
.buckets(vec![0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0]),
)?;
registry.register(Box::new(command_duration.clone()))?;
let errors_total = CounterVec::new(
Opts::new("expect_errors_total", "Total errors by type"),
&["error_type"],
)?;
registry.register(Box::new(errors_total.clone()))?;
let dialogs_total =
Counter::with_opts(Opts::new("expect_dialogs_total", "Total dialog executions"))?;
registry.register(Box::new(dialogs_total.clone()))?;
let dialog_duration = Histogram::with_opts(
HistogramOpts::new(
"expect_dialog_duration_seconds",
"Duration of dialog execution in seconds",
)
.buckets(vec![0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0]),
)?;
registry.register(Box::new(dialog_duration.clone()))?;
let dialog_steps = Histogram::with_opts(
HistogramOpts::new("expect_dialog_steps", "Number of steps in executed dialogs")
.buckets(vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0]),
)?;
registry.register(Box::new(dialog_steps.clone()))?;
Ok(Self {
sessions_active,
sessions_total,
session_duration,
bytes_sent_total,
bytes_received_total,
expect_total,
expect_duration,
expect_timeouts,
commands_total,
command_duration,
errors_total,
dialogs_total,
dialog_duration,
dialog_steps,
})
}
pub fn session_started(&self) {
self.sessions_total.inc();
self.sessions_active.inc();
}
pub fn session_ended(&self, duration_seconds: f64) {
self.sessions_active.dec();
self.session_duration.observe(duration_seconds);
}
pub fn bytes_sent(&self, count: u64) {
self.bytes_sent_total.inc_by(count as f64);
}
pub fn bytes_received(&self, count: u64) {
self.bytes_received_total.inc_by(count as f64);
}
pub fn expect_succeeded(&self, pattern_type: &str, duration_seconds: f64) {
self.expect_total.with_label_values(&["success"]).inc();
self.expect_duration
.with_label_values(&[pattern_type])
.observe(duration_seconds);
}
pub fn expect_failed(&self, pattern_type: &str, duration_seconds: f64) {
self.expect_total.with_label_values(&["failure"]).inc();
self.expect_duration
.with_label_values(&[pattern_type])
.observe(duration_seconds);
}
pub fn expect_timeout(&self) {
self.expect_timeouts.inc();
self.expect_total.with_label_values(&["timeout"]).inc();
}
pub fn command_sent(&self) {
self.commands_total.inc();
}
pub fn command_completed(&self, duration_seconds: f64) {
self.command_duration.observe(duration_seconds);
}
pub fn error(&self, error_type: &str) {
self.errors_total.with_label_values(&[error_type]).inc();
}
pub fn io_error(&self) {
self.error("io");
}
pub fn timeout_error(&self) {
self.error("timeout");
}
pub fn pattern_error(&self) {
self.error("pattern");
}
pub fn eof_error(&self) {
self.error("eof");
}
pub fn dialog_started(&self, step_count: usize) {
self.dialogs_total.inc();
self.dialog_steps.observe(step_count as f64);
}
pub fn dialog_completed(&self, duration_seconds: f64) {
self.dialog_duration.observe(duration_seconds);
}
#[must_use]
pub fn active_sessions(&self) -> u64 {
self.sessions_active.get() as u64
}
#[must_use]
pub fn total_sessions(&self) -> u64 {
self.sessions_total.get() as u64
}
#[must_use]
pub fn total_bytes_sent(&self) -> u64 {
self.bytes_sent_total.get() as u64
}
#[must_use]
pub fn total_bytes_received(&self) -> u64 {
self.bytes_received_total.get() as u64
}
#[must_use]
pub fn total_timeouts(&self) -> u64 {
self.expect_timeouts.get() as u64
}
}
#[must_use]
pub fn gather_metrics() -> String {
let encoder = TextEncoder::new();
let metric_families = registry().gather();
let mut buffer = Vec::new();
encoder
.encode(&metric_families, &mut buffer)
.unwrap_or_default();
String::from_utf8(buffer).unwrap_or_default()
}
#[must_use]
pub fn global_registry() -> &'static Registry {
registry()
}
#[must_use]
pub fn new_registry() -> Registry {
Registry::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metrics_registration() {
let registry = new_registry();
let metrics = ExpectMetrics::new(®istry).expect("Failed to create metrics");
metrics.session_started();
metrics.bytes_sent(100);
metrics.expect_succeeded("string", 0.1);
metrics.error("test");
}
#[test]
fn session_tracking() {
let registry = new_registry();
let metrics = ExpectMetrics::new(®istry).unwrap();
assert_eq!(metrics.active_sessions(), 0);
assert_eq!(metrics.total_sessions(), 0);
metrics.session_started();
assert_eq!(metrics.active_sessions(), 1);
assert_eq!(metrics.total_sessions(), 1);
metrics.session_started();
assert_eq!(metrics.active_sessions(), 2);
assert_eq!(metrics.total_sessions(), 2);
metrics.session_ended(1.0);
assert_eq!(metrics.active_sessions(), 1);
assert_eq!(metrics.total_sessions(), 2);
}
#[test]
fn byte_counters() {
let registry = new_registry();
let metrics = ExpectMetrics::new(®istry).unwrap();
metrics.bytes_sent(100);
metrics.bytes_sent(50);
assert_eq!(metrics.total_bytes_sent(), 150);
metrics.bytes_received(200);
assert_eq!(metrics.total_bytes_received(), 200);
}
#[test]
fn gather_output() {
let registry = new_registry();
let metrics = ExpectMetrics::new(®istry).unwrap();
metrics.session_started();
metrics.bytes_sent(1000);
let encoder = TextEncoder::new();
let metric_families = registry.gather();
let mut buffer = Vec::new();
encoder.encode(&metric_families, &mut buffer).unwrap();
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("expect_sessions_total"));
assert!(output.contains("expect_bytes_sent_total"));
}
#[test]
fn error_types() {
let registry = new_registry();
let metrics = ExpectMetrics::new(®istry).unwrap();
metrics.io_error();
metrics.timeout_error();
metrics.pattern_error();
metrics.eof_error();
metrics.error("custom");
}
#[test]
fn dialog_metrics() {
let registry = new_registry();
let metrics = ExpectMetrics::new(®istry).unwrap();
metrics.dialog_started(5);
metrics.dialog_completed(2.5);
}
}