Initial implementation of a recorder
This commit is contained in:
parent
696954c631
commit
d570d35112
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "bare-metrics-recorder"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
metrics = "0.17.0"
|
||||||
|
anyhow = "1.0.48"
|
||||||
|
thiserror = "1.0.30"
|
||||||
|
log = "0.4.14"
|
||||||
|
bare-metrics-core = { version = "0.1.0", path = "../bare-metrics-core" }
|
||||||
|
dashmap = "4.0.2"
|
||||||
|
fxhash = "0.2.1"
|
||||||
|
crossbeam-channel = "0.5.1"
|
||||||
|
serde_bare = "0.5.0"
|
||||||
|
hdrhistogram = "7.4.0"
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod recording;
|
|
@ -0,0 +1,323 @@
|
||||||
|
use bare_metrics_core::structures::{
|
||||||
|
Frame, MetricDescriptor, MetricId, MetricKind, SerialisableHistogram,
|
||||||
|
};
|
||||||
|
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use hdrhistogram::Histogram;
|
||||||
|
use log::{error, warn};
|
||||||
|
use metrics::{GaugeValue, Key, Recorder, Unit};
|
||||||
|
use serde_bare::Uint;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::atomic::{AtomicU16, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread::JoinHandle;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
// TODO add a reader
|
||||||
|
// the reader accepts a Read or a File (is there a 'SeekRead'?)
|
||||||
|
// and streams BARE messages out of it
|
||||||
|
// Each message is returned with a seek token, which can be used by the application later to return
|
||||||
|
// the reader to that point in time.
|
||||||
|
|
||||||
|
pub struct BareMetricsRecorderCore<W: Write + Send> {
|
||||||
|
writer: W,
|
||||||
|
emission_interval: Duration,
|
||||||
|
queue_size: usize,
|
||||||
|
known_metric_kinds: HashMap<MetricId, MetricKind>,
|
||||||
|
next_frame: Frame,
|
||||||
|
current_gauges: HashMap<MetricId, f64>,
|
||||||
|
current_counters: HashMap<MetricId, u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write + Send + 'static> BareMetricsRecorderCore<W> {
|
||||||
|
pub fn new(writer: W) -> Self {
|
||||||
|
BareMetricsRecorderCore {
|
||||||
|
writer,
|
||||||
|
emission_interval: Duration::from_secs(60),
|
||||||
|
queue_size: 256,
|
||||||
|
known_metric_kinds: Default::default(),
|
||||||
|
next_frame: Frame::default(),
|
||||||
|
current_gauges: Default::default(),
|
||||||
|
current_counters: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_emission_interval(mut self, interval: Duration) -> Self {
|
||||||
|
self.emission_interval = interval;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_queue_size(mut self, queue_size: usize) -> Self {
|
||||||
|
self.queue_size = queue_size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(self) -> anyhow::Result<(BareMetricsRecorderShard, BareMetricsRecorderStopper)> {
|
||||||
|
let (tx, rx) = crossbeam_channel::bounded(self.queue_size);
|
||||||
|
|
||||||
|
let handle = std::thread::Builder::new()
|
||||||
|
.name("bare-metrics-recorder".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
if let Err(e) = self.run_forever(rx) {
|
||||||
|
error!("Recorder erred: {:?}", e);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let shard = BareMetricsRecorderShard {
|
||||||
|
metric_ids: Default::default(),
|
||||||
|
metric_id_generator: Default::default(),
|
||||||
|
recorder_tx: tx.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let stopper = BareMetricsRecorderStopper {
|
||||||
|
join_handle: handle,
|
||||||
|
sender: tx,
|
||||||
|
};
|
||||||
|
Ok((shard, stopper))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_and_clear_frame(&mut self) -> anyhow::Result<()> {
|
||||||
|
serde_bare::to_writer(&mut self.writer, &self.next_frame)?;
|
||||||
|
self.writer.flush()?;
|
||||||
|
|
||||||
|
// Clear the next frame so the diff starts from zero again.
|
||||||
|
self.next_frame.counter_updates.clear();
|
||||||
|
self.next_frame.gauge_updates.clear();
|
||||||
|
self.next_frame.histograms.clear();
|
||||||
|
self.next_frame.new_metrics.clear();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_forever(mut self, rx: Receiver<RecorderMessage>) -> anyhow::Result<()> {
|
||||||
|
let mut next_emission = Instant::now() + self.emission_interval;
|
||||||
|
loop {
|
||||||
|
match rx.recv_deadline(next_emission) {
|
||||||
|
Ok(RecorderMessage::NewMetric {
|
||||||
|
metric_id,
|
||||||
|
metric_kind,
|
||||||
|
unit,
|
||||||
|
description,
|
||||||
|
}) => {
|
||||||
|
self.next_frame.new_metrics.insert(
|
||||||
|
metric_id,
|
||||||
|
MetricDescriptor {
|
||||||
|
kind: metric_kind,
|
||||||
|
unit: unit.map(|unit| unit.as_str().to_string()),
|
||||||
|
description: description.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.known_metric_kinds.insert(metric_id, metric_kind);
|
||||||
|
match metric_kind {
|
||||||
|
MetricKind::Histogram => {}
|
||||||
|
MetricKind::Gauge => {
|
||||||
|
self.current_gauges.insert(metric_id, 0.0);
|
||||||
|
}
|
||||||
|
MetricKind::Counter => {
|
||||||
|
self.current_counters.insert(metric_id, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(RecorderMessage::HistogramReading { metric_id, value }) => {
|
||||||
|
let histogram =
|
||||||
|
self.next_frame
|
||||||
|
.histograms
|
||||||
|
.entry(metric_id)
|
||||||
|
.or_insert_with(|| SerialisableHistogram {
|
||||||
|
underlying: Histogram::new(3).unwrap(),
|
||||||
|
});
|
||||||
|
if let Err(err) = histogram.underlying.record(value as u64) {
|
||||||
|
warn!("Couldn't record histogram entry because {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(RecorderMessage::CounterCount { metric_id, value }) => {
|
||||||
|
let entry = self.current_counters.get_mut(&metric_id);
|
||||||
|
if let Some(counter_value) = entry {
|
||||||
|
*counter_value += value;
|
||||||
|
self.next_frame
|
||||||
|
.counter_updates
|
||||||
|
.insert(metric_id, Uint(*counter_value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(RecorderMessage::GaugeUpdate {
|
||||||
|
metric_id,
|
||||||
|
gauge_update,
|
||||||
|
}) => {
|
||||||
|
let entry = self.current_gauges.get_mut(&metric_id);
|
||||||
|
if let Some(gauge_value) = entry {
|
||||||
|
match gauge_update {
|
||||||
|
GaugeValue::Absolute(value) => {
|
||||||
|
*gauge_value = value;
|
||||||
|
}
|
||||||
|
GaugeValue::Increment(by) => {
|
||||||
|
*gauge_value += by;
|
||||||
|
}
|
||||||
|
GaugeValue::Decrement(by) => {
|
||||||
|
*gauge_value -= by;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.next_frame
|
||||||
|
.gauge_updates
|
||||||
|
.insert(metric_id, *gauge_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(RecorderMessage::Stop) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Timeout) => {
|
||||||
|
// It's time to emit a new frame.
|
||||||
|
self.emit_and_clear_frame()?;
|
||||||
|
next_emission = Instant::now() + self.emission_interval;
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BareMetricsRecorderStopper {
|
||||||
|
join_handle: JoinHandle<()>,
|
||||||
|
sender: Sender<RecorderMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BareMetricsRecorderStopper {
|
||||||
|
pub fn stop(self) {
|
||||||
|
self.sender
|
||||||
|
.send(RecorderMessage::Stop)
|
||||||
|
.expect("Should have been able to stop bare metrics' recorder.");
|
||||||
|
self.join_handle
|
||||||
|
.join()
|
||||||
|
.expect("Should have been able to join on bare metrics' join handle.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct MetricIdGenerator {
|
||||||
|
next_id: AtomicU16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetricIdGenerator {
|
||||||
|
pub fn generate(&self) -> MetricId {
|
||||||
|
MetricId(self.next_id.fetch_add(1, Ordering::SeqCst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RecorderMessage {
|
||||||
|
NewMetric {
|
||||||
|
metric_id: MetricId,
|
||||||
|
metric_kind: MetricKind,
|
||||||
|
unit: Option<Unit>,
|
||||||
|
description: &'static str,
|
||||||
|
},
|
||||||
|
HistogramReading {
|
||||||
|
metric_id: MetricId,
|
||||||
|
value: u64,
|
||||||
|
},
|
||||||
|
CounterCount {
|
||||||
|
metric_id: MetricId,
|
||||||
|
value: u64,
|
||||||
|
},
|
||||||
|
GaugeUpdate {
|
||||||
|
metric_id: MetricId,
|
||||||
|
gauge_update: GaugeValue,
|
||||||
|
},
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BareMetricsRecorderShard {
|
||||||
|
metric_ids: Arc<DashMap<Key, MetricId>>,
|
||||||
|
metric_id_generator: Arc<MetricIdGenerator>,
|
||||||
|
recorder_tx: Sender<RecorderMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BareMetricsRecorderShard {
|
||||||
|
fn register_metric(
|
||||||
|
&self,
|
||||||
|
key: &Key,
|
||||||
|
metric_kind: MetricKind,
|
||||||
|
unit: Option<Unit>,
|
||||||
|
description: Option<&'static str>,
|
||||||
|
) {
|
||||||
|
let metric_id = self.metric_id_generator.generate();
|
||||||
|
if !self.metric_ids.insert(key.clone(), metric_id).is_none() {
|
||||||
|
error!(
|
||||||
|
"Double-registered metric key {:?} (id {:?} here)",
|
||||||
|
key, metric_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let registration = RecorderMessage::NewMetric {
|
||||||
|
metric_id,
|
||||||
|
metric_kind,
|
||||||
|
unit,
|
||||||
|
description: description.unwrap_or(""),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = self.recorder_tx.send(registration) {
|
||||||
|
error!(
|
||||||
|
"Couldn't register metric {:?}: sending to channel: {:?}",
|
||||||
|
key, e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_as_metrics_recorder(&self) -> anyhow::Result<()> {
|
||||||
|
metrics::set_boxed_recorder(Box::new(self.clone()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Recorder for BareMetricsRecorderShard {
|
||||||
|
fn register_counter(&self, key: &Key, unit: Option<Unit>, description: Option<&'static str>) {
|
||||||
|
self.register_metric(key, MetricKind::Counter, unit, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_gauge(&self, key: &Key, unit: Option<Unit>, description: Option<&'static str>) {
|
||||||
|
self.register_metric(key, MetricKind::Gauge, unit, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_histogram(&self, key: &Key, unit: Option<Unit>, description: Option<&'static str>) {
|
||||||
|
self.register_metric(key, MetricKind::Histogram, unit, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_counter(&self, key: &Key, value: u64) {
|
||||||
|
if let Some(counter_metric_id) = self.metric_ids.get(key) {
|
||||||
|
// we intentionally ignore errors (for now). Maybe logging occasionally is the right
|
||||||
|
// solution?
|
||||||
|
let _ = self.recorder_tx.send(RecorderMessage::CounterCount {
|
||||||
|
metric_id: *counter_metric_id,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_gauge(&self, key: &Key, value: GaugeValue) {
|
||||||
|
if let Some(gauge_metric_id) = self.metric_ids.get(key) {
|
||||||
|
// we intentionally ignore errors (for now). Maybe logging occasionally is the right
|
||||||
|
// solution?
|
||||||
|
let _ = self.recorder_tx.send(RecorderMessage::GaugeUpdate {
|
||||||
|
metric_id: *gauge_metric_id,
|
||||||
|
gauge_update: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_histogram(&self, key: &Key, value: f64) {
|
||||||
|
if let Some(histogram_metric_id) = self.metric_ids.get(key) {
|
||||||
|
// we intentionally ignore errors (for now). Maybe logging occasionally is the right
|
||||||
|
// solution?
|
||||||
|
let _ = self.recorder_tx.send(RecorderMessage::HistogramReading {
|
||||||
|
metric_id: *histogram_metric_id,
|
||||||
|
// Our Histogram supports u64 only (maybe expand to i64 later?).
|
||||||
|
value: value as u64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue