Resolves the 142 findings from tidal/docs/reviews/CODE_REVIEW_m0-m10.md across the engine, server, net, and CLI surfaces: - WAL/session-journal durability, checkpoint format, and crash-recovery hardening - Replication shipper/receiver, tenant isolation, and migration paths - Cluster scatter-gather, router, standalone server + health/offload endpoints - tidalctl refactored into command modules with JSON output and WAL-state tooling - Cohort, governance, signal-ledger, and vector-registry correctness fixes - Expanded UAT/integration/durability test coverage across all milestones
170 lines
4.6 KiB
Rust
170 lines
4.6 KiB
Rust
//! Integration tests for the metrics HTTP server.
|
|
//!
|
|
//! These tests require `--features metrics`.
|
|
|
|
#![allow(clippy::too_many_lines, clippy::unwrap_used)]
|
|
|
|
use std::{
|
|
io::{Read, Write},
|
|
net::TcpStream,
|
|
time::Duration,
|
|
};
|
|
|
|
use tidaldb::TidalDb;
|
|
|
|
/// Make an HTTP GET request to the given address and path.
|
|
/// Returns (`status_code`, body).
|
|
fn http_get(addr: std::net::SocketAddr, path: &str) -> (u16, String) {
|
|
let mut stream = TcpStream::connect(addr).expect("connect");
|
|
write!(
|
|
stream,
|
|
"GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.unwrap();
|
|
stream.flush().unwrap();
|
|
let mut response = String::new();
|
|
stream.read_to_string(&mut response).unwrap();
|
|
// Parse status line: "HTTP/1.1 200 OK"
|
|
let status: u16 = response
|
|
.lines()
|
|
.next()
|
|
.and_then(|l| l.split_whitespace().nth(1))
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(0);
|
|
let body = response
|
|
.split_once("\r\n\r\n")
|
|
.map_or("", |x| x.1)
|
|
.to_string();
|
|
(status, body)
|
|
}
|
|
|
|
fn open_with_metrics() -> (std::net::SocketAddr, TidalDb) {
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.enable_metrics("127.0.0.1:0")
|
|
.open()
|
|
.expect("open should succeed");
|
|
let addr = db
|
|
.metrics_addr()
|
|
.expect("metrics_addr should be Some when metrics enabled");
|
|
(addr, db)
|
|
}
|
|
|
|
#[test]
|
|
fn metrics_server_starts_on_port_zero() {
|
|
let (addr, db) = open_with_metrics();
|
|
assert_ne!(addr.port(), 0, "OS should have assigned a real port");
|
|
db.close().expect("close should succeed");
|
|
}
|
|
|
|
#[test]
|
|
fn healthz_returns_200_json() {
|
|
let (addr, _db) = open_with_metrics();
|
|
|
|
let (status, body) = http_get(addr, "/healthz");
|
|
assert_eq!(status, 200);
|
|
assert!(body.contains("\"status\":\"ok\""), "body: {body}");
|
|
assert!(
|
|
body.contains("\"uptime_seconds\":"),
|
|
"body should have uptime: {body}"
|
|
);
|
|
assert!(
|
|
body.contains("\"version\":"),
|
|
"body should have version: {body}"
|
|
);
|
|
assert!(
|
|
body.contains("\"build_hash\":"),
|
|
"body should have build_hash: {body}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn metrics_returns_200_prometheus() {
|
|
let (addr, _db) = open_with_metrics();
|
|
|
|
let (status, body) = http_get(addr, "/metrics");
|
|
assert_eq!(status, 200);
|
|
assert!(
|
|
body.contains("tidaldb_uptime_seconds"),
|
|
"missing uptime metric in body: {body}"
|
|
);
|
|
assert!(
|
|
body.contains("tidaldb_health_ok"),
|
|
"missing health_ok metric in body: {body}"
|
|
);
|
|
assert!(
|
|
body.contains("tidaldb_info"),
|
|
"missing info metric in body: {body}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn uptime_increases_over_time() {
|
|
let (addr, _db) = open_with_metrics();
|
|
|
|
let (_, body1) = http_get(addr, "/metrics");
|
|
std::thread::sleep(Duration::from_millis(150));
|
|
let (_, body2) = http_get(addr, "/metrics");
|
|
|
|
let uptime1 = extract_uptime(&body1);
|
|
let uptime2 = extract_uptime(&body2);
|
|
|
|
assert!(
|
|
uptime2 > uptime1,
|
|
"uptime should increase: {uptime1} -> {uptime2}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn health_ok_value_is_one() {
|
|
let (addr, _db) = open_with_metrics();
|
|
|
|
let (_, body) = http_get(addr, "/metrics");
|
|
let health_line = body
|
|
.lines()
|
|
.find(|l| l.starts_with("tidaldb_health_ok{"))
|
|
.expect("should have health_ok metric");
|
|
assert!(
|
|
health_line.ends_with(" 1"),
|
|
"health should be 1: {health_line}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn not_found_returns_404() {
|
|
let (addr, _db) = open_with_metrics();
|
|
let (status, _) = http_get(addr, "/nonexistent");
|
|
assert_eq!(status, 404);
|
|
}
|
|
|
|
#[test]
|
|
fn close_stops_server() {
|
|
let (addr, db) = open_with_metrics();
|
|
// Server should respond before close
|
|
let (status, _) = http_get(addr, "/healthz");
|
|
assert_eq!(status, 200);
|
|
|
|
db.close().expect("close should succeed");
|
|
|
|
// Give the thread a moment to shut down
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
|
|
// After close, connection should be refused
|
|
let result = TcpStream::connect_timeout(&addr, Duration::from_millis(200));
|
|
assert!(result.is_err(), "connection should be refused after close");
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Helpers
|
|
// -----------------------------------------------------------------------
|
|
|
|
fn extract_uptime(prometheus_body: &str) -> f64 {
|
|
for line in prometheus_body.lines() {
|
|
if line.starts_with("tidaldb_uptime_seconds{") {
|
|
let val = line.split_whitespace().last().unwrap_or("0");
|
|
return val.parse().unwrap_or(0.0);
|
|
}
|
|
}
|
|
0.0
|
|
}
|