use anyhow::{Context, Result};
use prometheus::{Counter, Opts, Registry, TextEncoder};
use std::collections::{BTreeMap, HashMap};
use std::io::Read;
use std::net::{SocketAddr, TcpListener as StdTcpListener};
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tracing::{error, info};
pub struct PrometheusExporter {
registry: Registry,
bind_addr: Option<SocketAddr>,
running: Arc<tokio::sync::Mutex<bool>>,
}
impl PrometheusExporter {
pub fn new() -> Result<Self> {
let registry: _ = Registry::new();
info!("Prometheus exporter created");
Ok(Self {
registry,
bind_addr: None,
running: Arc::new(Mutex::new(tokio::sync::Mutex::new(false))),
})
}
pub fn new_with_registry(registry: Registry) -> Self {
info!("Prometheus exporter created with custom registry");
Self {
registry,
bind_addr: None,
running: Arc::new(Mutex::new(tokio::sync::Mutex::new(false))),
}
}
pub fn registry(&self) -> &Registry {
&self.registry
}
pub async fn start_server(&mut self, addr: SocketAddr) -> Result<()> {
info!("Starting Prometheus metrics server on {}", addr);
let listener: _ = TcpListener::bind(&addr)
.await
.context("Failed to bind to address")?;
self.bind_addr = Some(addr);
*self.running.lock().await = true;
info!("Prometheus metrics server listening on {}", addr);
loop {
let (stream, peer_addr) = match listener.accept().await {
Ok(conn) => conn,
Err(e) => {
error!("Failed to accept connection: {}", e);
continue;
}
};
let registry: _ = self.registry.clone();
let running: _ = self.running.clone();
tokio::spawn(async move {
if let Err(e) = handle_connection(stream, registry, running).await {
error!("Error handling connection from {}: {}", peer_addr, e);
}
});
}
}
pub async fn shutdown(&mut self) -> Result<()> {
info!("Shutting down Prometheus exporter...");
*self.running.lock().await = false;
Ok(())
}
pub fn gather_metrics(&self) -> Result<String> {
let metric_families: _ = self.registry.gather();
let encoder: _ = TextEncoder::new();
let metrics_text: _ = encoder
.encode_to_string(&metric_families)
.context("Failed to encode metrics")?;
Ok(metrics_text)
}
pub fn bind_addr(&self) -> Option<SocketAddr> {
self.bind_addr
}
pub async fn is_running(&self) -> bool {
*self.running.lock().await
}
}
impl Default for PrometheusExporter {
fn default() -> Self {
Self::new().expect("Failed to create PrometheusExporter")
}
}
async fn handle_connection(
mut stream: TcpStream,
registry: Registry,
running: Arc<tokio::sync::Mutex<bool>>,
) -> Result<()> {
let mut buffer = [0; 1024];
let _: _ = stream.read(&mut buffer).await?;
let request: _ = String::from_utf8_lossy(&buffer);
if !*running.lock().await {
return Ok(());
}
let path: _ = if let Some(first_line) = request.lines().next() {
first_line.split(' ').nth(1).unwrap_or("/")
} else {
"/"
};
match path {
"/metrics" => {
serve_metrics(&mut stream, ®istry).await?;
}
"/health" => {
serve_health_check(&mut stream).await?;
}
_ => {
serve_not_found(&mut stream).await?;
}
}
Ok(())
}
async fn serve_metrics(stream: &mut TcpStream, registry: &Registry) -> Result<()> {
let metric_families: _ = registry.gather();
let encoder: _ = prometheus::TextEncoder::new();
let metrics_text: _ = encoder
.encode_to_string(&metric_families)
.context("Failed to encode metrics")?;
let response: _ = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: {}\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n\
{}",
prometheus::TEXT_FORMAT,
metrics_text.len(),
metrics_text
);
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
async fn serve_health_check(stream: &mut TcpStream) -> Result<()> {
let response: _ = "HTTP/1.1 200 OK\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
\r\n\
{\"status\": \"healthy\"}";
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
async fn serve_not_found(stream: &mut TcpStream) -> Result<()> {
let response: _ = "HTTP/1.1 404 Not Found\r\n\
Content-Type: text/plain\r\n\
Connection: close\r\n\
\r\n\
Not Found";
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_prometheus_exporter_creation() {
let exporter: _ = PrometheusExporter::new();
assert!(exporter.is_ok());
}
#[tokio::test]
async fn test_gather_metrics() {
let mut exporter = PrometheusExporter::new().unwrap();
let counter_opts: _ = Opts::new("test_counter".to_string(), "Test counter".to_string());
let counter: _ = Counter::with_opts(counter_opts).unwrap();
exporter
.registry()
.register(Box::new(counter.clone()))
.unwrap();
counter.inc();
let metrics: _ = exporter.gather_metrics();
assert!(metrics.is_ok());
assert!(metrics.unwrap().contains("test_counter"));
}
#[tokio::test]
async fn test_custom_registry() {
let registry: _ = Registry::new();
let exporter: _ = PrometheusExporter::new_with_registry(registry);
let metrics: _ = exporter.registry().gather();
assert!(metrics.len() >= 0); }
}