use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::Arc;
pub const MAX_METRIC_NAME_BYTES: usize = 128;
pub const MAX_IN_MEMORY_METRICS: usize = 4_096;
pub const MAX_IN_MEMORY_METRIC_BYTES: usize = 1024 * 1024;
const METRIC_FIXED_BYTES: usize =
std::mem::size_of::<(Arc<str>, u64)>() + std::mem::size_of::<usize>() * 4;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetricCounter {
pub name: String,
pub value: u64,
}
#[derive(Debug, Clone, Default)]
pub struct MetricSnapshot {
counters: Arc<BTreeMap<Arc<str>, u64>>,
}
impl MetricSnapshot {
pub fn len(&self) -> usize {
self.counters.len()
}
pub fn is_empty(&self) -> bool {
self.counters.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, u64)> {
self.counters
.iter()
.map(|(name, value)| (name.as_ref(), *value))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetricRegistryPressure {
pub entries: usize,
pub max_entries: usize,
pub used_bytes: usize,
pub peak_bytes: usize,
pub max_bytes: usize,
pub count_rejections: u64,
pub byte_rejections: u64,
pub name_rejections: u64,
}
#[derive(Debug)]
struct MetricState {
counters: Arc<BTreeMap<Arc<str>, u64>>,
pressure: MetricRegistryPressure,
}
#[derive(Debug)]
pub struct InMemoryMetrics {
state: Mutex<MetricState>,
}
impl InMemoryMetrics {
pub fn new() -> Self {
Self::with_limits(MAX_IN_MEMORY_METRICS, MAX_IN_MEMORY_METRIC_BYTES)
}
pub fn with_limits(max_entries: usize, max_bytes: usize) -> Self {
let max_entries = max_entries.clamp(1, MAX_IN_MEMORY_METRICS);
let max_bytes = max_bytes.clamp(1, MAX_IN_MEMORY_METRIC_BYTES);
Self {
state: Mutex::new(MetricState {
counters: Arc::new(BTreeMap::new()),
pressure: MetricRegistryPressure {
max_entries,
max_bytes,
..MetricRegistryPressure::default()
},
}),
}
}
pub fn increment(&self, name: &str) -> u64 {
self.try_increment(name).unwrap_or(0)
}
pub fn try_increment(&self, name: &str) -> Option<u64> {
let mut state = self.state.lock();
if !metric_name_is_valid(name) {
state.pressure.name_rejections = state.pressure.name_rejections.saturating_add(1);
return None;
}
if state.counters.contains_key(name) {
if let Some(value) = Arc::make_mut(&mut state.counters).get_mut(name) {
*value = value.saturating_add(1);
return Some(*value);
}
return None;
}
if state.pressure.entries >= state.pressure.max_entries {
state.pressure.count_rejections = state.pressure.count_rejections.saturating_add(1);
return None;
}
let retained_bytes = metric_retained_bytes(name);
if state.pressure.used_bytes.saturating_add(retained_bytes) > state.pressure.max_bytes {
state.pressure.byte_rejections = state.pressure.byte_rejections.saturating_add(1);
return None;
}
Arc::make_mut(&mut state.counters).insert(Arc::from(name), 1);
state.pressure.entries = state.pressure.entries.saturating_add(1);
state.pressure.used_bytes = state.pressure.used_bytes.saturating_add(retained_bytes);
state.pressure.peak_bytes = state.pressure.peak_bytes.max(state.pressure.used_bytes);
Some(1)
}
pub fn snapshot(&self) -> Vec<MetricCounter> {
self.shared_snapshot()
.iter()
.map(|(name, value)| MetricCounter {
name: name.to_string(),
value,
})
.collect()
}
pub fn shared_snapshot(&self) -> MetricSnapshot {
MetricSnapshot {
counters: Arc::clone(&self.state.lock().counters),
}
}
pub fn pressure(&self) -> MetricRegistryPressure {
self.state.lock().pressure
}
}
impl Default for InMemoryMetrics {
fn default() -> Self {
Self::new()
}
}
fn metric_name_is_valid(name: &str) -> bool {
!name.is_empty() && name.len() <= MAX_METRIC_NAME_BYTES && !name.contains('\0')
}
fn metric_retained_bytes(name: &str) -> usize {
METRIC_FIXED_BYTES.saturating_add(name.len())
}
#[cfg(test)]
#[path = "metrics_tests.rs"]
mod tests;