#![cfg(feature = "observability")]
use faucet_core::{InstallError, ObservabilityConfig, PrometheusConfig, install_observability};
use metrics_util::debugging::{DebugValue, DebuggingRecorder, Snapshotter};
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
static LOCK: Mutex<()> = Mutex::new(());
static SNAPSHOTTER: OnceLock<Snapshotter> = OnceLock::new();
fn snapshotter() -> &'static Snapshotter {
SNAPSHOTTER.get_or_init(|| {
let recorder = DebuggingRecorder::new();
let snap = recorder.snapshotter();
let _ = metrics::set_global_recorder(recorder);
snap
})
}
#[cfg(feature = "source-rest")]
#[cfg(feature = "sink-jsonl")]
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn three_row_matrix_produces_distinct_series() {
let _g = LOCK.lock().unwrap();
let snap = snapshotter();
let tmp = tempfile::tempdir().expect("tempdir");
let path_a = tmp.path().join("a.jsonl");
let path_b = tmp.path().join("b.jsonl");
let path_c = tmp.path().join("c.jsonl");
let server = wiremock::MockServer::start().await;
let body_a = serde_json::json!([{"id": 1}, {"id": 2}]);
let body_b = serde_json::json!([{"id": 10}]);
let body_c = serde_json::json!([{"id": 100}, {"id": 200}, {"id": 300}]);
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/a"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(&body_a))
.mount(&server)
.await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/b"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(&body_b))
.mount(&server)
.await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/c"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(&body_c))
.mount(&server)
.await;
let yaml = format!(
r#"
version: 1
name: e2e-obs-pipeline
pipeline:
source:
type: rest
config:
base_url: "{base}"
path: "/a"
method: GET
auth:
type: none
query_params: {{}}
pagination:
type: None
max_retries: 0
retry_backoff: 0
tolerated_http_errors: []
replication_method:
type: FullTable
primary_keys: []
partitions: []
schema_sample_size: 0
sink:
type: jsonl
config:
path: "{a}"
matrix:
- id: api-a
source:
config:
path: "/a"
sink:
config:
path: "{a}"
- id: api-b
source:
config:
path: "/b"
sink:
config:
path: "{b}"
- id: api-c
source:
config:
path: "/c"
sink:
config:
path: "{c}"
"#,
base = server.uri(),
a = path_a.display(),
b = path_b.display(),
c = path_c.display(),
);
faucet_cli::run_from_yaml_str(&yaml)
.await
.expect("three-row pipeline should succeed");
let snapshot = snap.snapshot();
let mut rows_seen: HashSet<String> = HashSet::new();
for (key, _u, _d, v) in snapshot.into_vec() {
if key.key().name() == "faucet_source_records_total" {
for label in key.key().labels() {
if label.key() == "row" {
rows_seen.insert(label.value().to_string());
}
}
assert!(
matches!(v, DebugValue::Counter(_)),
"faucet_source_records_total must be a counter"
);
}
}
assert!(
rows_seen.contains("api-a"),
"missing row=api-a in faucet_source_records_total; found: {rows_seen:?}"
);
assert!(
rows_seen.contains("api-b"),
"missing row=api-b in faucet_source_records_total; found: {rows_seen:?}"
);
assert!(
rows_seen.contains("api-c"),
"missing row=api-c in faucet_source_records_total; found: {rows_seen:?}"
);
let snapshot2 = snap.snapshot();
let mut connectors_seen = std::collections::HashSet::new();
let mut pipelines_seen = std::collections::HashSet::new();
for (key, _u, _d, _v) in snapshot2.into_vec() {
if key.key().name() == "faucet_source_records_total" {
for label in key.key().labels() {
if label.key() == "connector" {
connectors_seen.insert(label.value().to_string());
}
if label.key() == "pipeline" {
pipelines_seen.insert(label.value().to_string());
}
}
}
}
assert!(
connectors_seen.contains("rest"),
"expected connector=\"rest\" label on faucet_source_records_total, saw: {connectors_seen:?}"
);
assert!(
pipelines_seen.len() == 1,
"expected exactly one pipeline value, saw: {pipelines_seen:?}"
);
assert!(
pipelines_seen.contains("e2e-obs-pipeline"),
"expected pipeline=\"e2e-obs-pipeline\", saw: {pipelines_seen:?}"
);
}
#[test]
fn install_observability_idempotent_empty_config() {
let _g = LOCK.lock().unwrap();
let cfg = ObservabilityConfig::default();
install_observability(&cfg).expect("first call");
install_observability(&cfg).expect("second call");
}
#[test]
fn install_observability_returns_typed_error_on_garbage_listen() {
let _g = LOCK.lock().unwrap();
let cfg = ObservabilityConfig {
prometheus: Some(PrometheusConfig {
listen: "totally bogus".into(),
buckets: None,
}),
tracing: None,
otel: None,
};
match install_observability(&cfg) {
Err(InstallError::PrometheusBind { .. }) => {}
other => panic!("expected InstallError::PrometheusBind, got {other:?}"),
}
}