Files
cloudlysis/control/api/tests/observability_s3_docker_gated.rs
Vlad Durnea 2595e7f1c5
Some checks failed
ci / ui (push) Failing after 28s
ci / rust (push) Failing after 2m40s
images / build-and-push (push) Failing after 19s
feat(billing): implement tenant subscription entitlements system (milestones 0-6)
2026-03-30 18:41:23 +03:00

219 lines
6.7 KiB
Rust

use reqwest::StatusCode;
use serde_json::json;
use std::{
net::TcpStream,
path::PathBuf,
process::Command,
time::{Duration, Instant},
};
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("api crate should live under repo root")
.to_path_buf()
}
fn docker_enabled() -> bool {
std::env::var("CONTROL_TEST_DOCKER")
.ok()
.is_some_and(|v| v.trim() == "1")
}
fn wait_for_tcp(addr: &str, timeout: Duration) -> bool {
let start = Instant::now();
while start.elapsed() < timeout {
if TcpStream::connect_timeout(
&addr.parse().expect("invalid socket addr"),
Duration::from_secs(1),
)
.is_ok()
{
return true;
}
std::thread::sleep(Duration::from_millis(250));
}
false
}
fn mc_ls_bucket(compose: &PathBuf, bucket: &str) -> std::process::Output {
// Run inside compose network so it can reach `minio:9000`.
Command::new("docker")
.args(["compose", "-f"])
.arg(compose)
.args([
"run",
"--rm",
"minio-init",
"/bin/sh",
"-lc",
&format!(
"mc alias set local http://minio:9000 minioadmin minioadmin >/dev/null && mc ls --recursive local/{bucket}"
),
])
.output()
.expect("failed to run mc ls")
}
#[tokio::test]
async fn loki_and_tempo_write_objects_to_minio_in_s3_mode() {
if !docker_enabled() {
eprintln!("skipping: set CONTROL_TEST_DOCKER=1 to enable docker tests");
return;
}
let root = repo_root();
let base = root.join("docker-compose.yml");
let obs = root.join("observability/docker-compose.yml");
let obs_s3 = root.join("observability/docker-compose.s3.yml");
let up = Command::new("docker")
.args(["compose", "-f"])
.arg(&base)
.args(["-f"])
.arg(&obs)
.args(["-f"])
.arg(&obs_s3)
.args(["up", "-d"])
.status()
.expect("failed to run docker compose up");
assert!(up.success(), "docker compose up failed");
let reachable = wait_for_tcp("127.0.0.1:3100", Duration::from_secs(45))
&& wait_for_tcp("127.0.0.1:3200", Duration::from_secs(45))
&& wait_for_tcp("127.0.0.1:9411", Duration::from_secs(45))
&& wait_for_tcp("127.0.0.1:9000", Duration::from_secs(45));
assert!(reachable, "loki/tempo/minio ports not reachable in time");
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap();
// Push one log line into Loki.
let ts_ns = (std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos())
.to_string();
let push = http
.post("http://127.0.0.1:3100/loki/api/v1/push")
.json(&json!({
"streams": [{
"stream": { "app": "cloudlysis-test" },
"values": [[ts_ns, "hello from test"]]
}]
}))
.send()
.await
.expect("loki push request failed");
assert!(
push.status() == StatusCode::NO_CONTENT,
"unexpected loki push status: {}",
push.status()
);
// Emit one trace span via Zipkin v2.
let zipkin = http
.post("http://127.0.0.1:9411/api/v2/spans")
.json(&json!([{
"traceId": "463ac35c9f6413ad48485a3953bb6124",
"id": "a2fb4a1d1a96d312",
"name": "test-span",
"timestamp": 1700000000000000u64,
"duration": 1000u64,
"localEndpoint": { "serviceName": "cloudlysis-test" }
}]))
.send()
.await
.expect("zipkin post failed");
assert!(
zipkin.status().is_success(),
"zipkin ingest failed: {}",
zipkin.status()
);
// Query Loki back to ensure the line is retrievable (not just accepted).
// Loki may need a short delay to index.
let loki_deadline = Instant::now() + Duration::from_secs(30);
let mut loki_ok = false;
while Instant::now() < loki_deadline && !loki_ok {
let q = http
.get("http://127.0.0.1:3100/loki/api/v1/query")
.query(&[("query", r#"{app="cloudlysis-test"}"#)])
.send()
.await
.expect("loki query failed");
if q.status().is_success() {
let v: serde_json::Value = q.json().await.expect("invalid loki query json");
// We only need to see any non-empty result.
let has = v
.get("data")
.and_then(|d| d.get("result"))
.and_then(|r| r.as_array())
.is_some_and(|a| !a.is_empty());
if has {
loki_ok = true;
break;
}
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
// Query Tempo back by trace id (Zipkin traceId used above).
let tempo_deadline = Instant::now() + Duration::from_secs(30);
let mut tempo_ok = false;
while Instant::now() < tempo_deadline && !tempo_ok {
let res = http
.get("http://127.0.0.1:3200/api/traces/463ac35c9f6413ad48485a3953bb6124")
.send()
.await
.expect("tempo get trace failed");
if res.status().is_success() {
tempo_ok = true;
break;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
// Poll buckets until at least one object appears.
let deadline = Instant::now() + Duration::from_secs(45);
let mut loki_has_objects = false;
let mut tempo_has_objects = false;
while Instant::now() < deadline && (!loki_has_objects || !tempo_has_objects) {
let loki_out = mc_ls_bucket(&base, "cloudlysis-loki");
if loki_out.status.success() && !loki_out.stdout.is_empty() {
loki_has_objects = true;
}
let tempo_out = mc_ls_bucket(&base, "cloudlysis-tempo");
if tempo_out.status.success() && !tempo_out.stdout.is_empty() {
tempo_has_objects = true;
}
if !loki_has_objects || !tempo_has_objects {
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
let _ = Command::new("docker")
.args(["compose", "-f"])
.arg(&base)
.args(["-f"])
.arg(&obs)
.args(["-f"])
.arg(&obs_s3)
.args(["down", "-v"])
.status();
assert!(loki_has_objects, "expected Loki to write objects to MinIO");
assert!(
tempo_has_objects,
"expected Tempo to write objects to MinIO"
);
assert!(loki_ok, "expected Loki query to return a result");
assert!(tempo_ok, "expected Tempo to return the ingested trace");
}