use arc_swap::ArcSwap;
use chrono::Utc;
use clap::{crate_name, crate_version};
use std::fmt::Write;
use std::fmt::{self, Debug};
use std::sync::{Arc, Mutex, Weak};
#[cfg(test)]
use std::{cmp::Ordering, collections::BTreeMap};
const PROMETHEUS_PREFIX: &str = "rotonda";
#[derive(Clone, Default)]
pub struct Collection {
sources: Arc<ArcSwap<Vec<RegisteredSource>>>,
register: Arc<Mutex<()>>,
}
impl Collection {
const ASSEMBLE_TIME_MS_METRIC: Metric = Metric::new(
"metric_assemble_duration",
"the time taken in milliseconds to assemble the last metric snapshot",
MetricType::Gauge,
MetricUnit::Millisecond,
);
}
impl Collection {
pub fn register(&self, name: Arc<str>, source: Weak<dyn Source>) {
let lock = self.register.lock().unwrap();
let old_sources = self.sources.load();
let mut new_sources = Vec::new();
for item in old_sources.iter() {
if item.source.strong_count() > 0 {
new_sources.push(item.clone())
}
}
new_sources.push(RegisteredSource { name, source });
new_sources.sort_by(|l, r| l.name.as_ref().cmp(r.name.as_ref()));
self.sources.store(new_sources.into());
drop(lock);
}
pub fn assemble(&self, format: OutputFormat) -> String {
let start_time = Utc::now();
let sources = self.sources.load();
let mut target = Target::new(format);
for item in sources.iter() {
if let Some(source) = item.source.upgrade() {
source.append(&item.name, &mut target)
}
}
let assemble_ms = (Utc::now() - start_time).num_milliseconds();
target.append_simple(
&Self::ASSEMBLE_TIME_MS_METRIC,
None,
assemble_ms,
);
target.into_string()
}
}
impl fmt::Debug for Collection {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let len = self.sources.load().len();
write!(f, "Collection({} sources)", len)
}
}
#[derive(Clone)]
struct RegisteredSource {
name: Arc<str>,
source: Weak<dyn Source>,
}
pub trait Source: Send + Sync {
fn append(&self, unit_name: &str, target: &mut Target);
}
impl<T: Source> Source for Arc<T> {
fn append(&self, unit_name: &str, target: &mut Target) {
AsRef::<T>::as_ref(self).append(unit_name, target)
}
}
#[cfg(test)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RawMetricValue(String);
#[cfg(test)]
impl RawMetricValue {
pub fn raw(self) -> String {
self.0
}
pub fn parse<T>(self) -> T
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: Debug,
{
str::parse(self.0.as_str()).unwrap()
}
}
#[cfg(test)]
impl<T> From<T> for RawMetricValue
where
T: std::fmt::Display,
{
fn from(value: T) -> Self {
Self(format!("{value}"))
}
}
#[cfg(test)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct RawMetricKey {
pub name: String,
pub unit: MetricUnit,
pub suffix: Option<String>,
pub labels: Vec<(String, String)>,
}
#[cfg(test)]
impl RawMetricKey {
pub fn named(
name: String,
unit: MetricUnit,
suffix: Option<String>,
) -> Self {
Self {
name,
unit,
suffix,
labels: Default::default(),
}
}
pub fn labelled(
name: String,
unit: MetricUnit,
suffix: Option<String>,
labels: Vec<(String, String)>,
) -> Self {
Self {
name,
unit,
suffix,
labels,
}
}
}
#[cfg(test)]
impl PartialOrd for RawMetricKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
impl Ord for RawMetricKey {
fn cmp(&self, other: &Self) -> Ordering {
match self.name.cmp(&other.name) {
Ordering::Equal => {}
ord => return ord,
}
match self.suffix.cmp(&other.suffix) {
Ordering::Equal => {}
ord => return ord,
}
self.labels.cmp(&other.labels)
}
}
#[cfg(test)]
impl TryFrom<String> for RawMetricKey {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
let parts = value.split('|').collect::<Vec<_>>();
match parts.len() {
3|4 => {
let name = parts[0].to_string();
let unit = MetricUnit::try_from(parts[1])?;
let suffix = parts[2];
let suffix = match suffix {
"None" => None,
_ => Some(suffix.to_string()),
};
match parts.len() {
3 => Ok(RawMetricKey::named(name, unit, suffix)),
4 => {
let labels = parts[3].split(',').filter_map(|v| {
v.split_once('=').map(|(lhs, rhs)| (lhs.to_string(), rhs.to_string()))
}).collect::<Vec<_>>();
Ok(RawMetricKey::labelled(name, unit, suffix, labels))
}
_ => unreachable!(),
}
}
_ => Err(format!("Metric key '{value}' should be in the form name|unit|suffix or name|unit|suffix|label"))
}
}
}
#[derive(Clone, Debug)]
pub struct Target {
format: OutputFormat,
target: String,
#[cfg(test)]
raw: BTreeMap<RawMetricKey, RawMetricValue>,
}
impl Target {
pub fn new(format: OutputFormat) -> Self {
let mut target = String::new();
if matches!(format, OutputFormat::Plain) {
target.push_str(concat!(
"version: ",
crate_name!(),
"/",
crate_version!(),
"\n"
));
}
Target {
format,
target,
#[cfg(test)]
raw: Default::default(),
}
}
#[cfg(test)]
pub fn with_name<T>(&self, name: &str) -> T
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: Debug,
{
self.raw
.iter()
.find(|(k, _v)| k.name == name && k.labels.is_empty())
.map(|(_k, v)| v.clone())
.unwrap()
.parse()
}
#[cfg(test)]
pub fn with_label<T>(&self, name: &str, label: (&str, &str)) -> T
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: Debug,
{
let label = (label.0.to_string(), label.1.to_string());
match self.raw
.iter()
.find(|(k, _v)| k.name == name && k.labels.contains(&label))
.map(|(_k, v)| v.clone()) {
Some(found) => found.parse(),
None => panic!("No metric {name} with label {label:?} found.\nAvailable metrics are:\n{:#?}", self.raw),
}
}
#[cfg(test)]
pub fn with_labels<T>(&self, name: &str, labels: &[(&str, &str)]) -> T
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: Debug,
{
'outer: for (k, v) in self.raw.iter().filter(|(k, _v)| k.name == name)
{
for needle in labels {
if !k
.labels
.iter()
.map(|(name, value)| (name.as_str(), value.as_str()))
.any(|haystack| &haystack == needle)
{
continue 'outer;
}
}
return v.clone().parse();
}
panic!("No metric {name} found with {labels:?}\nAvailable metrics are:\n{:#?}", self.raw);
}
pub fn into_string(self) -> String {
self.target
}
pub fn append<F: FnOnce(&mut Records)>(
&mut self,
metric: &Metric,
unit_name: Option<&str>,
values: F,
) {
if !self.format.supports_type(metric.metric_type) {
return;
}
if matches!(self.format, OutputFormat::Prometheus) {
self.target.push_str("# HELP ");
self.append_metric_name(metric, unit_name, None);
self.target.push(' ');
self.target.push_str(metric.help);
self.target.push('\n');
self.target.push_str("# TYPE ");
self.append_metric_name(metric, unit_name, None);
writeln!(&mut self.target, " {}", metric.metric_type).unwrap();
}
values(&mut Records {
target: self,
metric,
unit_name,
})
}
pub fn append_simple(
&mut self,
metric: &Metric,
unit_name: Option<&str>,
value: impl fmt::Display,
) {
self.append(metric, unit_name, |records| records.value(value))
}
fn append_metric_name(
&mut self,
metric: &Metric,
unit_name: Option<&str>,
suffix: Option<&str>,
) {
match self.format {
OutputFormat::Prometheus => match suffix {
Some(suffix) => {
write!(
&mut self.target,
"{}_{}_{}_{}",
PROMETHEUS_PREFIX, metric.name, metric.unit, suffix,
)
.unwrap();
}
None => {
write!(
&mut self.target,
"{}_{}_{}",
PROMETHEUS_PREFIX, metric.name, metric.unit
)
.unwrap();
}
},
OutputFormat::Plain => match unit_name {
Some(unit) => {
write!(&mut self.target, "{} {}", unit, metric.name)
.unwrap();
}
None => {
write!(&mut self.target, "{}", metric.name).unwrap();
}
},
#[cfg(test)]
OutputFormat::Test => {}
}
}
}
pub struct Records<'a> {
target: &'a mut Target,
metric: &'a Metric,
unit_name: Option<&'a str>,
}
impl<'b, 'a: 'b> Records<'a> {
pub fn value(&mut self, value: impl fmt::Display) {
self.suffixed_value(value, None)
}
pub fn suffixed_value(
&mut self,
value: impl fmt::Display,
suffix: Option<&str>,
) {
match self.target.format {
OutputFormat::Prometheus => {
self.target.append_metric_name(
self.metric,
self.unit_name,
suffix,
);
if let Some(unit_name) = self.unit_name {
write!(
&mut self.target.target,
"{{component=\"{}\"}}",
unit_name
)
.unwrap();
}
writeln!(&mut self.target.target, " {}", value).unwrap()
}
OutputFormat::Plain => {
self.target.append_metric_name(
self.metric,
self.unit_name,
suffix,
);
writeln!(&mut self.target.target, ": {}", value).unwrap()
}
#[cfg(test)]
OutputFormat::Test => {
let key = format!(
"{}|{}|{}",
self.metric.name,
self.metric.unit,
suffix.unwrap_or("None")
);
self.target
.raw
.insert(key.try_into().unwrap(), value.into());
}
}
}
pub fn label_value(
&mut self,
labels: &[(&str, &str)],
value: impl fmt::Display + Clone,
) {
self.suffixed_label_value(labels, value, None)
}
pub fn suffixed_label_value(
&mut self,
labels: &[(&str, &str)],
value: impl fmt::Display + Clone,
suffix: Option<&str>,
) {
match self.target.format {
OutputFormat::Prometheus => {
self.target.append_metric_name(
self.metric,
self.unit_name,
suffix,
);
self.target.target.push('{');
let mut comma = false;
if let Some(unit_name) = self.unit_name {
write!(
&mut self.target.target,
"component=\"{}\"",
unit_name
)
.unwrap();
comma = true;
}
for (name, value) in labels {
if comma {
write!(
&mut self.target.target,
",{}=\"{}\"",
name, value
)
.unwrap();
} else {
write!(
&mut self.target.target,
"{}=\"{}\"",
name, value
)
.unwrap();
comma = true;
}
}
writeln!(&mut self.target.target, "}} {}", value).unwrap()
}
OutputFormat::Plain => {
self.target.append_metric_name(
self.metric,
self.unit_name,
suffix,
);
for (name, value) in labels {
write!(&mut self.target.target, " {}={}", name, value)
.unwrap();
}
writeln!(&mut self.target.target, ": {}", value).unwrap()
}
#[cfg(test)]
OutputFormat::Test => {
let key = format!(
"{}|{}|{}|{}",
self.metric.name,
self.metric.unit,
suffix.unwrap_or("None"),
labels
.iter()
.map(|(ln, lv)| format!("{ln}={lv}"))
.collect::<Vec<_>>()
.join(",")
);
self.target
.raw
.insert(key.try_into().unwrap(), value.into());
}
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub enum OutputFormat {
Prometheus,
Plain,
#[cfg(test)]
Test,
}
impl OutputFormat {
#[allow(clippy::match_like_matches_macro)]
pub fn allows_text(self) -> bool {
match self {
OutputFormat::Prometheus => false,
OutputFormat::Plain => true,
#[cfg(test)]
OutputFormat::Test => true,
}
}
#[allow(clippy::match_like_matches_macro)]
pub fn supports_type(self, metric: MetricType) -> bool {
match (self, metric) {
(OutputFormat::Prometheus, MetricType::Text) => false,
_ => true,
}
}
}
pub struct Metric {
pub name: &'static str,
pub help: &'static str,
pub metric_type: MetricType,
pub unit: MetricUnit,
}
impl Metric {
pub const fn new(
name: &'static str,
help: &'static str,
metric_type: MetricType,
unit: MetricUnit,
) -> Self {
Metric {
name,
help,
metric_type,
unit,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum MetricType {
Counter,
Gauge,
Histogram,
Summary,
Text,
}
impl fmt::Display for MetricType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
MetricType::Counter => f.write_str("counter"),
MetricType::Gauge => f.write_str("gauge"),
MetricType::Histogram => f.write_str("histogram"),
MetricType::Summary => f.write_str("summary"),
MetricType::Text => f.write_str("text"),
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum MetricUnit {
Second,
Millisecond,
Microsecond,
Byte,
Total,
State,
Info,
}
impl fmt::Display for MetricUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
MetricUnit::Second => f.write_str("seconds"),
MetricUnit::Millisecond => f.write_str("milliseconds"),
MetricUnit::Microsecond => f.write_str("microseconds"),
MetricUnit::Byte => f.write_str("bytes"),
MetricUnit::Total => f.write_str("total"),
MetricUnit::Info => f.write_str("info"),
MetricUnit::State => f.write_str("state"),
}
}
}
impl TryFrom<&str> for MetricUnit {
type Error = String;
fn try_from(unit: &str) -> Result<Self, Self::Error> {
match unit.to_lowercase().as_str() {
"s" | "second" | "seconds" => Ok(MetricUnit::Second),
"ms" | "millisecond" | "milliseconds" => {
Ok(MetricUnit::Millisecond)
}
"µs" | "microsecond" | "microseconds" => {
Ok(MetricUnit::Microsecond)
}
"byte" | "bytes" => Ok(MetricUnit::Byte),
"total" => Ok(MetricUnit::Total),
"info" => Ok(MetricUnit::Info),
"state" => Ok(MetricUnit::State),
_ => Err(format!("Unknown metric unit '{unit}'")),
}
}
}
pub mod util {
use std::fmt::Display;
use super::{Metric, Target};
pub fn append_per_router_metric<K: AsRef<str>, V: Display + Clone>(
unit_name: &str,
target: &mut Target,
router_id: K,
metric: Metric,
metric_value: V,
) {
append_labelled_metric(
unit_name,
target,
"router",
router_id,
metric,
metric_value,
)
}
pub fn append_labelled_metric<
J: AsRef<str>,
K: AsRef<str>,
V: Display + Clone,
>(
unit_name: &str,
target: &mut Target,
label_id: J,
label_value: K,
metric: Metric,
metric_value: V,
) {
target.append(&metric, Some(unit_name), |records| {
records.label_value(
&[(label_id.as_ref(), label_value.as_ref())],
metric_value,
)
});
}
}