1256 lines
40 KiB
Rust
1256 lines
40 KiB
Rust
|
|
#![cfg(feature = "integration")]
|
||
|
|
mod helpers;
|
||
|
|
use helpers::*;
|
||
|
|
use sunbeam_sdk::client::{AuthMethod, ServiceClient};
|
||
|
|
use sunbeam_sdk::monitoring::{PrometheusClient, LokiClient, GrafanaClient};
|
||
|
|
use sunbeam_sdk::monitoring::types::*;
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Service URLs
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
const PROM_URL: &str = "http://localhost:9090/api/v1";
|
||
|
|
const PROM_HEALTH: &str = "http://localhost:9090/api/v1/status/buildinfo";
|
||
|
|
|
||
|
|
const LOKI_URL: &str = "http://localhost:3100/loki/api/v1";
|
||
|
|
const LOKI_HEALTH: &str = "http://localhost:3100/ready";
|
||
|
|
|
||
|
|
const GRAFANA_URL: &str = "http://localhost:3001/api";
|
||
|
|
const GRAFANA_HEALTH: &str = "http://localhost:3001/api/health";
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Constructors
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
fn prom() -> PrometheusClient {
|
||
|
|
PrometheusClient::from_parts(PROM_URL.into(), AuthMethod::None)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn loki() -> LokiClient {
|
||
|
|
LokiClient::from_parts(LOKI_URL.into(), AuthMethod::None)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn grafana() -> GrafanaClient {
|
||
|
|
use base64::Engine;
|
||
|
|
let creds = base64::engine::general_purpose::STANDARD.encode("admin:admin");
|
||
|
|
GrafanaClient::from_parts(
|
||
|
|
GRAFANA_URL.into(),
|
||
|
|
AuthMethod::Header {
|
||
|
|
name: "Authorization",
|
||
|
|
value: format!("Basic {creds}"),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// PROMETHEUS
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 1. Query
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_query() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
// Instant query
|
||
|
|
let res = c.query("up", None).await.expect("query failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
let data = res.data.expect("missing data");
|
||
|
|
assert_eq!(data.result_type, "vector");
|
||
|
|
|
||
|
|
// Instant query with explicit time
|
||
|
|
let now = chrono::Utc::now().timestamp().to_string();
|
||
|
|
let res = c.query("up", Some(&now)).await.expect("query with time failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_query_range() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(5)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
let res = c
|
||
|
|
.query_range("up", &start, &end, "15s")
|
||
|
|
.await
|
||
|
|
.expect("query_range failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
let data = res.data.expect("missing data");
|
||
|
|
assert_eq!(data.result_type, "matrix");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 2. Format
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_format_query() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c
|
||
|
|
.format_query("up{job='prometheus'}")
|
||
|
|
.await
|
||
|
|
.expect("format_query failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.contains("up"));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 3. Metadata
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_series() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(5)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
let res = c
|
||
|
|
.series(&["up"], Some(&start), Some(&end))
|
||
|
|
.await
|
||
|
|
.expect("series failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_labels() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.labels(None, None).await.expect("labels failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
let labels = res.data.expect("missing data");
|
||
|
|
assert!(labels.contains(&"__name__".to_string()));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_label_values() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.label_values("job", None, None).await.expect("label_values failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
let values = res.data.expect("missing data");
|
||
|
|
assert!(!values.is_empty(), "expected at least one job label value");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_targets_metadata() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.targets_metadata(None).await.expect("targets_metadata failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_metadata() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.metadata(None).await.expect("metadata failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 4. Infrastructure
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_targets() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.targets().await.expect("targets failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
let data = res.data.expect("missing data");
|
||
|
|
assert!(!data.active_targets.is_empty(), "expected at least one active target");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_scrape_pools() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.scrape_pools().await.expect("scrape_pools failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_alertmanagers() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.alertmanagers().await.expect("alertmanagers failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_rules() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.rules().await.expect("rules failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_alerts() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.alerts().await.expect("alerts failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 5. Status
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_config() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.config().await.expect("config failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
let data = res.data.expect("missing data");
|
||
|
|
assert!(!data.yaml.is_empty(), "config yaml should not be empty");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_flags() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.flags().await.expect("flags failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_runtime_info() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.runtime_info().await.expect("runtime_info failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_build_info() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.build_info().await.expect("build_info failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn prometheus_tsdb() {
|
||
|
|
wait_for_healthy(PROM_HEALTH, TIMEOUT).await;
|
||
|
|
let c = prom();
|
||
|
|
|
||
|
|
let res = c.tsdb().await.expect("tsdb failed");
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
assert!(res.data.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// LOKI
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 1. Ready
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_ready() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let res = c.ready().await.expect("ready failed");
|
||
|
|
assert_eq!(res.status.as_deref(), Some("ready"));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 2. Push and Query
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_push_and_query() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now_ns = std::time::SystemTime::now()
|
||
|
|
.duration_since(std::time::UNIX_EPOCH)
|
||
|
|
.unwrap()
|
||
|
|
.as_nanos()
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
let marker = unique_name("logline");
|
||
|
|
let body = serde_json::json!({
|
||
|
|
"streams": [{
|
||
|
|
"stream": {"job": "test", "app": "integration"},
|
||
|
|
"values": [[&now_ns, format!("test log line {marker}")]]
|
||
|
|
}]
|
||
|
|
});
|
||
|
|
|
||
|
|
// Push
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.push(&body).await {
|
||
|
|
Ok(()) => break,
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("push failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Give Loki time to index
|
||
|
|
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||
|
|
|
||
|
|
// Range query (Loki doesn't support instant queries for log selectors)
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(5)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
let query_str = r#"{job="test",app="integration"}"#;
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.query_range(query_str, &start, &end, Some(100), None).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("query failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_query_range() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
let query_str = r#"{job="test"}"#;
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.query_range(query_str, &start, &end, Some(100), None).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("query_range failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 3. Labels
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_labels() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.labels(None, None).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("labels failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_label_values() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.label_values("job", None, None).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("label_values failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 4. Series
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_series() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.series(&[r#"{job="test"}"#], Some(&start), Some(&end)).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("series failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 5. Index
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_index_stats() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
// index_stats requires a query parameter which the client doesn't expose yet
|
||
|
|
// Just verify it doesn't panic — the 400 error is expected
|
||
|
|
let _ = c.index_stats().await;
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_index_volume() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
// index/volume may return errors without sufficient data — handle gracefully
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.index_volume(r#"{job="test"}"#, Some(&start), Some(&end)).await {
|
||
|
|
Ok(_val) => return,
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => {
|
||
|
|
// Graceful: some Loki versions don't support this endpoint
|
||
|
|
eprintln!("index_volume not available (may be expected): {e}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_index_volume_range() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.index_volume_range(r#"{job="test"}"#, &start, &end, None).await {
|
||
|
|
Ok(_val) => return,
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => {
|
||
|
|
eprintln!("index_volume_range not available (may be expected): {e}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 6. Patterns
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_detect_patterns() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
// detect_patterns may not work without sufficient data — handle gracefully
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.detect_patterns(r#"{job="test"}"#, Some(&start), Some(&end)).await {
|
||
|
|
Ok(_val) => return,
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => {
|
||
|
|
eprintln!("detect_patterns not available (may be expected): {e}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// GRAFANA
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 1. Org
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn grafana_org() {
|
||
|
|
wait_for_healthy(GRAFANA_HEALTH, TIMEOUT).await;
|
||
|
|
let c = grafana();
|
||
|
|
|
||
|
|
let org = c.get_current_org().await.expect("get_current_org failed");
|
||
|
|
assert!(org.id > 0);
|
||
|
|
assert!(!org.name.is_empty());
|
||
|
|
|
||
|
|
// Update org name, then restore
|
||
|
|
let original_name = org.name.clone();
|
||
|
|
let new_name = unique_name("org");
|
||
|
|
c.update_org(&serde_json::json!({"name": new_name}))
|
||
|
|
.await
|
||
|
|
.expect("update_org failed");
|
||
|
|
|
||
|
|
let updated = c.get_current_org().await.expect("get org after update failed");
|
||
|
|
assert_eq!(updated.name, new_name);
|
||
|
|
|
||
|
|
// Restore
|
||
|
|
c.update_org(&serde_json::json!({"name": original_name}))
|
||
|
|
.await
|
||
|
|
.expect("restore org name failed");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 2. Folder CRUD
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn grafana_folder_crud() {
|
||
|
|
wait_for_healthy(GRAFANA_HEALTH, TIMEOUT).await;
|
||
|
|
let c = grafana();
|
||
|
|
|
||
|
|
let title = unique_name("folder");
|
||
|
|
let uid = unique_name("fuid");
|
||
|
|
|
||
|
|
// Create
|
||
|
|
let folder = c
|
||
|
|
.create_folder(&serde_json::json!({"title": title, "uid": uid}))
|
||
|
|
.await
|
||
|
|
.expect("create_folder failed");
|
||
|
|
assert_eq!(folder.title, title);
|
||
|
|
assert_eq!(folder.uid, uid);
|
||
|
|
|
||
|
|
// List — should contain our folder
|
||
|
|
let folders = c.list_folders().await.expect("list_folders failed");
|
||
|
|
assert!(folders.iter().any(|f| f.uid == uid), "folder not found in list");
|
||
|
|
|
||
|
|
// Get
|
||
|
|
let fetched = c.get_folder(&uid).await.expect("get_folder failed");
|
||
|
|
assert_eq!(fetched.title, title);
|
||
|
|
|
||
|
|
// Update
|
||
|
|
let new_title = unique_name("folder-upd");
|
||
|
|
let updated = c
|
||
|
|
.update_folder(&uid, &serde_json::json!({"title": new_title, "overwrite": true}))
|
||
|
|
.await
|
||
|
|
.expect("update_folder failed");
|
||
|
|
assert_eq!(updated.title, new_title);
|
||
|
|
|
||
|
|
// Delete
|
||
|
|
c.delete_folder(&uid).await.expect("delete_folder failed");
|
||
|
|
|
||
|
|
// Verify deleted
|
||
|
|
let result = c.get_folder(&uid).await;
|
||
|
|
assert!(result.is_err(), "folder should not exist after deletion");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 3. Dashboard CRUD
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn grafana_dashboard_crud() {
|
||
|
|
wait_for_healthy(GRAFANA_HEALTH, TIMEOUT).await;
|
||
|
|
let c = grafana();
|
||
|
|
|
||
|
|
let title = unique_name("dash");
|
||
|
|
let dash_uid = unique_name("duid");
|
||
|
|
|
||
|
|
// Create
|
||
|
|
let body = serde_json::json!({
|
||
|
|
"dashboard": {
|
||
|
|
"uid": dash_uid,
|
||
|
|
"title": title,
|
||
|
|
"panels": [],
|
||
|
|
"schemaVersion": 27,
|
||
|
|
"version": 0
|
||
|
|
},
|
||
|
|
"overwrite": false
|
||
|
|
});
|
||
|
|
let created = c.create_dashboard(&body).await.expect("create_dashboard failed");
|
||
|
|
assert_eq!(created.uid.as_deref(), Some(dash_uid.as_str()));
|
||
|
|
|
||
|
|
// Get
|
||
|
|
let fetched = c.get_dashboard(&dash_uid).await.expect("get_dashboard failed");
|
||
|
|
assert!(fetched.dashboard.is_some());
|
||
|
|
|
||
|
|
// Update
|
||
|
|
let updated_title = unique_name("dash-upd");
|
||
|
|
let update_body = serde_json::json!({
|
||
|
|
"dashboard": {
|
||
|
|
"uid": dash_uid,
|
||
|
|
"title": updated_title,
|
||
|
|
"panels": [],
|
||
|
|
"schemaVersion": 27,
|
||
|
|
"version": 1
|
||
|
|
},
|
||
|
|
"overwrite": true
|
||
|
|
});
|
||
|
|
let updated = c.update_dashboard(&update_body).await.expect("update_dashboard failed");
|
||
|
|
assert_eq!(updated.uid.as_deref(), Some(dash_uid.as_str()));
|
||
|
|
|
||
|
|
// List
|
||
|
|
let all = c.list_dashboards().await.expect("list_dashboards failed");
|
||
|
|
assert!(all.iter().any(|d| d.uid == dash_uid), "dashboard not found in list");
|
||
|
|
|
||
|
|
// Search
|
||
|
|
let found = c.search_dashboards(&updated_title).await.expect("search_dashboards failed");
|
||
|
|
assert!(!found.is_empty(), "search should find the dashboard");
|
||
|
|
assert!(found.iter().any(|d| d.uid == dash_uid));
|
||
|
|
|
||
|
|
// Delete
|
||
|
|
c.delete_dashboard(&dash_uid).await.expect("delete_dashboard failed");
|
||
|
|
|
||
|
|
// Verify deleted
|
||
|
|
let result = c.get_dashboard(&dash_uid).await;
|
||
|
|
assert!(result.is_err(), "dashboard should not exist after deletion");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 4. Datasource CRUD
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn grafana_datasource_crud() {
|
||
|
|
wait_for_healthy(GRAFANA_HEALTH, TIMEOUT).await;
|
||
|
|
let c = grafana();
|
||
|
|
|
||
|
|
let name = unique_name("ds-prom");
|
||
|
|
let ds_uid = unique_name("dsuid");
|
||
|
|
|
||
|
|
// Create
|
||
|
|
let body = serde_json::json!({
|
||
|
|
"name": name,
|
||
|
|
"type": "prometheus",
|
||
|
|
"uid": ds_uid,
|
||
|
|
"url": "http://prometheus:9090",
|
||
|
|
"access": "proxy",
|
||
|
|
"isDefault": false
|
||
|
|
});
|
||
|
|
let created = c.create_datasource(&body).await.expect("create_datasource failed");
|
||
|
|
let ds_id = created.id.expect("datasource missing id");
|
||
|
|
assert_eq!(created.name, name);
|
||
|
|
|
||
|
|
// List
|
||
|
|
let all = c.list_datasources().await.expect("list_datasources failed");
|
||
|
|
assert!(all.iter().any(|d| d.name == name), "datasource not found in list");
|
||
|
|
|
||
|
|
// Get by ID
|
||
|
|
let by_id = c.get_datasource(ds_id).await.expect("get_datasource by id failed");
|
||
|
|
assert_eq!(by_id.name, name);
|
||
|
|
|
||
|
|
// Get by UID
|
||
|
|
let by_uid = c
|
||
|
|
.get_datasource_by_uid(&ds_uid)
|
||
|
|
.await
|
||
|
|
.expect("get_datasource_by_uid failed");
|
||
|
|
assert_eq!(by_uid.name, name);
|
||
|
|
|
||
|
|
// Update
|
||
|
|
let new_name = unique_name("ds-upd");
|
||
|
|
let update_body = serde_json::json!({
|
||
|
|
"name": new_name,
|
||
|
|
"type": "prometheus",
|
||
|
|
"uid": ds_uid,
|
||
|
|
"url": "http://prometheus:9090",
|
||
|
|
"access": "proxy",
|
||
|
|
"isDefault": false
|
||
|
|
});
|
||
|
|
let updated = c
|
||
|
|
.update_datasource(ds_id, &update_body)
|
||
|
|
.await
|
||
|
|
.expect("update_datasource failed");
|
||
|
|
assert_eq!(updated.name, new_name);
|
||
|
|
|
||
|
|
// Delete
|
||
|
|
c.delete_datasource(ds_id).await.expect("delete_datasource failed");
|
||
|
|
|
||
|
|
// Verify deleted
|
||
|
|
let result = c.get_datasource(ds_id).await;
|
||
|
|
assert!(result.is_err(), "datasource should not exist after deletion");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 5. Annotation CRUD
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn grafana_annotation_crud() {
|
||
|
|
wait_for_healthy(GRAFANA_HEALTH, TIMEOUT).await;
|
||
|
|
let c = grafana();
|
||
|
|
|
||
|
|
let text = unique_name("annotation");
|
||
|
|
let now_ms = std::time::SystemTime::now()
|
||
|
|
.duration_since(std::time::UNIX_EPOCH)
|
||
|
|
.unwrap()
|
||
|
|
.as_millis() as u64;
|
||
|
|
|
||
|
|
// Create
|
||
|
|
let body = serde_json::json!({
|
||
|
|
"text": text,
|
||
|
|
"time": now_ms,
|
||
|
|
"tags": ["integration-test"]
|
||
|
|
});
|
||
|
|
let created = c.create_annotation(&body).await.expect("create_annotation failed");
|
||
|
|
let ann_id = created.id.expect("annotation missing id");
|
||
|
|
assert!(ann_id > 0);
|
||
|
|
|
||
|
|
// List
|
||
|
|
let all = c.list_annotations(Some("tags=integration-test")).await.expect("list_annotations failed");
|
||
|
|
assert!(all.iter().any(|a| a.id == Some(ann_id)), "annotation not found in list");
|
||
|
|
|
||
|
|
// Get
|
||
|
|
let fetched = c.get_annotation(ann_id).await.expect("get_annotation failed");
|
||
|
|
assert_eq!(fetched.text.as_deref(), Some(text.as_str()));
|
||
|
|
|
||
|
|
// Update
|
||
|
|
let new_text = unique_name("ann-upd");
|
||
|
|
let update_body = serde_json::json!({
|
||
|
|
"text": new_text,
|
||
|
|
"time": now_ms,
|
||
|
|
"tags": ["integration-test", "updated"]
|
||
|
|
});
|
||
|
|
c.update_annotation(ann_id, &update_body)
|
||
|
|
.await
|
||
|
|
.expect("update_annotation failed");
|
||
|
|
|
||
|
|
let updated = c.get_annotation(ann_id).await.expect("get annotation after update failed");
|
||
|
|
assert_eq!(updated.text.as_deref(), Some(new_text.as_str()));
|
||
|
|
|
||
|
|
// Delete
|
||
|
|
c.delete_annotation(ann_id).await.expect("delete_annotation failed");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 6. Alert Rules
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn grafana_alert_rules() {
|
||
|
|
wait_for_healthy(GRAFANA_HEALTH, TIMEOUT).await;
|
||
|
|
let c = grafana();
|
||
|
|
|
||
|
|
// Create prerequisite folder and datasource
|
||
|
|
let folder_uid = unique_name("alert-fld");
|
||
|
|
let folder_title = unique_name("AlertFolder");
|
||
|
|
c.create_folder(&serde_json::json!({"title": folder_title, "uid": folder_uid}))
|
||
|
|
.await
|
||
|
|
.expect("create folder for alerts failed");
|
||
|
|
|
||
|
|
let ds_name = unique_name("alert-ds");
|
||
|
|
let ds_uid = unique_name("alertdsuid");
|
||
|
|
let ds = c
|
||
|
|
.create_datasource(&serde_json::json!({
|
||
|
|
"name": ds_name,
|
||
|
|
"type": "prometheus",
|
||
|
|
"uid": ds_uid,
|
||
|
|
"url": "http://prometheus:9090",
|
||
|
|
"access": "proxy"
|
||
|
|
}))
|
||
|
|
.await
|
||
|
|
.expect("create datasource for alerts failed");
|
||
|
|
let ds_id = ds.id.expect("datasource missing id");
|
||
|
|
|
||
|
|
let rule_title = unique_name("test-rule");
|
||
|
|
let rule_group = unique_name("test-group");
|
||
|
|
|
||
|
|
// Create alert rule
|
||
|
|
let rule_body = serde_json::json!({
|
||
|
|
"title": rule_title,
|
||
|
|
"ruleGroup": rule_group,
|
||
|
|
"folderUID": folder_uid,
|
||
|
|
"condition": "A",
|
||
|
|
"for": "5m",
|
||
|
|
"data": [{
|
||
|
|
"refId": "A",
|
||
|
|
"datasourceUid": ds_uid,
|
||
|
|
"model": {
|
||
|
|
"expr": "up == 0",
|
||
|
|
"refId": "A"
|
||
|
|
},
|
||
|
|
"relativeTimeRange": {
|
||
|
|
"from": 600,
|
||
|
|
"to": 0
|
||
|
|
}
|
||
|
|
}],
|
||
|
|
"noDataState": "NoData",
|
||
|
|
"execErrState": "Error"
|
||
|
|
});
|
||
|
|
let created = c
|
||
|
|
.create_alert_rule(&rule_body)
|
||
|
|
.await
|
||
|
|
.expect("create_alert_rule failed");
|
||
|
|
let rule_uid = created.uid.clone().expect("alert rule missing uid");
|
||
|
|
assert_eq!(created.title.as_deref(), Some(rule_title.as_str()));
|
||
|
|
|
||
|
|
// List
|
||
|
|
let rules = c.get_alert_rules().await.expect("get_alert_rules failed");
|
||
|
|
assert!(
|
||
|
|
rules.iter().any(|r| r.uid.as_deref() == Some(&rule_uid)),
|
||
|
|
"alert rule not found in list"
|
||
|
|
);
|
||
|
|
|
||
|
|
// Update
|
||
|
|
let updated_title = unique_name("rule-upd");
|
||
|
|
let update_body = serde_json::json!({
|
||
|
|
"title": updated_title,
|
||
|
|
"ruleGroup": rule_group,
|
||
|
|
"folderUID": folder_uid,
|
||
|
|
"condition": "A",
|
||
|
|
"for": "10m",
|
||
|
|
"data": [{
|
||
|
|
"refId": "A",
|
||
|
|
"datasourceUid": ds_uid,
|
||
|
|
"model": {
|
||
|
|
"expr": "up == 0",
|
||
|
|
"refId": "A"
|
||
|
|
},
|
||
|
|
"relativeTimeRange": {
|
||
|
|
"from": 600,
|
||
|
|
"to": 0
|
||
|
|
}
|
||
|
|
}],
|
||
|
|
"noDataState": "NoData",
|
||
|
|
"execErrState": "Error"
|
||
|
|
});
|
||
|
|
let updated = c
|
||
|
|
.update_alert_rule(&rule_uid, &update_body)
|
||
|
|
.await
|
||
|
|
.expect("update_alert_rule failed");
|
||
|
|
assert_eq!(updated.title.as_deref(), Some(updated_title.as_str()));
|
||
|
|
|
||
|
|
// Delete alert rule
|
||
|
|
c.delete_alert_rule(&rule_uid)
|
||
|
|
.await
|
||
|
|
.expect("delete_alert_rule failed");
|
||
|
|
|
||
|
|
// Cleanup: delete datasource and folder
|
||
|
|
c.delete_datasource(ds_id)
|
||
|
|
.await
|
||
|
|
.expect("cleanup datasource failed");
|
||
|
|
c.delete_folder(&folder_uid)
|
||
|
|
.await
|
||
|
|
.expect("cleanup folder failed");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 7. Proxy
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn grafana_proxy_datasource() {
|
||
|
|
wait_for_healthy(GRAFANA_HEALTH, TIMEOUT).await;
|
||
|
|
let c = grafana();
|
||
|
|
|
||
|
|
// Create a datasource to proxy through
|
||
|
|
let ds_name = unique_name("proxy-ds");
|
||
|
|
let ds_uid = unique_name("proxydsuid");
|
||
|
|
let ds = c
|
||
|
|
.create_datasource(&serde_json::json!({
|
||
|
|
"name": ds_name,
|
||
|
|
"type": "prometheus",
|
||
|
|
"uid": ds_uid,
|
||
|
|
"url": "http://prometheus:9090",
|
||
|
|
"access": "proxy"
|
||
|
|
}))
|
||
|
|
.await
|
||
|
|
.expect("create datasource for proxy failed");
|
||
|
|
let ds_id = ds.id.expect("datasource missing id");
|
||
|
|
|
||
|
|
// Proxy a Prometheus query through Grafana
|
||
|
|
let result = c
|
||
|
|
.proxy_datasource(ds_id, "api/v1/query?query=up")
|
||
|
|
.await
|
||
|
|
.expect("proxy_datasource failed — ensure Grafana can reach Prometheus at http://prometheus:9090");
|
||
|
|
assert_eq!(result["status"], "success");
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
c.delete_datasource(ds_id)
|
||
|
|
.await
|
||
|
|
.expect("cleanup proxy datasource failed");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// LOKI — additional coverage
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki instant query (exercises the query() method)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_instant_query() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
// Instant query with a metric-style expression (count_over_time)
|
||
|
|
let query_str = r#"count_over_time({job="test"}[5m])"#;
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.query(query_str, Some(10), None).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("instant query failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki instant query with explicit time
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_instant_query_with_time() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now().timestamp().to_string();
|
||
|
|
let query_str = r#"count_over_time({job="test"}[5m])"#;
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.query(query_str, None, Some(&now)).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("instant query with time failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki query_range with step parameter
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_query_range_with_step() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
// Use a metric expression so step makes sense
|
||
|
|
let query_str = r#"count_over_time({job="test"}[1m])"#;
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.query_range(query_str, &start, &end, Some(100), Some("60s")).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("query_range with step failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki labels with start/end parameters
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_labels_with_time_range() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.labels(Some(&start), Some(&end)).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("labels with time range failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki label_values with start/end parameters
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_label_values_with_time_range() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.label_values("job", Some(&start), Some(&end)).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("label_values with time range failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki push error path (malformed body)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_push_malformed() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
// Send a completely malformed push body
|
||
|
|
let bad_body = serde_json::json!({
|
||
|
|
"streams": [{
|
||
|
|
"stream": {},
|
||
|
|
"values": "not-an-array"
|
||
|
|
}]
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = c.push(&bad_body).await;
|
||
|
|
assert!(result.is_err(), "push with malformed body should fail");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki series without time bounds
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_series_no_time_bounds() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.series(&[r#"{job="test"}"#], None, None).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("series without time bounds failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki series with multiple matchers
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_series_multiple_matchers() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c
|
||
|
|
.series(
|
||
|
|
&[r#"{job="test"}"#, r#"{app="integration"}"#],
|
||
|
|
Some(&start),
|
||
|
|
Some(&end),
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
{
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("series with multiple matchers failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki index_volume_range with step
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_index_volume_range_with_step() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c
|
||
|
|
.index_volume_range(r#"{job="test"}"#, &start, &end, Some("60s"))
|
||
|
|
.await
|
||
|
|
{
|
||
|
|
Ok(_val) => return,
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => {
|
||
|
|
eprintln!("index_volume_range with step not available (may be expected): {e}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki labels with only start (no end)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_labels_start_only() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let start = (now - chrono::Duration::minutes(10)).timestamp().to_string();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.labels(Some(&start), None).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("labels with start only failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki label_values with only end (no start) — exercises sep logic
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_label_values_end_only() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let now = chrono::Utc::now();
|
||
|
|
let end = now.timestamp().to_string();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.label_values("job", None, Some(&end)).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("label_values with end only failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki detect_patterns with no time bounds
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_detect_patterns_no_time() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.detect_patterns(r#"{job="test"}"#, None, None).await {
|
||
|
|
Ok(_val) => return,
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => {
|
||
|
|
eprintln!("detect_patterns without time not available (may be expected): {e}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki index_volume with no time bounds
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_index_volume_no_time() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.index_volume(r#"{job="test"}"#, None, None).await {
|
||
|
|
Ok(_val) => return,
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => {
|
||
|
|
eprintln!("index_volume without time not available (may be expected): {e}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Loki query with default limit (None)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn loki_instant_query_default_limit() {
|
||
|
|
wait_for_healthy(LOKI_HEALTH, TIMEOUT).await;
|
||
|
|
let c = loki();
|
||
|
|
|
||
|
|
let query_str = r#"count_over_time({job="test"}[5m])"#;
|
||
|
|
for i in 0..10 {
|
||
|
|
match c.query(query_str, None, None).await {
|
||
|
|
Ok(res) => {
|
||
|
|
assert_eq!(res.status, "success");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Err(_) if i < 9 => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
|
||
|
|
Err(e) => panic!("instant query with default limit failed after retries: {e}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|