use std::sync::Arc;
use std::time::Instant;
use prometheus::{HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Registry};
pub static METRICS: std::sync::LazyLock<CdkMetrics> = std::sync::LazyLock::new(CdkMetrics::default);
#[derive(Debug)]
pub struct MintMetricGuard {
operation: &'static str,
start_time: Instant,
}
impl MintMetricGuard {
#[must_use]
pub fn new(operation: &'static str) -> Self {
METRICS.inc_in_flight_requests(operation);
Self {
operation,
start_time: Instant::now(),
}
}
pub fn record(self, success: bool) {
METRICS.record_mint_operation(self.operation, success);
METRICS.record_mint_operation_histogram(
self.operation,
success,
self.start_time.elapsed().as_secs_f64(),
);
if !success {
METRICS.record_error();
}
}
}
impl Drop for MintMetricGuard {
fn drop(&mut self) {
METRICS.dec_in_flight_requests(self.operation);
}
}
#[derive(Clone, Debug)]
pub struct CdkMetrics {
registry: Arc<Registry>,
http_requests_total: IntCounterVec,
http_request_duration: HistogramVec,
auth_attempts_total: IntCounter,
auth_successes_total: IntCounter,
payments_total: IntCounterVec,
payment_amount: HistogramVec,
payment_fees: HistogramVec,
db_operations_total: IntCounter,
db_operation_duration: HistogramVec,
db_connections_active: IntGauge,
errors_total: IntCounter,
mint_operations_total: IntCounterVec,
mint_in_flight_requests: IntGaugeVec,
mint_operation_duration: HistogramVec,
}
impl CdkMetrics {
pub fn new() -> crate::Result<Self> {
let registry = Arc::new(Registry::new());
let (http_requests_total, http_request_duration) = Self::create_http_metrics(®istry)?;
let (auth_attempts_total, auth_successes_total) = Self::create_auth_metrics(®istry)?;
let (payments_total, payment_amount, payment_fees) =
Self::create_payment_metrics(®istry)?;
let (db_operations_total, db_operation_duration, db_connections_active) =
Self::create_db_metrics(®istry)?;
let errors_total = Self::create_error_metrics(®istry)?;
let (mint_operations_total, mint_operation_duration, mint_in_flight_requests) =
Self::create_mint_metrics(®istry)?;
Ok(Self {
registry,
http_requests_total,
http_request_duration,
auth_attempts_total,
auth_successes_total,
payments_total,
payment_amount,
payment_fees,
db_operations_total,
db_operation_duration,
db_connections_active,
errors_total,
mint_operations_total,
mint_in_flight_requests,
mint_operation_duration,
})
}
fn create_http_metrics(registry: &Registry) -> crate::Result<(IntCounterVec, HistogramVec)> {
let http_requests_total = IntCounterVec::new(
prometheus::Opts::new("cdk_http_requests_total", "Total number of HTTP requests"),
&["endpoint", "status"],
)?;
registry.register(Box::new(http_requests_total.clone()))?;
let http_request_duration = HistogramVec::new(
prometheus::HistogramOpts::new(
"cdk_http_request_duration_seconds",
"HTTP request duration in seconds",
)
.buckets(vec![
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
]),
&["endpoint"],
)?;
registry.register(Box::new(http_request_duration.clone()))?;
Ok((http_requests_total, http_request_duration))
}
fn create_auth_metrics(registry: &Registry) -> crate::Result<(IntCounter, IntCounter)> {
let auth_attempts_total =
IntCounter::new("cdk_auth_attempts_total", "Total authentication attempts")?;
registry.register(Box::new(auth_attempts_total.clone()))?;
let auth_successes_total = IntCounter::new(
"cdk_auth_successes_total",
"Total successful authentications",
)?;
registry.register(Box::new(auth_successes_total.clone()))?;
Ok((auth_attempts_total, auth_successes_total))
}
fn create_payment_metrics(
registry: &Registry,
) -> crate::Result<(IntCounterVec, HistogramVec, HistogramVec)> {
let wallet_operations_total =
IntCounter::new("cdk_wallet_operations_total", "Total wallet operations")?;
registry.register(Box::new(wallet_operations_total))?;
let payments_total = IntCounterVec::new(
prometheus::Opts::new("cdk_payments_total", "Total confirmed payments"),
&["method"],
)?;
registry.register(Box::new(payments_total.clone()))?;
let payment_amount = HistogramVec::new(
prometheus::HistogramOpts::new(
"cdk_payment_amount_sats",
"Confirmed payment amounts in satoshis",
)
.buckets(vec![
1.0,
10.0,
100.0,
1000.0,
10_000.0,
100_000.0,
1_000_000.0,
]),
&["method"],
)?;
registry.register(Box::new(payment_amount.clone()))?;
let payment_fees = HistogramVec::new(
prometheus::HistogramOpts::new(
"cdk_payment_fees_sats",
"Confirmed payment fees in satoshis",
)
.buckets(vec![0.0, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0]),
&["method"],
)?;
registry.register(Box::new(payment_fees.clone()))?;
Ok((payments_total, payment_amount, payment_fees))
}
fn create_db_metrics(
registry: &Registry,
) -> crate::Result<(IntCounter, HistogramVec, IntGauge)> {
let db_operations_total =
IntCounter::new("cdk_db_operations_total", "Total database operations")?;
registry.register(Box::new(db_operations_total.clone()))?;
let db_operation_duration = HistogramVec::new(
prometheus::HistogramOpts::new(
"cdk_db_operation_duration_seconds",
"Database operation duration in seconds",
)
.buckets(vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0]),
&["operation"],
)?;
registry.register(Box::new(db_operation_duration.clone()))?;
let db_connections_active = IntGauge::new(
"cdk_db_connections_active",
"Number of active database connections",
)?;
registry.register(Box::new(db_connections_active.clone()))?;
Ok((
db_operations_total,
db_operation_duration,
db_connections_active,
))
}
fn create_error_metrics(registry: &Registry) -> crate::Result<IntCounter> {
let errors_total = IntCounter::new("cdk_errors_total", "Total errors")?;
registry.register(Box::new(errors_total.clone()))?;
Ok(errors_total)
}
fn create_mint_metrics(
registry: &Registry,
) -> crate::Result<(IntCounterVec, HistogramVec, IntGaugeVec)> {
let mint_operations_total = IntCounterVec::new(
prometheus::Opts::new(
"cdk_mint_operations_total",
"Total number of mint operations",
),
&["operation", "status"],
)?;
registry.register(Box::new(mint_operations_total.clone()))?;
let mint_operation_duration = HistogramVec::new(
prometheus::HistogramOpts::new(
"cdk_mint_operation_duration_seconds",
"Duration of mint operations in seconds",
)
.buckets(vec![
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
]),
&["operation", "status"],
)?;
registry.register(Box::new(mint_operation_duration.clone()))?;
let mint_in_flight_requests = IntGaugeVec::new(
prometheus::Opts::new(
"cdk_mint_in_flight_requests",
"Number of in-flight mint requests",
),
&["operation"],
)?;
registry.register(Box::new(mint_in_flight_requests.clone()))?;
Ok((
mint_operations_total,
mint_operation_duration,
mint_in_flight_requests,
))
}
#[must_use]
pub fn registry(&self) -> Arc<Registry> {
Arc::<Registry>::clone(&self.registry)
}
pub fn record_http_request(&self, endpoint: &str, status: &str) {
self.http_requests_total
.with_label_values(&[endpoint, status])
.inc();
}
pub fn record_http_request_duration(&self, duration_seconds: f64, endpoint: &str) {
self.http_request_duration
.with_label_values(&[endpoint])
.observe(duration_seconds);
}
pub fn record_auth_attempt(&self) {
self.auth_attempts_total.inc();
}
pub fn record_auth_success(&self) {
self.auth_successes_total.inc();
}
pub fn record_payment(&self, method: &str, amount: f64, fee: f64) {
self.record_payment_total(method);
self.record_payment_amount(method, amount);
self.record_payment_fee(method, fee);
}
pub fn record_payment_total(&self, method: &str) {
self.payments_total.with_label_values(&[method]).inc();
}
pub fn record_payment_amount(&self, method: &str, amount: f64) {
self.payment_amount
.with_label_values(&[method])
.observe(amount);
}
pub fn record_payment_fee(&self, method: &str, fee: f64) {
self.payment_fees.with_label_values(&[method]).observe(fee);
}
pub fn record_db_operation(&self, duration_seconds: f64, op: &str) {
self.db_operations_total.inc();
self.db_operation_duration
.with_label_values(&[op])
.observe(duration_seconds);
}
pub fn set_db_connections_active(&self, count: i64) {
self.db_connections_active.set(count);
}
pub fn record_error(&self) {
self.errors_total.inc();
}
pub fn record_mint_operation(&self, operation: &str, success: bool) {
let status = if success { "success" } else { "error" };
self.mint_operations_total
.with_label_values(&[operation, status])
.inc();
}
pub fn record_mint_operation_histogram(
&self,
operation: &str,
success: bool,
duration_seconds: f64,
) {
let status = if success { "success" } else { "error" };
self.mint_operation_duration
.with_label_values(&[operation, status])
.observe(duration_seconds);
}
pub fn inc_in_flight_requests(&self, operation: &str) {
self.mint_in_flight_requests
.with_label_values(&[operation])
.inc();
}
pub fn dec_in_flight_requests(&self, operation: &str) {
self.mint_in_flight_requests
.with_label_values(&[operation])
.dec();
}
}
impl Default for CdkMetrics {
fn default() -> Self {
Self::new().expect("Failed to create default CdkMetrics")
}
}
#[cfg(test)]
mod tests {
use std::sync::{Mutex, MutexGuard};
use std::time::Duration;
use super::{MintMetricGuard, METRICS};
static METRICS_TEST_LOCK: Mutex<()> = Mutex::new(());
fn metrics_lock() -> MutexGuard<'static, ()> {
METRICS_TEST_LOCK
.lock()
.expect("metrics test lock should not be poisoned")
}
#[test]
fn mint_metric_guard_records_success_and_balances_in_flight() {
let _lock = metrics_lock();
let operation = "test_guard_success";
let in_flight = METRICS
.mint_in_flight_requests
.with_label_values(&[operation]);
let success_count = METRICS
.mint_operations_total
.with_label_values(&[operation, "success"]);
let error_count = METRICS
.mint_operations_total
.with_label_values(&[operation, "error"]);
let duration = METRICS
.mint_operation_duration
.with_label_values(&[operation, "success"]);
let in_flight_before = in_flight.get();
let success_count_before = success_count.get();
let error_count_before = error_count.get();
let duration_count_before = duration.get_sample_count();
let errors_before = METRICS.errors_total.get();
let guard = MintMetricGuard::new(operation);
assert_eq!(in_flight.get(), in_flight_before + 1);
std::thread::sleep(Duration::from_millis(1));
guard.record(true);
assert_eq!(in_flight.get(), in_flight_before);
assert_eq!(success_count.get(), success_count_before + 1);
assert_eq!(error_count.get(), error_count_before);
assert_eq!(duration.get_sample_count(), duration_count_before + 1);
assert_eq!(METRICS.errors_total.get(), errors_before);
}
#[test]
fn mint_metric_guard_records_error_and_global_error_count() {
let _lock = metrics_lock();
let operation = "test_guard_error";
let in_flight = METRICS
.mint_in_flight_requests
.with_label_values(&[operation]);
let error_count = METRICS
.mint_operations_total
.with_label_values(&[operation, "error"]);
let duration = METRICS
.mint_operation_duration
.with_label_values(&[operation, "error"]);
let in_flight_before = in_flight.get();
let error_count_before = error_count.get();
let duration_count_before = duration.get_sample_count();
let errors_before = METRICS.errors_total.get();
let guard = MintMetricGuard::new(operation);
assert_eq!(in_flight.get(), in_flight_before + 1);
guard.record(false);
assert_eq!(in_flight.get(), in_flight_before);
assert_eq!(error_count.get(), error_count_before + 1);
assert_eq!(duration.get_sample_count(), duration_count_before + 1);
assert_eq!(METRICS.errors_total.get(), errors_before + 1);
}
#[test]
fn mint_metric_guard_drop_without_record_only_balances_in_flight() {
let _lock = metrics_lock();
let operation = "test_guard_drop_without_record";
let in_flight = METRICS
.mint_in_flight_requests
.with_label_values(&[operation]);
let success_count = METRICS
.mint_operations_total
.with_label_values(&[operation, "success"]);
let error_count = METRICS
.mint_operations_total
.with_label_values(&[operation, "error"]);
let success_duration = METRICS
.mint_operation_duration
.with_label_values(&[operation, "success"]);
let error_duration = METRICS
.mint_operation_duration
.with_label_values(&[operation, "error"]);
let in_flight_before = in_flight.get();
let success_count_before = success_count.get();
let error_count_before = error_count.get();
let success_duration_before = success_duration.get_sample_count();
let error_duration_before = error_duration.get_sample_count();
let errors_before = METRICS.errors_total.get();
{
let _guard = MintMetricGuard::new(operation);
assert_eq!(in_flight.get(), in_flight_before + 1);
}
assert_eq!(in_flight.get(), in_flight_before);
assert_eq!(success_count.get(), success_count_before);
assert_eq!(error_count.get(), error_count_before);
assert_eq!(success_duration.get_sample_count(), success_duration_before);
assert_eq!(error_duration.get_sample_count(), error_duration_before);
assert_eq!(METRICS.errors_total.get(), errors_before);
}
#[test]
fn payment_metrics_are_labeled_by_method() {
let _lock = metrics_lock();
let method = "test_payment_method";
let payments = METRICS.payments_total.with_label_values(&[method]);
let amount = METRICS.payment_amount.with_label_values(&[method]);
let fee = METRICS.payment_fees.with_label_values(&[method]);
let payments_before = payments.get();
let amount_count_before = amount.get_sample_count();
let fee_count_before = fee.get_sample_count();
METRICS.record_payment(method, 21.0, 1.0);
assert_eq!(payments.get(), payments_before + 1);
assert_eq!(amount.get_sample_count(), amount_count_before + 1);
assert_eq!(fee.get_sample_count(), fee_count_before + 1);
}
}