#![forbid(unsafe_code)]
#![deny(missing_docs)]
use crate::{
register_int_counter_vec,
register_int_gauge,
register_int_gauge_vec,
};
use crate::errors::ExporterError;
use crate::httpd::{
Collector,
HttpdError,
};
use jail::RunningJail;
use log::debug;
use prometheus::{
Encoder,
IntCounterVec,
IntGauge,
IntGaugeVec,
Registry,
TextEncoder,
};
use std::collections::HashMap;
use std::sync::{
Arc,
Mutex,
};
enum BookKept {
CpuTime(u64),
Wallclock(u64),
}
type CounterBookKeeper = HashMap<String, u64>;
type Rusage = HashMap<rctl::Resource, usize>;
type DeadJails = Vec<String>;
type SeenJails = Vec<String>;
type ExportedMetrics = Vec<u8>;
#[derive(Clone)]
pub struct Exporter {
registry: Registry,
coredumpsize_bytes: IntGaugeVec,
cputime_seconds_total: IntCounterVec,
datasize_bytes: IntGaugeVec,
memorylocked_bytes: IntGaugeVec,
memoryuse_bytes: IntGaugeVec,
msgqsize_bytes: IntGaugeVec,
maxproc: IntGaugeVec,
msgqqueued: IntGaugeVec,
nmsgq: IntGaugeVec,
nsem: IntGaugeVec,
nsemop: IntGaugeVec,
nshm: IntGaugeVec,
nthr: IntGaugeVec,
openfiles: IntGaugeVec,
pcpu_used: IntGaugeVec,
pseudoterminals: IntGaugeVec,
readbps: IntGaugeVec,
readiops: IntGaugeVec,
shmsize_bytes: IntGaugeVec,
stacksize_bytes: IntGaugeVec,
swapuse_bytes: IntGaugeVec,
vmemoryuse_bytes: IntGaugeVec,
wallclock_seconds_total: IntCounterVec,
writebps: IntGaugeVec,
writeiops: IntGaugeVec,
build_info: IntGaugeVec,
jail_id: IntGaugeVec,
jail_total: IntGauge,
cputime_seconds_total_old: Arc<Mutex<CounterBookKeeper>>,
wallclock_seconds_total_old: Arc<Mutex<CounterBookKeeper>>,
}
impl Default for Exporter {
fn default() -> Self {
let registry = Registry::new();
let labels: &[&str] = &["name"];
let metrics = Self {
coredumpsize_bytes: register_int_gauge_vec!(
registry,
"jail_coredumpsize_bytes",
"core dump size, in bytes",
labels
).unwrap(),
cputime_seconds_total: register_int_counter_vec!(
registry,
"jail_cputime_seconds_total",
"CPU time, in seconds",
labels
).unwrap(),
datasize_bytes: register_int_gauge_vec!(
registry,
"jail_datasize_bytes",
"data size, in bytes",
labels
).unwrap(),
maxproc: register_int_gauge_vec!(
registry,
"jail_maxproc",
"number of processes",
labels
).unwrap(),
memorylocked_bytes: register_int_gauge_vec!(
registry,
"jail_memorylocked_bytes",
"locked memory, in bytes",
labels
).unwrap(),
memoryuse_bytes: register_int_gauge_vec!(
registry,
"jail_memoryuse_bytes",
"resident set size, in bytes",
labels
).unwrap(),
msgqqueued: register_int_gauge_vec!(
registry,
"jail_msgqqueued",
"number of queued SysV messages",
labels
).unwrap(),
msgqsize_bytes: register_int_gauge_vec!(
registry,
"jail_msgqsize_bytes",
"SysV message queue size, in bytes",
labels
).unwrap(),
nmsgq: register_int_gauge_vec!(
registry,
"jail_nmsgq",
"number of SysV message queues",
labels
).unwrap(),
nsem: register_int_gauge_vec!(
registry,
"jail_nsem",
"number of SysV semaphores",
labels
).unwrap(),
nsemop: register_int_gauge_vec!(
registry,
"jail_nsemop",
"number of SysV semaphores modified in a single semop(2) call",
labels
).unwrap(),
nshm: register_int_gauge_vec!(
registry,
"jail_nshm",
"number of SysV shared memory segments",
labels
).unwrap(),
nthr: register_int_gauge_vec!(
registry,
"jail_nthr",
"number of threads",
labels
).unwrap(),
openfiles: register_int_gauge_vec!(
registry,
"jail_openfiles",
"file descriptor table size",
labels
).unwrap(),
pcpu_used: register_int_gauge_vec!(
registry,
"jail_pcpu_used",
"%CPU, in percents of a single CPU core",
labels
).unwrap(),
pseudoterminals: register_int_gauge_vec!(
registry,
"jail_pseudoterminals",
"number of PTYs",
labels
).unwrap(),
readbps: register_int_gauge_vec!(
registry,
"jail_readbps",
"filesystem reads, in bytes per second",
labels
).unwrap(),
readiops: register_int_gauge_vec!(
registry,
"jail_readiops",
"filesystem reads, in operations per second",
labels
).unwrap(),
shmsize_bytes: register_int_gauge_vec!(
registry,
"jail_shmsize_bytes",
"SysV shared memory size, in bytes",
labels
).unwrap(),
stacksize_bytes: register_int_gauge_vec!(
registry,
"jail_stacksize_bytes",
"stack size, in bytes",
labels
).unwrap(),
swapuse_bytes: register_int_gauge_vec!(
registry,
"jail_swapuse_bytes",
"swap space that may be reserved or used, in bytes",
labels
).unwrap(),
vmemoryuse_bytes: register_int_gauge_vec!(
registry,
"jail_vmemoryuse_bytes",
"address space limit, in bytes",
labels
).unwrap(),
wallclock_seconds_total: register_int_counter_vec!(
registry,
"jail_wallclock_seconds_total",
"wallclock time, in seconds",
labels
).unwrap(),
writebps: register_int_gauge_vec!(
registry,
"jail_writebps",
"filesystem writes, in bytes per second",
labels
).unwrap(),
writeiops: register_int_gauge_vec!(
registry,
"jail_writeiops",
"filesystem writes, in operations per second",
labels
).unwrap(),
build_info: register_int_gauge_vec!(
registry,
"jail_exporter_build_info",
"A metric with a constant '1' value labelled by version \
from which jail_exporter was built",
&["rustversion", "version"]
).unwrap(),
jail_id: register_int_gauge_vec!(
registry,
"jail_id",
"ID of the named jail.",
labels
).unwrap(),
jail_total: register_int_gauge!(
registry,
"jail_num",
"Current number of running jails."
).unwrap(),
registry: registry,
cputime_seconds_total_old: Arc::new(Mutex::new(
CounterBookKeeper::new()
)),
wallclock_seconds_total_old: Arc::new(Mutex::new(
CounterBookKeeper::new()
)),
};
let build_info_labels = [
env!("RUSTC_VERSION"),
env!("CARGO_PKG_VERSION"),
];
metrics.build_info.with_label_values(&build_info_labels).set(1);
metrics
}
}
impl Exporter {
pub fn new() -> Self {
Default::default()
}
pub fn export(&self) -> Result<ExportedMetrics, ExporterError> {
self.get_jail_metrics()?;
let metric_families = self.registry.gather();
let mut buffer = vec![];
let encoder = TextEncoder::new();
encoder.encode(&metric_families, &mut buffer)?;
Ok(buffer)
}
fn update_metric_book(&self, name: &str, resource: &BookKept) -> u64 {
let (mut book, value) = match *resource {
BookKept::CpuTime(v) => {
let book = self.cputime_seconds_total_old.lock().unwrap();
(book, v)
},
BookKept::Wallclock(v) => {
let book = self.wallclock_seconds_total_old.lock().unwrap();
(book, v)
},
};
let old_value = match book.get(name) {
None => 0,
Some(v) => *v,
};
let inc = if old_value <= value {
value - old_value
}
else {
value
};
book.insert(name.to_owned(), value);
inc
}
fn process_rusage(&self, name: &str, metrics: &Rusage) {
debug!("process_metrics_hash");
let labels: &[&str] = &[&name];
for (key, value) in metrics {
let value = *value as i64;
match key {
rctl::Resource::CoreDumpSize => {
self.coredumpsize_bytes
.with_label_values(labels)
.set(value);
},
rctl::Resource::CpuTime => {
let inc = self.update_metric_book(
&name,
&BookKept::CpuTime(value as u64)
);
self.cputime_seconds_total
.with_label_values(labels)
.inc_by(inc as u64);
},
rctl::Resource::DataSize => {
self.datasize_bytes.with_label_values(labels).set(value);
},
rctl::Resource::MaxProcesses => {
self.maxproc.with_label_values(labels).set(value);
},
rctl::Resource::MemoryLocked => {
self.memorylocked_bytes
.with_label_values(labels)
.set(value);
},
rctl::Resource::MemoryUse => {
self.memoryuse_bytes.with_label_values(labels).set(value);
},
rctl::Resource::MsgqQueued => {
self.msgqqueued.with_label_values(labels).set(value);
},
rctl::Resource::MsgqSize => {
self.msgqsize_bytes.with_label_values(labels).set(value);
},
rctl::Resource::NMsgq => {
self.nmsgq.with_label_values(labels).set(value);
},
rctl::Resource::Nsem => {
self.nsem.with_label_values(labels).set(value);
},
rctl::Resource::NSemop => {
self.nsemop.with_label_values(labels).set(value);
},
rctl::Resource::NShm => {
self.nshm.with_label_values(labels).set(value);
},
rctl::Resource::NThreads => {
self.nthr.with_label_values(labels).set(value);
},
rctl::Resource::OpenFiles => {
self.openfiles.with_label_values(labels).set(value);
},
rctl::Resource::PercentCpu => {
self.pcpu_used.with_label_values(labels).set(value);
},
rctl::Resource::PseudoTerminals => {
self.pseudoterminals.with_label_values(labels).set(value);
},
rctl::Resource::ReadBps => {
self.readbps.with_label_values(labels).set(value);
},
rctl::Resource::ReadIops => {
self.readiops.with_label_values(labels).set(value);
},
rctl::Resource::ShmSize => {
self.shmsize_bytes.with_label_values(labels).set(value);
},
rctl::Resource::StackSize => {
self.stacksize_bytes.with_label_values(labels).set(value);
},
rctl::Resource::SwapUse => {
self.swapuse_bytes.with_label_values(labels).set(value);
},
rctl::Resource::VMemoryUse => {
self.vmemoryuse_bytes.with_label_values(labels).set(value);
},
rctl::Resource::Wallclock => {
let inc = self.update_metric_book(
&name,
&BookKept::Wallclock(value as u64)
);
self.wallclock_seconds_total
.with_label_values(labels)
.inc_by(inc as u64);
},
rctl::Resource::WriteBps => {
self.writebps.with_label_values(labels).set(value)
},
rctl::Resource::WriteIops => {
self.writeiops.with_label_values(labels).set(value)
},
}
}
}
fn get_jail_metrics(&self) -> Result<(), ExporterError> {
debug!("get_jail_metrics");
self.jail_total.set(0);
let mut seen = SeenJails::new();
for jail in RunningJail::all() {
let name = jail.name()?;
let rusage = jail.racct_statistics()?;
debug!("JID: {}, Name: {:?}", jail.jid, name);
seen.push(name.to_owned());
self.process_rusage(&name, &rusage);
self.jail_id.with_label_values(&[&name]).set(i64::from(jail.jid));
self.jail_total.set(self.jail_total.get() + 1);
}
let dead = self.dead_jails(seen);
self.reap(dead);
Ok(())
}
fn dead_jails(&self, seen: SeenJails) -> DeadJails {
let book = self.cputime_seconds_total_old.lock().unwrap();
book
.keys()
.filter(|n| !seen.contains(&n))
.map(|n| n.to_owned())
.collect()
}
fn reap(&self, dead: DeadJails) {
for name in dead {
self.remove_jail_metrics(&name);
}
}
fn remove_jail_metrics(&self, name: &str) {
let labels: &[&str] = &[&name];
self.coredumpsize_bytes.remove_label_values(labels).ok();
self.cputime_seconds_total.remove_label_values(labels).ok();
self.datasize_bytes.remove_label_values(labels).ok();
self.maxproc.remove_label_values(labels).ok();
self.memorylocked_bytes.remove_label_values(labels).ok();
self.memoryuse_bytes.remove_label_values(labels).ok();
self.msgqqueued.remove_label_values(labels).ok();
self.msgqsize_bytes.remove_label_values(labels).ok();
self.nmsgq.remove_label_values(labels).ok();
self.nsem.remove_label_values(labels).ok();
self.nsemop.remove_label_values(labels).ok();
self.nshm.remove_label_values(labels).ok();
self.nthr.remove_label_values(labels).ok();
self.openfiles.remove_label_values(labels).ok();
self.pcpu_used.remove_label_values(labels).ok();
self.pseudoterminals.remove_label_values(labels).ok();
self.readbps.remove_label_values(labels).ok();
self.readiops.remove_label_values(labels).ok();
self.shmsize_bytes.remove_label_values(labels).ok();
self.stacksize_bytes.remove_label_values(labels).ok();
self.swapuse_bytes.remove_label_values(labels).ok();
self.vmemoryuse_bytes.remove_label_values(labels).ok();
self.wallclock_seconds_total.remove_label_values(labels).ok();
self.writebps.remove_label_values(labels).ok();
self.writeiops.remove_label_values(labels).ok();
self.jail_id.remove_label_values(labels).ok();
let books = [
&self.cputime_seconds_total_old,
&self.wallclock_seconds_total_old,
];
for book in books.iter() {
let mut book = book.lock().unwrap();
book.remove(name);
}
}
}
impl Collector for Exporter {
fn collect(&self) -> Result<Vec<u8>, HttpdError> {
self.export()
.map_err(|e| HttpdError::CollectorError(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn cputime_counter_increase() {
let names = ["test", "test2"];
let mut hash = Rusage::new();
let exporter = Exporter::new();
for name in names.iter() {
let series = exporter
.cputime_seconds_total
.with_label_values(&[&name]);
assert_eq!(series.get(), 0);
hash.insert(rctl::Resource::CpuTime, 1000);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1000);
hash.insert(rctl::Resource::CpuTime, 1020);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1020);
hash.insert(rctl::Resource::CpuTime, 10);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1030);
hash.insert(rctl::Resource::CpuTime, 50);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1070);
hash.insert(rctl::Resource::CpuTime, 50);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1070);
}
}
#[test]
fn dead_jails_ok() {
let names = ["test_a", "test_b", "test_c"];
let mut hash = Rusage::new();
let exporter = Exporter::new();
for name in names.iter() {
hash.insert(rctl::Resource::CpuTime, 1000);
exporter.process_rusage(&name, &hash);
}
let mut seen = SeenJails::new();
seen.push("test_a".into());
seen.push("test_c".into());
let dead = exporter.dead_jails(seen);
let ok: DeadJails = vec![
"test_b".into(),
];
assert_eq!(ok, dead);
}
#[test]
fn reap_ok() {
let names = ["test_a", "test_b", "test_c"];
let mut hash = Rusage::new();
let exporter = Exporter::new();
for name in names.iter() {
hash.insert(rctl::Resource::CpuTime, 1000);
exporter.process_rusage(&name, &hash);
}
let mut seen = SeenJails::new();
seen.push("test_a".into());
seen.push("test_c".into());
let dead_jail = "test_b";
let series = exporter
.cputime_seconds_total
.with_label_values(&[dead_jail]);
assert_eq!(series.get(), 1000);
let dead = exporter.dead_jails(seen);
exporter.reap(dead);
let series = exporter
.cputime_seconds_total
.with_label_values(&[dead_jail]);
assert_eq!(series.get(), 0);
}
#[test]
fn wallclock_counter_increase() {
let names = ["test", "test2"];
let mut hash = Rusage::new();
let exporter = Exporter::new();
for name in names.iter() {
let series = exporter
.wallclock_seconds_total
.with_label_values(&[&name]);
assert_eq!(series.get(), 0);
hash.insert(rctl::Resource::Wallclock, 1000);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1000);
hash.insert(rctl::Resource::Wallclock, 1020);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1020);
hash.insert(rctl::Resource::Wallclock, 10);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1030);
hash.insert(rctl::Resource::Wallclock, 50);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1070);
hash.insert(rctl::Resource::Wallclock, 50);
exporter.process_rusage(&name, &hash);
assert_eq!(series.get(), 1070);
}
}
}