#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![warn(clippy::unwrap_used)]
#![warn(rust_2018_idioms, unused_lifetimes, missing_debug_implementations)]
#[cfg(test)]
mod test;
pub use prometheus;
#[cfg(feature = "internal_metrics")]
use crate::prometheus::{
register_histogram,
register_int_counter,
register_int_gauge,
Histogram,
IntCounter,
IntGauge,
};
#[cfg(feature = "internal_metrics")]
use lazy_static::lazy_static;
#[cfg(feature = "logging")]
use log::{
error,
info,
};
use crate::prometheus::{
Encoder,
TextEncoder,
};
use std::{
net::SocketAddr,
sync::{
atomic::{
AtomicBool,
Ordering,
},
mpsc::{
sync_channel,
Receiver,
SyncSender,
},
Arc,
Barrier,
Mutex,
MutexGuard,
},
thread,
time::Duration,
};
use thiserror::Error;
use tiny_http::{
Header,
Request,
Response,
Server as HTTPServer,
};
#[cfg(feature = "internal_metrics")]
lazy_static! {
static ref HTTP_COUNTER: IntCounter = register_int_counter!(
"prometheus_exporter_requests_total",
"Number of HTTP requests received."
)
.expect("can not create HTTP_COUNTER metric. this should never fail");
static ref HTTP_BODY_GAUGE: IntGauge = register_int_gauge!(
"prometheus_exporter_response_size_bytes",
"The HTTP response sizes in bytes."
)
.expect("can not create HTTP_BODY_GAUGE metric. this should never fail");
static ref HTTP_REQ_HISTOGRAM: Histogram = register_histogram!(
"prometheus_exporter_request_duration_seconds",
"The HTTP request latencies in seconds."
)
.expect("can not create HTTP_REQ_HISTOGRAM metric. this should never fail");
}
#[derive(Debug, Error)]
pub enum Error {
#[error("can not start http server: {0}")]
ServerStart(Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("supplied endpoint is not valid ascii: {0}")]
EndpointNotAscii(String),
}
#[derive(Debug, Error)]
enum HandlerError {
#[error("can not encode metrics: {0}")]
EncodeMetrics(prometheus::Error),
#[error("can not generate response: {0}")]
Response(std::io::Error),
}
#[derive(Debug)]
pub struct Builder {
binding: SocketAddr,
endpoint: Endpoint,
registry: prometheus::Registry,
}
#[derive(Debug)]
struct Endpoint(String);
impl Default for Endpoint {
fn default() -> Self {
Self("/metrics".to_string())
}
}
#[derive(Debug)]
pub struct Exporter {
request_receiver: Receiver<Arc<Barrier>>,
is_waiting: Arc<AtomicBool>,
update_lock: Arc<Mutex<()>>,
}
#[derive(Debug)]
struct Server {}
pub fn start(binding: SocketAddr) -> Result<Exporter, Error> {
Builder::new(binding).start()
}
impl Builder {
#[must_use]
pub fn new(binding: SocketAddr) -> Builder {
Self {
binding,
endpoint: Endpoint::default(),
registry: prometheus::default_registry().clone(),
}
}
pub fn with_endpoint(&mut self, endpoint: &str) -> Result<(), Error> {
if !endpoint.is_ascii() {
return Err(Error::EndpointNotAscii(endpoint.to_string()));
}
let mut clean_endpoint = String::from('/');
clean_endpoint.push_str(endpoint.trim_matches('/'));
self.endpoint = Endpoint(clean_endpoint);
Ok(())
}
pub fn with_registry(&mut self, registry: prometheus::Registry) {
self.registry = registry;
}
pub fn start(self) -> Result<Exporter, Error> {
let (request_sender, request_receiver) = sync_channel(0);
let is_waiting = Arc::new(AtomicBool::new(false));
let update_lock = Arc::new(Mutex::new(()));
let exporter = Exporter {
request_receiver,
is_waiting: Arc::clone(&is_waiting),
update_lock: Arc::clone(&update_lock),
};
Server::start(
self.binding,
self.endpoint.0,
request_sender,
is_waiting,
update_lock,
self.registry,
)?;
Ok(exporter)
}
}
impl Exporter {
#[must_use]
pub fn builder(binding: SocketAddr) -> Builder {
Builder::new(binding)
}
#[must_use = "not using the guard will result in the exporter returning the prometheus data \
immediately over http"]
pub fn wait_request(&self) -> MutexGuard<'_, ()> {
self.is_waiting.store(true, Ordering::SeqCst);
let update_waitgroup = self
.request_receiver
.recv()
.expect("can not receive from request_receiver channel. this should never happen");
self.is_waiting.store(false, Ordering::SeqCst);
let guard = self
.update_lock
.lock()
.expect("poisioned mutex. should never happen");
update_waitgroup.wait();
guard
}
#[must_use = "not using the guard will result in the exporter returning the prometheus data \
immediately over http"]
pub fn wait_duration(&self, duration: Duration) -> MutexGuard<'_, ()> {
thread::sleep(duration);
self.update_lock
.lock()
.expect("poisioned mutex. should never happen")
}
}
impl Server {
fn start(
binding: SocketAddr,
endpoint: String,
request_sender: SyncSender<Arc<Barrier>>,
is_waiting: Arc<AtomicBool>,
update_lock: Arc<Mutex<()>>,
registry: prometheus::Registry,
) -> Result<(), Error> {
let server = HTTPServer::http(&binding).map_err(Error::ServerStart)?;
thread::spawn(move || {
#[cfg(feature = "logging")]
info!("exporting metrics to http://{}{}", binding, endpoint);
let encoder = TextEncoder::new();
for request in server.incoming_requests() {
if let Err(err) = if request.url() == endpoint {
Self::handler_metrics(
request,
&encoder,
&request_sender,
&is_waiting,
&update_lock,
®istry,
)
} else {
Self::handler_redirect(request, &endpoint)
} {
#[cfg(feature = "logging")]
error!("{}", err);
drop(err);
}
}
});
Ok(())
}
fn handler_metrics(
request: Request,
encoder: &TextEncoder,
request_sender: &SyncSender<Arc<Barrier>>,
is_waiting: &Arc<AtomicBool>,
update_lock: &Arc<Mutex<()>>,
registry: &prometheus::Registry,
) -> Result<(), HandlerError> {
#[cfg(feature = "internal_metrics")]
HTTP_COUNTER.inc();
#[cfg(feature = "internal_metrics")]
let timer = HTTP_REQ_HISTOGRAM.start_timer();
if is_waiting.load(Ordering::SeqCst) {
let barrier = Arc::new(Barrier::new(2));
request_sender
.send(Arc::clone(&barrier))
.expect("can not send to request_sender. this should never happen");
barrier.wait();
}
let _lock = update_lock
.lock()
.expect("poisioned mutex. should never happen");
#[cfg(feature = "internal_metrics")]
drop(timer);
Self::process_request(request, encoder, registry)
}
fn process_request(
request: Request,
encoder: &TextEncoder,
registry: &prometheus::Registry,
) -> Result<(), HandlerError> {
let metric_families = registry.gather();
let mut buffer = vec![];
encoder
.encode(&metric_families, &mut buffer)
.map_err(HandlerError::EncodeMetrics)?;
#[cfg(feature = "internal_metrics")]
HTTP_BODY_GAUGE.set(buffer.len() as i64);
let response = Response::from_data(buffer);
request.respond(response).map_err(HandlerError::Response)?;
Ok(())
}
fn handler_redirect(request: Request, endpoint: &str) -> Result<(), HandlerError> {
let response = Response::from_string(format!("try {} for metrics\n", endpoint))
.with_status_code(301)
.with_header(Header {
field: "Location"
.parse()
.expect("can not parse location header field. this should never fail"),
value: ascii::AsciiString::from_ascii(endpoint)
.expect("can not parse header value. this should never fail"),
});
request.respond(response).map_err(HandlerError::Response)?;
Ok(())
}
}