use std::collections::{BTreeMap, BTreeSet};
use std::io::{self, prelude::*};
use std::net::{Ipv4Addr, TcpListener, TcpStream};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{LazyLock, Mutex};
use std::time::Duration;
use std::{fmt, thread};
use tracing::*;
struct State {
chan: Sender<(Metric, Action)>,
tracker: Mutex<Tracker>,
}
static STATE: LazyLock<State> = LazyLock::new(|| {
let (tx, rx) = channel();
let state = State {
chan: tx,
tracker: Mutex::new(Tracker {
metrics: BTreeMap::default(),
chan: rx,
}),
};
thread::Builder::new()
.name("epimetheus-drainer".into())
.spawn(move || {
loop {
thread::sleep(Duration::from_secs(20));
STATE.tracker.lock().unwrap().update();
}
})
.expect("Failed to spawn drainer thread");
if let Some(port) = http_port() {
match try_spawn_http_server_on(port) {
Ok(()) => (),
Err(e) => warn!("{e:#}"),
}
}
if let Some(path) = systemd_path() {
match try_spawn_systemd_server_at(&path) {
Ok(()) => (),
Err(e) => warn!("{e:#}"),
}
}
state
});
pub struct Metric {
pub name: &'static str, pub labels: Labels, }
type Labels = Vec<(&'static str, Box<dyn fmt::Display + Send>)>;
enum Action {
Inc(f64),
Set(f64),
Min(f64),
Max(f64),
}
impl Metric {
#[inline]
pub fn set(self, x: f64) {
send_chan((self, Action::Set(x)));
}
#[inline]
pub fn add(self, x: f64) {
send_chan((self, Action::Inc(x)));
}
#[inline]
pub fn min(self, x: f64) {
send_chan((self, Action::Min(x)));
}
#[inline]
pub fn max(self, x: f64) {
send_chan((self, Action::Max(x)));
}
}
impl From<&'static str> for Metric {
fn from(name: &'static str) -> Self {
Metric {
name,
labels: vec![],
}
}
}
impl Metric {
fn prepare(self) -> PreparedMetric {
let labels = self
.labels
.into_iter()
.map(|(k, v)| (k, v.to_string()))
.collect::<BTreeMap<_, _>>();
PreparedMetric {
name: self.name,
labels,
}
}
}
#[inline]
fn send_chan(x: (Metric, Action)) {
STATE.chan.send(x).unwrap();
}
#[macro_export]
macro_rules! metric {
($name:ident) => {
$crate::Metric {
name: stringify!($name),
labels: Vec::new(),
}
};
($name:ident{$($key:ident = $val:expr),*}) => {
$crate::Metric {
name: stringify!($name),
labels: vec![$((stringify!($key), Box::new($val))),*],
}
};
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
struct PreparedMetric {
name: &'static str,
labels: BTreeMap<&'static str, String>,
}
impl PreparedMetric {
fn prometheus<'a>(&'a self) -> PrometheusMetric<'a> {
PrometheusMetric {
name: self.name,
labels: self.labels.iter(),
}
}
fn json<'a>(&'a self, namespace: &'a str) -> JsonMetric<'a> {
JsonMetric {
name: self.name,
labels: self.labels.iter(),
namespace,
}
}
}
struct PrometheusMetric<'a> {
name: &'a str,
labels: std::collections::btree_map::Iter<'a, &'static str, String>,
}
impl<'a> fmt::Display for PrometheusMetric<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.name)?;
let mut labels = self.labels.clone();
if let Some((k, v)) = labels.next() {
write!(f, "{{{k}=\"{v}\"")?;
for (k, v) in labels {
write!(f, ",{k}=\"{v}\"")?;
}
f.write_str("}")?;
}
Ok(())
}
}
struct JsonMetric<'a> {
name: &'a str,
labels: std::collections::btree_map::Iter<'a, &'static str, String>,
namespace: &'a str,
}
impl<'a> fmt::Display for JsonMetric<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, r#""name":"{}.{}""#, self.namespace, self.name)?;
let mut labels = self.labels.clone();
if let Some(head) = labels.next() {
f.write_str(r#","fields":"#)?;
f.write_str("{\"")?;
f.write_str(head.0)?;
f.write_str("\":")?;
fmt::Debug::fmt(head.1, f)?;
for (k, v) in labels {
f.write_str(",\"")?;
f.write_str(k)?;
f.write_str("\":")?;
fmt::Debug::fmt(v, f)?;
}
f.write_str("}")?;
}
Ok(())
}
}
struct Tracker {
metrics: BTreeMap<PreparedMetric, f64>,
chan: Receiver<(Metric, Action)>,
}
impl Tracker {
fn update(&mut self) {
let mut n = 0.;
for (metric, action) in self.chan.try_iter() {
let metric = metric.prepare();
let entry = self.metrics.entry(metric).or_insert(0.0);
match action {
Action::Inc(x) => *entry += x,
Action::Set(x) => *entry = x,
Action::Min(x) => *entry = entry.min(x),
Action::Max(x) => *entry = entry.max(x),
}
n += 1.;
}
let total_updates = Metric::from("epimetheus_total_updates").prepare();
let total_flushes = Metric::from("epimetheus_total_flushes").prepare();
*self.metrics.entry(total_updates).or_insert(0.) += n;
*self.metrics.entry(total_flushes).or_insert(0.) += 1.;
}
}
pub fn query() -> impl Iterator<Item = (String, f64)> {
get_metrics()
.into_iter()
.map(|(metric, val)| (metric.prometheus().to_string(), val))
}
fn get_metrics() -> BTreeMap<PreparedMetric, f64> {
let mut tracker = STATE.tracker.lock().unwrap();
tracker.update();
tracker.metrics.clone()
}
fn http_port() -> Option<u16> {
match std::env::var("RUST_METRICS_PORT") {
Ok(x) => match x.parse::<u16>() {
Ok(port) => return Some(port),
Err(_) => warn!("RUST_METRICS_PORT present but not a valid port number"),
},
Err(std::env::VarError::NotPresent) => (),
Err(std::env::VarError::NotUnicode(_)) => {
warn!("RUST_METRICS_PORT present but not a valid port number")
}
}
None
}
fn systemd_path() -> Option<PathBuf> {
match std::env::var("RUST_METRICS_PATH") {
Ok(x) => return Some(PathBuf::from(x)),
Err(std::env::VarError::NotPresent) => (),
Err(std::env::VarError::NotUnicode(_)) => {
warn!("RUST_METRICS_PATH present but not a valid path")
}
}
None
}
fn try_spawn_http_server_on(port: u16) -> std::io::Result<()> {
let sock = TcpListener::bind((Ipv4Addr::LOCALHOST, port))?;
info!("Listening on port {port}");
std::thread::Builder::new()
.name("epimetheus-http".into())
.spawn(move || {
for conn in sock.incoming() {
if let Err(e) = conn.and_then(|conn| handle_http_client(conn, get_metrics())) {
warn!("{}", e);
}
}
})?;
Ok(())
}
fn handle_http_client(
conn: TcpStream,
metrics: BTreeMap<PreparedMetric, f64>,
) -> Result<(), std::io::Error> {
let mut conn = std::io::BufReader::with_capacity(128, conn);
let mut progress = 0;
for b in std::io::Read::by_ref(&mut conn).bytes() {
match b {
Ok(b) => match progress {
0 if b == b'\r' => progress = 1,
1 if b == b'\n' => progress = 2,
2 if b == b'\r' => progress = 3,
3 if b == b'\n' => break,
_ => progress = 0,
},
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => (),
Err(e) => return Err(e),
}
}
let mut conn = conn.into_inner();
writeln!(conn, "HTTP/1.1 200 OK\r\n")?;
for (metric, val) in metrics {
writeln!(conn, "{} {val}", metric.prometheus())?;
}
Ok(())
}
fn try_spawn_systemd_server_at(path: &Path) -> std::io::Result<()> {
if !path.starts_with("/run/systemd/report/") {
warn!(
"{}: Not in system-report's socket directory",
path.display()
);
}
let namespace = path
.file_name()
.and_then(|x| x.to_str())
.ok_or_else(|| io::Error::other(format!("{}: No filename", path.display())))?
.to_owned();
match std::fs::remove_file(path) {
Err(e) if e.kind() == io::ErrorKind::NotFound => (), Ok(()) => warn!("{}: already exists. Removing...", path.display()),
Err(e) => Err(e)?,
}
let sock = UnixListener::bind(path)?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o666))?;
info!("Listening at {}", path.display());
std::thread::Builder::new()
.name("epimetheus-systemd".into())
.spawn(move || {
for conn in sock.incoming() {
if let Err(e) =
conn.and_then(|conn| handle_systemd_client(conn, &namespace, get_metrics()))
{
warn!("{}", e);
}
}
})?;
Ok(())
}
fn handle_systemd_client(
mut conn: UnixStream,
namespace: &str,
metrics: BTreeMap<PreparedMetric, f64>,
) -> Result<(), std::io::Error> {
let mut method = vec![];
std::io::BufReader::new(&conn).read_until(0, &mut method)?;
method.pop(); let method = String::from_utf8(method).map_err(|e| io::Error::other(e.to_string()))?;
debug!(method, "Got connection");
let Some(method) = method
.strip_prefix(r#"{"method":"io.systemd.Metrics."#)
.and_then(|x| x.strip_suffix(r#"","more":true}"#))
else {
return Err(io::Error::other(format!("{method}: Unexpected interface")));
};
match method {
"Describe" => {
let names = metrics.keys().map(|x| x.name).collect::<BTreeSet<_>>();
for (i, name) in names.iter().enumerate() {
let continues = i + 1 != names.len();
let desc = "(Epimetheus doesn't allow users to describe their metrics)";
write!(
conn,
r#"{{"parameters":{{"name":"{namespace}.{}","description":"{desc}","type":"gauge"}},"continues":{continues}}}"#,
name,
)?;
conn.write_all(&[0])?;
}
}
"List" => {
for (i, (name, val)) in metrics.iter().enumerate() {
let continues = i + 1 != metrics.len();
write!(
conn,
r#"{{"parameters":{{{},"value":{val}}},"continues":{continues}}}"#,
name.json(namespace),
)?;
conn.write_all(&[0])?;
}
}
_ => return Err(io::Error::other(format!("{method}: Unexpected method"))),
}
let mut buf = vec![];
std::io::BufReader::new(&conn).read_until(0, &mut buf)?;
debug!("Client said: {buf:?}");
Ok(())
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn test_non_http() -> Result<(), Box<dyn std::error::Error>> {
metric!(foo).set(1.0);
metric!(bar).add(1.0);
metric!(bar).add(2.0);
assert_eq!(query().find(|(k, _)| k == "foo").map(|x| x.1), Some(1.0));
assert_eq!(query().find(|(k, _)| k == "bar").map(|x| x.1), Some(3.0));
assert_eq!(query().find(|(k, _)| k == "qux"), None);
metric!(bar).max(1.5);
assert_eq!(query().find(|(k, _)| k == "bar").map(|x| x.1), Some(3.0));
metric!(bar).min(1.5);
assert_eq!(query().find(|(k, _)| k == "bar").map(|x| x.1), Some(1.5));
Ok(())
}
#[test]
fn labels() -> Result<(), Box<dyn std::error::Error>> {
metric!(labels).set(1.0);
metric!(labels{user=1, admin=false}).set(1.0);
metric!(labels{user=2, name="Pete"}).set(1.0);
assert_eq!(
query()
.filter(|x| x.0.starts_with("labels"))
.collect::<Vec<_>>(),
[
r#"labels"#,
r#"labels{admin="false",user="1"}"#,
r#"labels{name="Pete",user="2"}"#,
]
.into_iter()
.map(|x| (x.to_string(), 1.0))
.collect::<Vec<_>>(),
);
Ok(())
}
}