use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, sync_channel};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
use crossbeam_utils::CachePadded;
use rustc_hash::FxHashMap as HashMap;
use thread_local::ThreadLocal;
use crate::tags::TagValues;
use crate::{Metric, MetricKey, MetricMeta, MetricType, Sink};
pub trait AggregationSink: Send + 'static {
type Output;
fn emit(&mut self, metrics: Aggregations) -> Self::Output;
}
pub(crate) struct LocalKey(pub(crate) MetricKey<'static>);
impl LocalKey {
fn into_metric(self) -> AggregatedMetric {
AggregatedMetric {
meta: *self.0.meta,
tag_values: self.0.tag_values,
}
}
}
impl LocalKey {
fn key(&self) -> (*const MetricMeta, &TagValues) {
(self.0.meta as *const _, &self.0.tag_values)
}
}
impl Hash for LocalKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key().hash(state);
}
}
impl PartialEq for LocalKey {
fn eq(&self, other: &Self) -> bool {
self.key() == other.key()
}
}
impl Eq for LocalKey {}
pub struct AggregatedGauge {
pub min: f64,
pub max: f64,
pub sum: f64,
pub count: u64,
pub last: f64,
}
impl Default for AggregatedGauge {
fn default() -> Self {
Self {
min: f64::INFINITY,
max: f64::NEG_INFINITY,
sum: 0.0,
count: 0,
last: 0.0,
}
}
}
#[derive(Default)]
pub struct PreciseAggregatedDistribution {
pub values: Vec<f64>,
}
#[derive(Default)]
pub(crate) struct PreAggregations {
counters: HashMap<LocalKey, f64>,
pub(crate) gauges: HashMap<LocalKey, AggregatedGauge>,
distributions: HashMap<LocalKey, PreciseAggregatedDistribution>,
}
type ThreadLocalAggregations = Arc<ThreadLocal<CachePadded<Mutex<PreAggregations>>>>;
pub(crate) enum Task<Output> {
Flush(SyncSender<Output>),
Shutdown,
}
pub struct ThreadLocalAggregator<Output> {
pub(crate) aggregations: ThreadLocalAggregations,
pub(crate) thread: Option<(SyncSender<Task<Output>>, JoinHandle<()>)>,
}
impl<Output> Drop for ThreadLocalAggregator<Output> {
fn drop(&mut self) {
if let Some((sender, thread)) = self.thread.take() {
let _ = sender.try_send(Task::Shutdown);
drop(sender);
thread.join().unwrap();
}
}
}
impl<Output: Send + 'static> ThreadLocalAggregator<Output> {
pub fn new(flush_interval: Duration, sink: impl AggregationSink<Output = Output>) -> Self {
let aggregations = Default::default();
let (send_signal, recv_signal) = sync_channel(0);
let thread = std::thread::Builder::new()
.name("merni-aggregator".into())
.spawn({
let aggregations = Arc::clone(&aggregations);
move || Self::thread(aggregations, flush_interval, sink, recv_signal)
})
.unwrap();
Self {
aggregations,
thread: Some((send_signal, thread)),
}
}
pub fn flush(&self, timeout: Option<Duration>) -> Result<Output, RecvTimeoutError> {
let Some((thread_sender, _thread)) = &self.thread else {
return Err(RecvTimeoutError::Disconnected);
};
let (sender, receiver) = sync_channel(1);
thread_sender
.send(Task::Flush(sender))
.map_err(|_| RecvTimeoutError::Disconnected)?;
if let Some(timeout) = timeout {
receiver.recv_timeout(timeout)
} else {
receiver.recv().map_err(|_| RecvTimeoutError::Disconnected)
}
}
fn thread(
thread_locals: ThreadLocalAggregations,
flush_interval: Duration,
mut sink: impl AggregationSink<Output = Output>,
recv_signal: Receiver<Task<Output>>,
) {
loop {
let signal = recv_signal.recv_timeout(flush_interval);
let mut all_aggregations = Aggregations::default();
for thread_local in thread_locals.iter() {
let mut thread_local = thread_local.lock().unwrap();
all_aggregations.merge_aggregations(&mut thread_local);
}
let output = sink.emit(all_aggregations);
match signal {
Ok(Task::Flush(sender)) => {
let _ = sender.send(output);
}
Ok(Task::Shutdown) | Err(RecvTimeoutError::Disconnected) => return,
_ => {}
}
}
}
fn add_metric(&self, metric: Metric) {
let mut aggregations = self.aggregations.get_or_default().lock().unwrap();
let ty = metric.ty();
let key = LocalKey(metric.key);
let value = metric.value.get();
match ty {
MetricType::Counter => {
*aggregations.counters.entry(key).or_default() += value;
}
MetricType::Gauge => {
let gauge = aggregations.gauges.entry(key).or_default();
gauge.last = value;
gauge.min = gauge.min.min(value);
gauge.max = gauge.max.max(value);
gauge.sum += value;
gauge.count += 1;
}
MetricType::Distribution | MetricType::Timer => {
aggregations
.distributions
.entry(key)
.or_default()
.values
.push(value);
}
}
}
}
impl<Output: Send + 'static> Sink for ThreadLocalAggregator<Output> {
fn emit(&self, metric: Metric) {
self.add_metric(metric)
}
}
#[derive(Hash, PartialEq, Eq)]
pub struct AggregatedMetric {
pub(crate) meta: MetricMeta,
pub(crate) tag_values: TagValues,
}
impl Deref for AggregatedMetric {
type Target = MetricMeta;
fn deref(&self) -> &Self::Target {
&self.meta
}
}
impl AggregatedMetric {
pub fn tags(&self) -> impl ExactSizeIterator<Item = (&str, &str)> {
let values = self.tag_values.as_deref().unwrap_or_default();
self.meta
.tag_keys
.iter()
.copied()
.zip(values.iter().map(|s| s.as_ref()))
}
}
#[derive(Default)]
pub struct Aggregations {
pub counters: HashMap<AggregatedMetric, f64>,
pub gauges: HashMap<AggregatedMetric, AggregatedGauge>,
pub distributions: HashMap<AggregatedMetric, PreciseAggregatedDistribution>,
}
impl Aggregations {
pub(crate) fn merge_aggregations(&mut self, aggregations: &mut PreAggregations) {
for (key, value) in aggregations.counters.drain() {
let key = key.into_metric();
*self.counters.entry(key).or_default() += value;
}
for (key, other) in aggregations.gauges.drain() {
let key = key.into_metric();
let gauge = self.gauges.entry(key).or_default();
gauge.min = gauge.min.min(other.min);
gauge.max = gauge.max.max(other.max);
gauge.sum += other.sum;
gauge.count += other.count;
gauge.last = other.last;
}
for (key, other) in aggregations.distributions.drain() {
let key = key.into_metric();
self.distributions
.entry(key)
.or_default()
.values
.extend(other.values);
}
}
}