use once_cell::sync::Lazy;
use prometheus::{
Encoder, HistogramOpts, HistogramVec, IntCounterVec, IntGauge, Opts, TextEncoder,
register_histogram_vec, register_int_counter_vec, register_int_gauge,
};
use std::time::Instant;
pub static OPERATION_LATENCY: Lazy<Option<HistogramVec>> = Lazy::new(|| {
register_histogram_vec_resilient(
"stateset_operation_duration_milliseconds",
"Duration of commerce operations in milliseconds",
&["operation", "domain"],
vec![0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0, 5000.0],
)
});
pub static OPERATIONS_TOTAL: Lazy<Option<IntCounterVec>> = Lazy::new(|| {
register_int_counter_vec_resilient(
"stateset_operations_total",
"Total number of commerce operations",
&["operation", "domain"],
)
});
pub static OPERATIONS_FAILED: Lazy<Option<IntCounterVec>> = Lazy::new(|| {
register_int_counter_vec_resilient(
"stateset_operations_failed_total",
"Total number of failed commerce operations",
&["operation", "domain"],
)
});
pub static ACTIVE_RESERVATIONS: Lazy<Option<IntGauge>> = Lazy::new(|| {
register_int_gauge_resilient(
"stateset_inventory_active_reservations",
"Total number of active inventory reservations",
)
});
fn register_histogram_vec_resilient(
name: &str,
help: &str,
labels: &[&str],
buckets: Vec<f64>,
) -> Option<HistogramVec> {
match register_histogram_vec!(name, help, labels, buckets.clone()) {
Ok(metric) => Some(metric),
Err(err) => {
eprintln!(
"stateset-core metrics: failed to register histogram '{}' ({}); using local fallback collector",
name, err
);
let opts = HistogramOpts::new(name, help).buckets(buckets);
match HistogramVec::new(opts, labels) {
Ok(metric) => Some(metric),
Err(create_err) => {
eprintln!(
"stateset-core metrics: failed to create fallback histogram '{}' ({}); disabling metric",
name, create_err
);
None
}
}
}
}
}
fn register_int_counter_vec_resilient(
name: &str,
help: &str,
labels: &[&str],
) -> Option<IntCounterVec> {
match register_int_counter_vec!(name, help, labels) {
Ok(metric) => Some(metric),
Err(err) => {
eprintln!(
"stateset-core metrics: failed to register counter '{}' ({}); using local fallback collector",
name, err
);
match IntCounterVec::new(Opts::new(name, help), labels) {
Ok(metric) => Some(metric),
Err(create_err) => {
eprintln!(
"stateset-core metrics: failed to create fallback counter '{}' ({}); disabling metric",
name, create_err
);
None
}
}
}
}
}
fn register_int_gauge_resilient(name: &str, help: &str) -> Option<IntGauge> {
match register_int_gauge!(name, help) {
Ok(metric) => Some(metric),
Err(err) => {
eprintln!(
"stateset-core metrics: failed to register gauge '{}' ({}); using local fallback collector",
name, err
);
match IntGauge::new(name, help) {
Ok(metric) => Some(metric),
Err(create_err) => {
eprintln!(
"stateset-core metrics: failed to create fallback gauge '{}' ({}); disabling metric",
name, create_err
);
None
}
}
}
}
}
#[must_use]
#[derive(Debug)]
pub struct OperationTimer {
operation: &'static str,
labels: Vec<String>,
start: Instant,
}
impl OperationTimer {
pub fn start(operation: &'static str, labels: Vec<String>) -> Self {
Self { operation, labels, start: Instant::now() }
}
}
impl Drop for OperationTimer {
fn drop(&mut self) {
let duration = self.start.elapsed().as_secs_f64() * 1000.0;
let domain = domain_from_labels(&self.labels);
if let Some(metric) = OPERATION_LATENCY.as_ref() {
metric.with_label_values(&[self.operation, domain]).observe(duration);
}
}
}
pub fn domain_from_labels(labels: &[String]) -> &str {
labels
.iter()
.find_map(|label| label.strip_prefix("domain:"))
.filter(|value| !value.is_empty())
.unwrap_or("unknown")
}
#[macro_export]
macro_rules! track_operation {
(operation = $op:expr, labels = $labels:expr, $body:expr) => {{
let _timer = $crate::metrics::OperationTimer::start($op, $labels.clone());
let result = $body;
match result {
Ok(value) => {
let domain = $crate::metrics::domain_from_labels(&$labels);
if let Some(metric) = $crate::metrics::OPERATIONS_TOTAL.as_ref() {
metric.with_label_values(&[$op, domain]).inc();
}
Ok(value)
}
Err(error) => {
let domain = $crate::metrics::domain_from_labels(&$labels);
if let Some(metric) = $crate::metrics::OPERATIONS_FAILED.as_ref() {
metric.with_label_values(&[$op, domain]).inc();
}
Err(error)
}
}
}};
}
#[macro_export]
macro_rules! instrument_repository {
($repository_type:ident, $method:ident, $self:expr) => {
|operation_name| {
move || {
$crate::track_operation!(
operation = operation_name,
labels = vec![format!("domain:{}", stringify!($repository_type))],
$self.$method(operation_name)
)
}
}
};
}
pub mod orders {
use super::*;
pub fn track_order_creation(_customer_id: &str) {
if let Some(metric) = OPERATIONS_TOTAL.as_ref() {
metric.with_label_values(&["orders.create", "orders"]).inc();
}
}
pub fn track_order_status_transition(_order_id: &str, _from: &str, _to: &str) {
if let Some(metric) = OPERATIONS_TOTAL.as_ref() {
metric.with_label_values(&["orders.status_transition", "orders"]).inc();
}
}
}
pub mod inventory {
use super::*;
pub fn track_reservation(_sku: &str, _quantity: f64) {
if let Some(metric) = ACTIVE_RESERVATIONS.as_ref() {
metric.inc();
}
if let Some(metric) = OPERATIONS_TOTAL.as_ref() {
metric.with_label_values(&["inventory.reserve", "inventory"]).inc();
}
}
pub fn track_stock_adjustment(_sku: &str, _delta: f64) {
if let Some(metric) = OPERATIONS_TOTAL.as_ref() {
metric.with_label_values(&["inventory.adjust", "inventory"]).inc();
}
}
}
pub mod payments {
use super::*;
pub fn track_payment_processing(_order_id: &str, _amount: rust_decimal::Decimal) {
if let Some(metric) = OPERATIONS_TOTAL.as_ref() {
metric.with_label_values(&["payments.process", "payments"]).inc();
}
}
pub fn track_refund(_payment_id: &str, _amount: rust_decimal::Decimal) {
if let Some(metric) = OPERATIONS_TOTAL.as_ref() {
metric.with_label_values(&["payments.refund", "payments"]).inc();
}
}
}
#[derive(Debug)]
#[must_use]
pub struct LabelsBuilder {
labels: Vec<String>,
}
impl LabelsBuilder {
pub const fn new() -> Self {
Self { labels: Vec::new() }
}
pub fn add(mut self, key: &str, value: &str) -> Self {
self.labels.push(format!("{}:{}", key, value));
self
}
pub fn build(self) -> Vec<String> {
self.labels
}
}
impl Default for LabelsBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn export_metrics() -> Result<String, prometheus::Error> {
let encoder = TextEncoder::new();
let metric_families = prometheus::gather();
let mut buffer = Vec::new();
encoder.encode(&metric_families, &mut buffer)?;
String::from_utf8(buffer).map_err(|e| prometheus::Error::Msg(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_registration() {
let _ = &OPERATION_LATENCY;
let _ = &OPERATIONS_TOTAL;
let _ = &OPERATIONS_FAILED;
let _ = &ACTIVE_RESERVATIONS;
}
#[test]
fn test_operation_timer() {
let _timer = OperationTimer::start(
"test_operation",
vec!["label1:value1".to_string(), "label2:value2".to_string()],
);
std::thread::sleep(std::time::Duration::from_millis(10));
}
#[test]
fn test_labels_builder() {
let labels =
LabelsBuilder::new().add("domain", "orders").add("operation", "create").build();
assert_eq!(labels.len(), 2);
}
#[test]
fn test_domain_from_labels() {
let labels = vec!["foo:bar".to_string(), "domain:orders".to_string()];
assert_eq!(domain_from_labels(&labels), "orders");
let labels = vec!["domain:".to_string()];
assert_eq!(domain_from_labels(&labels), "unknown");
}
}