pg_exporter 0.11.1

PostgreSQL metric exporter for Prometheus
Documentation
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
#![allow(clippy::indexing_slicing)]
use anyhow::Result;
use pg_exporter::collectors::config::CollectorConfig;

mod common;

fn collector_config(names: &[&str]) -> CollectorConfig {
    let enabled = names
        .iter()
        .map(|name| (*name).to_string())
        .collect::<Vec<_>>();
    CollectorConfig::new(25).with_enabled(&enabled)
}

#[tokio::test]
async fn test_metrics_endpoint_returns_prometheus_format() -> Result<()> {
    let port = common::get_available_port();
    let dsn = common::get_test_dsn_secret();

    let handle = tokio::spawn(async move {
        pg_exporter::exporter::new(port, None, dsn, collector_config(&["default"])).await
    });

    assert!(common::wait_for_server(port, 50).await);

    let client = reqwest::Client::new();
    let response = client
        .get(format!("{}/metrics", common::get_test_url(port)))
        .send()
        .await?;

    assert_eq!(response.status(), 200);

    let content_type = response
        .headers()
        .get("content-type")
        .expect("Content-Type header should be present");
    assert_eq!(content_type, "text/plain; charset=utf-8");

    let body = response.text().await?;

    assert!(body.contains("# HELP"));
    assert!(body.contains("# TYPE"));
    assert!(body.contains("pg_up"));

    handle.abort();

    Ok(())
}

#[tokio::test]
async fn test_metrics_endpoint_with_multiple_collectors() -> Result<()> {
    let port = common::get_available_port();
    let dsn = common::get_test_dsn_secret();

    let config = collector_config(&[
        "default",
        "activity",
        "vacuum",
        "database",
        "locks",
        "stat",
        "replication",
        "index",
        "statements",
    ]);

    let handle =
        tokio::spawn(async move { pg_exporter::exporter::new(port, None, dsn, config).await });

    assert!(common::wait_for_server(port, 50).await);

    let client = reqwest::Client::new();
    let response = client
        .get(format!("{}/metrics", common::get_test_url(port)))
        .send()
        .await?;

    assert_eq!(response.status(), 200);

    let body = response.text().await?;

    assert!(body.contains("pg_up"));
    assert!(body.contains("pg_stat_activity") || body.contains("pg_connections"));

    handle.abort();

    Ok(())
}

#[tokio::test]
async fn test_metrics_endpoint_performance() -> Result<()> {
    let port = common::get_available_port();
    let dsn = common::get_test_dsn_secret();

    let handle = tokio::spawn(async move {
        pg_exporter::exporter::new(port, None, dsn, collector_config(&["default"])).await
    });

    assert!(common::wait_for_server(port, 50).await);

    let client = reqwest::Client::new();

    for _ in 0..3 {
        let start = std::time::Instant::now();
        let response = client
            .get(format!("{}/metrics", common::get_test_url(port)))
            .send()
            .await?;

        let duration = start.elapsed();

        assert_eq!(response.status(), 200);

        assert!(
            duration.as_secs() < 5,
            "Metrics collection took too long: {duration:?}"
        );
    }

    handle.abort();

    Ok(())
}