- Add BUILD.bazel across tidal, tidal-net, tidal-server, tidalctl for bzlmod build - Add tidal/ crate docs (README, CHANGELOG, CONTRIBUTING, AGENTS, CLAUDE, API, ARCHITECTURE) and ai-lookup reference - Add docker standalone/cluster/deploy images, compose, and prometheus config - Harden WAL (batch format, writer, dedup, diagnostics), text syncer/collectors, and vector registry - Expand tidalctl CLI and tests; restructure WAL/visibility integration test suites - Refine tidal-net transport/client/server and tidal-server cluster/scatter-gather
176 lines
5.5 KiB
Rust
176 lines
5.5 KiB
Rust
//! Integration tests for M7P4 Operational Visibility — signal/WAL + index health metrics.
|
|
//!
|
|
//! - Task 02: Signal + WAL metrics
|
|
//! - Task 03: Index health metrics
|
|
//!
|
|
//! Every test in this file is gated behind the `metrics` feature.
|
|
#![allow(clippy::unwrap_used)]
|
|
|
|
#[cfg(feature = "metrics")]
|
|
use std::time::Duration;
|
|
|
|
#[cfg(feature = "metrics")]
|
|
use tidaldb::{
|
|
TidalDb,
|
|
schema::{DecaySpec, EntityId, EntityKind, Timestamp, Window},
|
|
};
|
|
|
|
// ── Prometheus output helpers ────────────────────────────────────────────────
|
|
|
|
/// Extract the numeric value for a metric from Prometheus text output.
|
|
///
|
|
/// Looks for a line that starts with `name` (not a comment), then parses
|
|
/// the trailing float. Returns `None` if the metric is absent.
|
|
#[cfg(feature = "metrics")]
|
|
fn prometheus_value(prom: &str, name: &str) -> Option<f64> {
|
|
prom.lines()
|
|
.filter(|l| !l.starts_with('#'))
|
|
.find(|l| l.starts_with(name) && l[name.len()..].starts_with(' '))
|
|
.and_then(|l| l.split_whitespace().last())
|
|
.and_then(|v| v.parse().ok())
|
|
}
|
|
|
|
// ── Shared test schema ───────────────────────────────────────────────────────
|
|
|
|
#[cfg(feature = "metrics")]
|
|
fn build_test_schema() -> tidaldb::schema::Schema {
|
|
use tidaldb::{AgentPolicy, schema::SchemaBuilder};
|
|
|
|
let mut builder = SchemaBuilder::new();
|
|
let _ = builder
|
|
.signal(
|
|
"view",
|
|
EntityKind::Item,
|
|
DecaySpec::Exponential {
|
|
half_life: Duration::from_secs(7 * 24 * 3600),
|
|
},
|
|
)
|
|
.windows(&[Window::OneHour])
|
|
.add();
|
|
let _ = builder
|
|
.signal(
|
|
"like",
|
|
EntityKind::Item,
|
|
DecaySpec::Exponential {
|
|
half_life: Duration::from_secs(30 * 24 * 3600),
|
|
},
|
|
)
|
|
.windows(&[Window::OneHour])
|
|
.add();
|
|
builder.session_policy(
|
|
"default",
|
|
AgentPolicy {
|
|
allowed_signals: vec!["view".to_string(), "like".to_string()],
|
|
denied_signals: vec![],
|
|
max_session_duration: Duration::from_secs(3600),
|
|
max_signals_per_session: 1000,
|
|
},
|
|
);
|
|
builder.build().unwrap()
|
|
}
|
|
|
|
// ── Task 02: Signal + WAL Metrics ───────────────────────────────────────────
|
|
|
|
#[cfg(feature = "metrics")]
|
|
#[test]
|
|
fn signal_write_increments_counter() {
|
|
let schema = build_test_schema();
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema)
|
|
.open()
|
|
.unwrap();
|
|
|
|
let before = prometheus_value(
|
|
&db.metrics().render_prometheus(),
|
|
"tidaldb_signal_writes_total",
|
|
)
|
|
.unwrap_or(0.0);
|
|
db.signal("view", EntityId::new(1), 1.0, Timestamp::now())
|
|
.unwrap();
|
|
db.signal("like", EntityId::new(2), 1.0, Timestamp::now())
|
|
.unwrap();
|
|
db.signal("view", EntityId::new(3), 0.5, Timestamp::now())
|
|
.unwrap();
|
|
let after = prometheus_value(
|
|
&db.metrics().render_prometheus(),
|
|
"tidaldb_signal_writes_total",
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(
|
|
((after - before) - 3.0).abs() < 0.5,
|
|
"signal_writes_total must increment once per signal write"
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "metrics")]
|
|
#[test]
|
|
fn signal_write_latency_appears_in_prometheus() {
|
|
let schema = build_test_schema();
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema)
|
|
.open()
|
|
.unwrap();
|
|
|
|
for i in 1u64..=3 {
|
|
db.signal("view", EntityId::new(i), 1.0, Timestamp::now())
|
|
.unwrap();
|
|
}
|
|
|
|
let prom = db.metrics().render_prometheus();
|
|
assert!(
|
|
prom.contains("tidaldb_signal_write_latency_us"),
|
|
"Prometheus output must include signal write latency histogram"
|
|
);
|
|
assert!(
|
|
prom.contains("tidaldb_signal_writes_total"),
|
|
"Prometheus output must include signal_writes_total counter"
|
|
);
|
|
assert!(
|
|
prom.contains("tidaldb_wal_lag_bytes"),
|
|
"Prometheus output must include wal_lag_bytes gauge"
|
|
);
|
|
assert!(
|
|
prom.contains("tidaldb_checkpoint_age_seconds"),
|
|
"Prometheus output must include checkpoint_age_seconds gauge"
|
|
);
|
|
}
|
|
|
|
// ── Task 03: Index Health Metrics ────────────────────────────────────────────
|
|
|
|
#[cfg(feature = "metrics")]
|
|
#[test]
|
|
fn prometheus_output_contains_all_index_gauge_names() {
|
|
let schema = build_test_schema();
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema)
|
|
.open()
|
|
.unwrap();
|
|
|
|
let prom = db.metrics().render_prometheus();
|
|
|
|
assert!(
|
|
prom.contains("tidaldb_tantivy_segment_count"),
|
|
"Prometheus output must include tantivy_segment_count gauge"
|
|
);
|
|
assert!(
|
|
prom.contains("tidaldb_tantivy_indexed_docs"),
|
|
"Prometheus output must include tantivy_indexed_docs gauge"
|
|
);
|
|
assert!(
|
|
prom.contains("tidaldb_usearch_vector_count"),
|
|
"Prometheus output must include usearch_vector_count gauge"
|
|
);
|
|
assert!(
|
|
prom.contains("tidaldb_usearch_index_size_bytes"),
|
|
"Prometheus output must include usearch_index_size_bytes gauge"
|
|
);
|
|
assert!(
|
|
prom.contains("tidaldb_bitmap_index_cardinality"),
|
|
"Prometheus output must include bitmap_index_cardinality gauge"
|
|
);
|
|
}
|