use opentelemetry::metrics::{Counter, Meter};
use opentelemetry::{global, KeyValue};
use crate::reload::{ConfigStatus, ReloadEvent};
pub const CONFIG: &str = "dynamic_config.config";
pub const REASON: &str = "dynamic_config.reason";
pub const ERROR_KIND: &str = "dynamic_config.error_kind";
pub const FINGERPRINT: &str = "dynamic_config.fingerprint";
#[derive(Clone)]
pub struct Recorder {
name: &'static str,
installs: Counter<u64>,
failures: Counter<u64>,
}
impl std::fmt::Debug for Recorder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Recorder")
.field("config", &self.name)
.finish_non_exhaustive()
}
}
impl Recorder {
#[must_use]
pub fn new(name: &'static str) -> Self {
Self::with_meter(name, &global::meter("dynamic-config"))
}
#[must_use]
pub fn with_meter(name: &'static str, meter: &Meter) -> Self {
Self {
name,
installs: meter
.u64_counter(crate::telemetry::INSTALLS_TOTAL)
.with_description("Snapshots installed since the process started.")
.build(),
failures: meter
.u64_counter(crate::telemetry::RELOAD_FAILURES_TOTAL)
.with_description("Reloads that installed nothing.")
.build(),
}
}
pub fn installed<T>(&self, event: &ReloadEvent<T>, fingerprint: Option<&str>) {
let mut attributes = vec![
KeyValue::new(CONFIG, self.name),
KeyValue::new(REASON, event.reason.as_str()),
];
if let Some(fingerprint) = fingerprint {
attributes.push(KeyValue::new(FINGERPRINT, fingerprint.to_owned()));
}
self.installs.add(1, &attributes);
}
pub fn refused(&self, error: &crate::Error) {
self.failures.add(
1,
&[
KeyValue::new(CONFIG, self.name),
KeyValue::new(ERROR_KIND, error.kind().as_str()),
],
);
}
#[must_use]
pub fn attributes(&self, status: &ConfigStatus) -> Vec<KeyValue> {
let mut attributes = vec![KeyValue::new(CONFIG, self.name)];
if let Some(reason) = &status.last_reason {
attributes.push(KeyValue::new(REASON, reason.as_str()));
}
attributes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recording_without_a_provider_is_harmless() {
let recorder = Recorder::new("Test");
recorder.refused(&crate::Error::remote("the store is down"));
assert!(format!("{recorder:?}").contains("Test"));
}
#[test]
fn the_debug_carries_the_config_name_and_nothing_else() {
let recorder = Recorder::new("Billing");
let rendered = format!("{recorder:?}");
assert!(rendered.contains("Billing"));
assert!(!rendered.contains("http"));
}
}