use std::sync::LazyLock;
use regex::Regex;
use serde::{
de::{self, Error, MapAccess, Unexpected, Visitor},
Deserialize, Deserializer,
};
use crate::event::{Metric, MetricKind, MetricTags, MetricValue};
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Stats {
pub proc: Proc,
pub sys: Sys,
}
impl Stats {
pub fn metrics(&self, namespace: Option<String>) -> Vec<Metric> {
let mut result = Vec::new();
let mut tags = MetricTags::default();
let now = chrono::Utc::now();
let namespace = namespace.unwrap_or_else(|| "eventstoredb".to_string());
tags.replace("id".to_string(), self.proc.id.to_string());
result.push(
Metric::new(
"process_memory_used_bytes",
MetricKind::Absolute,
MetricValue::Gauge {
value: self.proc.mem as f64,
},
)
.with_namespace(Some(namespace.clone()))
.with_tags(Some(tags.clone()))
.with_timestamp(Some(now)),
);
result.push(
Metric::new(
"disk_read_bytes_total",
MetricKind::Absolute,
MetricValue::Counter {
value: self.proc.disk_io.read_bytes as f64,
},
)
.with_namespace(Some(namespace.clone()))
.with_tags(Some(tags.clone()))
.with_timestamp(Some(now)),
);
result.push(
Metric::new(
"disk_written_bytes_total",
MetricKind::Absolute,
MetricValue::Counter {
value: self.proc.disk_io.written_bytes as f64,
},
)
.with_namespace(Some(namespace.clone()))
.with_tags(Some(tags.clone()))
.with_timestamp(Some(now)),
);
result.push(
Metric::new(
"disk_read_ops_total",
MetricKind::Absolute,
MetricValue::Counter {
value: self.proc.disk_io.read_ops as f64,
},
)
.with_namespace(Some(namespace.clone()))
.with_tags(Some(tags.clone()))
.with_timestamp(Some(now)),
);
result.push(
Metric::new(
"disk_write_ops_total",
MetricKind::Absolute,
MetricValue::Counter {
value: self.proc.disk_io.write_ops as f64,
},
)
.with_namespace(Some(namespace.clone()))
.with_tags(Some(tags.clone()))
.with_timestamp(Some(now)),
);
result.push(
Metric::new(
"memory_free_bytes",
MetricKind::Absolute,
MetricValue::Gauge {
value: self.sys.free_mem as f64,
},
)
.with_namespace(Some(namespace.clone()))
.with_tags(Some(tags.clone()))
.with_timestamp(Some(now)),
);
if let Some(drive) = self.sys.drive.as_ref() {
tags.replace("path".to_string(), drive.path.clone());
result.push(
Metric::new(
"disk_total_bytes",
MetricKind::Absolute,
MetricValue::Gauge {
value: drive.stats.total_bytes as f64,
},
)
.with_namespace(Some(namespace.clone()))
.with_tags(Some(tags.clone()))
.with_timestamp(Some(now)),
);
result.push(
Metric::new(
"disk_free_bytes",
MetricKind::Absolute,
MetricValue::Gauge {
value: drive.stats.available_bytes as f64,
},
)
.with_namespace(Some(namespace.clone()))
.with_tags(Some(tags.clone()))
.with_timestamp(Some(now)),
);
result.push(
Metric::new(
"disk_used_bytes",
MetricKind::Absolute,
MetricValue::Gauge {
value: drive.stats.used_bytes as f64,
},
)
.with_namespace(Some(namespace))
.with_tags(Some(tags))
.with_timestamp(Some(now)),
);
}
result
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Proc {
pub id: usize,
pub mem: usize,
pub cpu: f64,
pub threads_count: i64,
pub thrown_exceptions_rate: f64,
pub disk_io: DiskIo,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DiskIo {
pub read_bytes: usize,
pub written_bytes: usize,
pub read_ops: usize,
pub write_ops: usize,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Sys {
pub free_mem: usize,
pub loadavg: LoadAvg,
pub drive: Option<Drive>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LoadAvg {
#[serde(rename = "1m")]
pub one_m: f64,
#[serde(rename = "5m")]
pub five_m: f64,
#[serde(rename = "15m")]
pub fifteen_m: f64,
}
#[derive(Debug)]
pub struct Drive {
pub path: String,
pub stats: DriveStats,
}
impl<'de> Deserialize<'de> for Drive {
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(DriveVisitor)
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DriveStats {
pub available_bytes: usize,
pub total_bytes: usize,
#[serde(deserialize_with = "percent_or_integer")]
pub usage: usize,
pub used_bytes: usize,
}
struct DriveVisitor;
impl<'de> Visitor<'de> for DriveVisitor {
type Value = Drive;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "DriveStats object")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, <A as MapAccess<'de>>::Error>
where
A: MapAccess<'de>,
{
if let Some(key) = map.next_key()? {
return Ok(Drive {
path: key,
stats: map.next_value()?,
});
}
Err(serde::de::Error::missing_field("<Drive path>"))
}
}
fn percent_or_integer<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: Deserializer<'de>,
{
struct PercentOrInteger;
static PERCENT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(\d+)%").unwrap());
impl<'de> Visitor<'de> for PercentOrInteger {
type Value = usize;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if let Some(caps) = PERCENT_REGEX.captures(value) {
caps[1].parse::<usize>().map_err(|err| {
Error::custom(format!("could not parse percent value into usize: {}", err))
})
} else {
Err(de::Error::invalid_value(
Unexpected::Str(value),
&"string did not contain a percent value like 30%",
))
}
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
usize::try_from(v).map_err(Error::custom)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
usize::try_from(v).map_err(Error::custom)
}
}
deserializer.deserialize_any(PercentOrInteger)
}