fix(cli): add --version and --help flags to miroir-proxy
Adds clap-based CLI argument parsing so `miroir-proxy --version` and `miroir-proxy --help` print version/usage and exit instead of starting the server and hanging. Also fixes numerous pre-existing clippy warnings in test files: - digit grouping inconsistencies - unused functions/variables - useless_vec (vec! -> array) - assert!(true) placeholders - too_many_arguments Resolves: bf-31ff
This commit is contained in:
parent
d10a9ac1fd
commit
4777bb6834
21 changed files with 117 additions and 78 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2515,6 +2515,7 @@ dependencies = [
|
|||
"bytes 1.11.1",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"clap",
|
||||
"config",
|
||||
"dashmap",
|
||||
"futures 0.3.32",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ path = "src/main.rs"
|
|||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
axum = { version = "0.7", features = ["macros"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
http = "1.1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "signal"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use axum::{
|
|||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use clap::Parser;
|
||||
use miroir_core::{
|
||||
config::MiroirConfig,
|
||||
peer_discovery::PeerDiscovery,
|
||||
|
|
@ -16,6 +17,17 @@ use tokio::signal;
|
|||
use tracing::{error, info, warn};
|
||||
use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
/// Miroir proxy - distributed search orchestrator for Meilisearch CE
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "miroir-proxy")]
|
||||
#[command(author, version, about)]
|
||||
#[command(long_version = option_env!("GIT_VERSION").unwrap_or_else(|| env!("CARGO_PKG_VERSION")))]
|
||||
struct CliArgs {
|
||||
/// Path to configuration file (YAML or TOML)
|
||||
#[arg(short, long)]
|
||||
config: Option<String>,
|
||||
}
|
||||
|
||||
mod admin_session;
|
||||
mod admin_ui;
|
||||
mod auth;
|
||||
|
|
@ -31,14 +43,12 @@ use admin_session::SealKey;
|
|||
use auth::AuthState;
|
||||
use middleware::{metrics_router, Metrics, TelemetryState};
|
||||
use miroir_core::{
|
||||
canary::{
|
||||
CanaryRunner, QueryCapture, SearchQuery, SearchResponse,
|
||||
},
|
||||
canary::{CanaryRunner, QueryCapture, SearchQuery, SearchResponse},
|
||||
task_store::TaskStore,
|
||||
};
|
||||
use routes::{
|
||||
admin, admin_endpoints, health, indexes, keys, multi_search, search, search_ui,
|
||||
settings, tasks, version,
|
||||
admin, admin_endpoints, health, indexes, keys, multi_search, search, search_ui, settings,
|
||||
tasks, version,
|
||||
};
|
||||
use scoped_key_rotation::ScopedKeyRotationState;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -281,9 +291,35 @@ impl FromRef<UnifiedState> for routes::canary::CanaryState {
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Handle --version and --help before any other setup
|
||||
// These must work without requiring config files or nodes
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() > 1 {
|
||||
match args[1].as_str() {
|
||||
"--version" | "-V" => {
|
||||
println!("miroir-proxy {}", env!("CARGO_PKG_VERSION"));
|
||||
std::process::exit(0);
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!("Miroir proxy - distributed search orchestrator for Meilisearch CE");
|
||||
println!();
|
||||
println!("Usage: miroir-proxy [OPTIONS]");
|
||||
println!();
|
||||
println!("Options:");
|
||||
println!(" -h, --help Print help information");
|
||||
println!(" -V, --version Print version information");
|
||||
println!(" -c, --config <FILE> Path to configuration file (YAML or TOML)");
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI arguments - config file path can be specified here
|
||||
let _cli = CliArgs::parse();
|
||||
|
||||
// Load configuration (file → env → CLI overlay)
|
||||
let config =
|
||||
MiroirConfig::load().map_err(|e| anyhow::anyhow!("Failed to load config: {e}"))?;
|
||||
let config = MiroirConfig::load().map_err(|e| anyhow::anyhow!("Failed to load config: {e}"))?;
|
||||
|
||||
// Initialize structured JSON logging (plan §10 format)
|
||||
// Fields on every line: timestamp, level, target, message, pod_id
|
||||
|
|
|
|||
|
|
@ -228,10 +228,7 @@ async fn test_document_round_trip() {
|
|||
assert_eq!(status, 200, "Failed to fetch document {i}: {body}");
|
||||
|
||||
let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
|
||||
assert_eq!(
|
||||
doc.get("id").and_then(|v| v.as_i64()),
|
||||
Some(i64::from(i))
|
||||
);
|
||||
assert_eq!(doc.get("id").and_then(|v| v.as_i64()), Some(i64::from(i)));
|
||||
assert_eq!(
|
||||
doc.get("title").and_then(|v| v.as_str()),
|
||||
Some(format!("Document {i}").as_str())
|
||||
|
|
@ -325,10 +322,7 @@ async fn test_search_shard_coverage() {
|
|||
.post("/indexes/shard_coverage_test/search", &search_body)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
status, 200,
|
||||
"Search failed for keyword {keyword}: {body}"
|
||||
);
|
||||
assert_eq!(status, 200, "Search failed for keyword {keyword}: {body}");
|
||||
|
||||
let response: serde_json::Value = serde_json::from_str(&body).unwrap();
|
||||
let hits = response.get("hits").and_then(|v| v.as_array()).unwrap();
|
||||
|
|
|
|||
|
|
@ -599,7 +599,7 @@ fn idempotency_key_follows_cross_vendor_convention() {
|
|||
#[test]
|
||||
fn validate_header_directions() {
|
||||
// Request headers
|
||||
let request_headers = vec![
|
||||
let request_headers = [
|
||||
"X-Miroir-Min-Settings-Version",
|
||||
"X-Miroir-Session", // Both directions
|
||||
"Idempotency-Key",
|
||||
|
|
@ -611,7 +611,7 @@ fn validate_header_directions() {
|
|||
];
|
||||
|
||||
// Response headers
|
||||
let response_headers = vec![
|
||||
let response_headers = [
|
||||
"X-Miroir-Degraded",
|
||||
"X-Miroir-Settings-Version",
|
||||
"X-Miroir-Settings-Inconsistent",
|
||||
|
|
@ -638,7 +638,8 @@ fn validate_header_directions() {
|
|||
#[test]
|
||||
fn header_contract_complete() {
|
||||
// Verify all headers from plan §5 are covered by tests
|
||||
let all_expected_headers = ["X-Miroir-Degraded",
|
||||
let all_expected_headers = [
|
||||
"X-Miroir-Degraded",
|
||||
"X-Miroir-Settings-Version",
|
||||
"X-Miroir-Min-Settings-Version",
|
||||
"X-Miroir-Settings-Inconsistent",
|
||||
|
|
@ -648,7 +649,8 @@ fn header_contract_complete() {
|
|||
"X-Miroir-Tenant",
|
||||
"X-Admin-Key",
|
||||
"X-CSRF-Token",
|
||||
"X-Search-UI-Key"];
|
||||
"X-Search-UI-Key",
|
||||
];
|
||||
|
||||
// This test serves as documentation that all headers are accounted for
|
||||
assert_eq!(
|
||||
|
|
@ -658,16 +660,20 @@ fn header_contract_complete() {
|
|||
);
|
||||
|
||||
// Categorize by direction
|
||||
let response_only = ["X-Miroir-Degraded",
|
||||
let response_only = [
|
||||
"X-Miroir-Degraded",
|
||||
"X-Miroir-Settings-Version",
|
||||
"X-Miroir-Settings-Inconsistent"];
|
||||
let request_only = ["Idempotency-Key",
|
||||
"X-Miroir-Settings-Inconsistent",
|
||||
];
|
||||
let request_only = [
|
||||
"Idempotency-Key",
|
||||
"X-Miroir-Min-Settings-Version",
|
||||
"X-Miroir-Over-Fetch",
|
||||
"X-Miroir-Tenant",
|
||||
"X-Admin-Key",
|
||||
"X-CSRF-Token",
|
||||
"X-Search-UI-Key"];
|
||||
"X-Search-UI-Key",
|
||||
];
|
||||
let bidirectional = ["X-Miroir-Session"];
|
||||
|
||||
assert_eq!(response_only.len(), 3, "3 response-only headers");
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use tokio::time::sleep;
|
|||
|
||||
/// Test configuration helper.
|
||||
struct TestSetup {
|
||||
#[allow(dead_code)]
|
||||
meilisearch_urls: Vec<String>,
|
||||
proxy_url: String,
|
||||
master_key: String,
|
||||
|
|
|
|||
|
|
@ -128,10 +128,7 @@ async fn delete_key(
|
|||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(format!(
|
||||
"DELETE /keys/{key_uid} failed: HTTP {status} — {text}"
|
||||
)
|
||||
.into());
|
||||
return Err(format!("DELETE /keys/{key_uid} failed: HTTP {status} — {text}").into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@
|
|||
use miroir_core::config::{MiroirConfig, NodeConfig, SearchUiConfig};
|
||||
use miroir_core::task_store::{RedisTaskStore, SearchUiScopedKey, TaskStore};
|
||||
use miroir_proxy::routes::indexes::MeilisearchClient;
|
||||
use miroir_proxy::scoped_key_rotation::{
|
||||
self, ScopedKeyRotationState,
|
||||
};
|
||||
use miroir_proxy::scoped_key_rotation::{self, ScopedKeyRotationState};
|
||||
use serde_json::json;
|
||||
use testcontainers::runners::AsyncRunner;
|
||||
use testcontainers_modules::redis::Redis;
|
||||
|
|
@ -55,6 +53,7 @@ async fn redis_store() -> RedisTaskStore {
|
|||
}
|
||||
|
||||
/// Seed a scoped key into Redis (simulating a previous rotation).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn seed_scoped_key(
|
||||
redis: &RedisTaskStore,
|
||||
index: &str,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use miroir_core::task_store::NewAdminSession;
|
|||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn now_ms() -> i64 {
|
||||
fn _now_ms() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
|
|
@ -26,20 +26,20 @@ fn now_ms() -> i64 {
|
|||
}
|
||||
|
||||
/// Create an admin session for testing.
|
||||
fn make_admin_session(id: &str, csrf_token: &str) -> NewAdminSession {
|
||||
fn _make_admin_session(id: &str, csrf_token: &str) -> NewAdminSession {
|
||||
NewAdminSession {
|
||||
session_id: id.to_string(),
|
||||
csrf_token: csrf_token.to_string(),
|
||||
admin_key_hash: "test-admin-key-hash".to_string(),
|
||||
created_at: now_ms(),
|
||||
expires_at: now_ms() + 3_600_000, // 1 hour
|
||||
created_at: _now_ms(),
|
||||
expires_at: _now_ms() + 3_600_000, // 1 hour
|
||||
user_agent: Some("test-agent".to_string()),
|
||||
source_ip: Some("127.0.0.1".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract error code from a Miroir error response.
|
||||
fn extract_error_code(body: &str) -> Option<String> {
|
||||
fn _extract_error_code(body: &str) -> Option<String> {
|
||||
let value: serde_json::Value = serde_json::from_str(body).ok()?;
|
||||
value
|
||||
.get("code")
|
||||
|
|
|
|||
|
|
@ -79,10 +79,7 @@ async fn five_failed_attempts_triggers_10_minute_backoff() {
|
|||
.expect("record failure");
|
||||
// First 4 failures don't trigger backoff
|
||||
if i < 5 {
|
||||
assert_eq!(
|
||||
wait_seconds, None,
|
||||
"failure {i} should not trigger backoff"
|
||||
);
|
||||
assert_eq!(wait_seconds, None, "failure {i} should not trigger backoff");
|
||||
} else {
|
||||
// 5th failure triggers backoff: 10 minutes = 600 seconds
|
||||
assert_eq!(
|
||||
|
|
@ -456,10 +453,7 @@ async fn different_ips_have_independent_buckets() {
|
|||
let (allowed, _) = store
|
||||
.check_rate_limit_admin_login(ip2, limit, window_seconds)
|
||||
.expect("check rate limit");
|
||||
assert!(
|
||||
allowed,
|
||||
"IP2 should not be affected by IP1's rate limit"
|
||||
);
|
||||
assert!(allowed, "IP2 should not be affected by IP1's rate limit");
|
||||
}
|
||||
|
||||
/// Rate limit window expires after TTL.
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ fn create_test_config() -> MiroirConfig {
|
|||
/// Test 1: Preview endpoint returns fingerprint and version information.
|
||||
#[tokio::test]
|
||||
async fn test_preview_endpoint_returns_fingerprint_and_version() {
|
||||
let config = Arc::new(create_test_config());
|
||||
let _config = Arc::new(create_test_config());
|
||||
|
||||
// This is a unit test for the response structure.
|
||||
// In a full integration test, we would:
|
||||
|
|
|
|||
|
|
@ -413,7 +413,7 @@ impl MockTaskRegistry {
|
|||
}
|
||||
|
||||
/// Add a task with a specific status.
|
||||
async fn add_task(&self, mtask_id: String, status: TaskStatus) {
|
||||
async fn _add_task(&self, mtask_id: String, status: TaskStatus) {
|
||||
let mut tasks = self.tasks.write().await;
|
||||
tasks.insert(
|
||||
mtask_id.clone(),
|
||||
|
|
@ -436,7 +436,7 @@ impl MockTaskRegistry {
|
|||
}
|
||||
|
||||
/// Update a task's status.
|
||||
async fn update_task(&self, mtask_id: &str, status: TaskStatus) {
|
||||
async fn _update_task(&self, mtask_id: &str, status: TaskStatus) {
|
||||
let mut tasks = self.tasks.write().await;
|
||||
if let Some(task) = tasks.get_mut(mtask_id) {
|
||||
task.status = status;
|
||||
|
|
@ -810,5 +810,4 @@ async fn integration_session_pinning_metrics() {
|
|||
metrics.inc_session_wait_timeout("block");
|
||||
|
||||
// If we got here without panicking, the metrics methods work
|
||||
assert!(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use miroir_core::router::shard_for_key;
|
|||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn make_config(
|
||||
fn _make_config(
|
||||
shards: u32,
|
||||
rf: u32,
|
||||
replica_groups: u32,
|
||||
|
|
|
|||
|
|
@ -48,8 +48,11 @@ fn contains_high_cardinality_id(s: &str) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
/// Type alias for parsed Prometheus metric line
|
||||
type ParsedMetric = Option<(String, Vec<(String, String)>, f64)>;
|
||||
|
||||
/// Helper: parse a Prometheus metric line and extract labels
|
||||
fn parse_metric_line(line: &str) -> Option<(String, Vec<(String, String)>, f64)> {
|
||||
fn parse_metric_line(line: &str) -> ParsedMetric {
|
||||
// Format: metric_name{label1="value1",label2="value2"} value
|
||||
let brace_start = line.find('{')?;
|
||||
let brace_end = line.find('}')?;
|
||||
|
|
|
|||
|
|
@ -502,10 +502,7 @@ fn test_error_shape_byte_for_byte_parity() {
|
|||
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
|
||||
|
||||
// Must have all four fields
|
||||
assert!(
|
||||
parsed.get("message").is_some(),
|
||||
"{code:?}: missing message"
|
||||
);
|
||||
assert!(parsed.get("message").is_some(), "{code:?}: missing message");
|
||||
assert!(parsed.get("code").is_some(), "{code:?}: missing code");
|
||||
assert!(parsed.get("type").is_some(), "{code:?}: missing type");
|
||||
assert!(parsed.get("link").is_some(), "{code:?}: missing link");
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
|
||||
use miroir_core::task_store::{
|
||||
IdempotencyEntry, NewAdminSession, NewAlias, NewCanary, NewCanaryRun, NewCdcCursor, NewJob,
|
||||
NewRolloverPolicy, NewSearchUiConfig, NewTask, NewTenantMapping, SessionRow, SqliteTaskStore, TaskFilter,
|
||||
TaskStore,
|
||||
NewRolloverPolicy, NewSearchUiConfig, NewTask, NewTenantMapping, SessionRow, SqliteTaskStore,
|
||||
TaskFilter, TaskStore,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -875,7 +875,7 @@ fn test_prune_tasks_removes_old_terminal_tasks() {
|
|||
store
|
||||
.insert_task(&NewTask {
|
||||
miroir_id: "old-task".to_string(),
|
||||
created_at: now - 86400_000, // 1 day ago
|
||||
created_at: now - 86_400_000, // 1 day ago
|
||||
status: "succeeded".to_string(),
|
||||
node_tasks,
|
||||
error: None,
|
||||
|
|
@ -911,7 +911,7 @@ fn test_prune_tasks_removes_old_terminal_tasks() {
|
|||
store
|
||||
.insert_task(&NewTask {
|
||||
miroir_id: "active-task".to_string(),
|
||||
created_at: now - 86400_000,
|
||||
created_at: now - 86_400_000,
|
||||
status: "processing".to_string(),
|
||||
node_tasks,
|
||||
error: None,
|
||||
|
|
|
|||
|
|
@ -119,8 +119,10 @@ async fn test_fingerprint_shard_pagination() {
|
|||
},
|
||||
);
|
||||
|
||||
let mut config = AntiEntropyConfig::default();
|
||||
config.fingerprint_batch_size = batch_size;
|
||||
let config = AntiEntropyConfig {
|
||||
fingerprint_batch_size: batch_size,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let topology = Arc::new(RwLock::new(Topology::new(1, 1, 1)));
|
||||
let reconciler = AntiEntropyReconciler::new(config, topology, Arc::new(mock_client));
|
||||
|
|
@ -148,7 +150,7 @@ async fn test_fingerprint_shard_content_hash_excludes_internal_fields() {
|
|||
"_rankingScore": 0.95,
|
||||
});
|
||||
|
||||
let doc2 = json!({
|
||||
let _doc2 = json!({
|
||||
"id": "doc-1",
|
||||
"title": "Same Title",
|
||||
"content": "Same Content",
|
||||
|
|
@ -462,8 +464,10 @@ async fn test_fingerprint_config_batch_size() {
|
|||
},
|
||||
);
|
||||
|
||||
let mut config = AntiEntropyConfig::default();
|
||||
config.fingerprint_batch_size = batch_size;
|
||||
let config = AntiEntropyConfig {
|
||||
fingerprint_batch_size: batch_size,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let topology = Arc::new(RwLock::new(Topology::new(1, 1, 1)));
|
||||
let reconciler = AntiEntropyReconciler::new(config, topology, Arc::new(mock_client));
|
||||
|
|
@ -493,7 +497,7 @@ async fn test_compute_content_hash_unit() {
|
|||
|
||||
// Create a dummy reconciler just to call the static method
|
||||
let topology = Arc::new(RwLock::new(Topology::new(1, 1, 1)));
|
||||
let reconciler = AntiEntropyReconciler::<MockNodeClient>::new(
|
||||
let _reconciler = AntiEntropyReconciler::<MockNodeClient>::new(
|
||||
AntiEntropyConfig::default(),
|
||||
topology,
|
||||
Arc::new(MockNodeClient::default()),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use miroir_proxy::middleware::Metrics;
|
|||
/// Helper to parse a metric line from Prometheus text format.
|
||||
///
|
||||
/// Returns (metric_name, labels_map, value) or None if not a valid metric line.
|
||||
fn parse_metric_line(
|
||||
fn _parse_metric_line(
|
||||
line: &str,
|
||||
) -> Option<(String, std::collections::HashMap<String, String>, f64)> {
|
||||
let line = line.trim();
|
||||
|
|
|
|||
|
|
@ -111,9 +111,11 @@ fn test_request_id_format_in_logs() {
|
|||
|
||||
#[test]
|
||||
fn test_request_id_extraction_from_logs() {
|
||||
let logs = [r#"{"timestamp":"2026-05-01T12:00:00.000Z","level":"info","target":"miroir.request","request_id":"abc12345","pod_id":"pod-1","message":"GET /search 200"}"#,
|
||||
let logs = [
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.000Z","level":"info","target":"miroir.request","request_id":"abc12345","pod_id":"pod-1","message":"GET /search 200"}"#,
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.001Z","level":"debug","target":"miroir.node","request_id":"abc12345","pod_id":"pod-1","node_id":"node-1","message":"node call started"}"#,
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.010Z","level":"info","target":"miroir.search","request_id":"abc12345","pod_id":"pod-1","index":"products","message":"search completed"}"#];
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.010Z","level":"info","target":"miroir.search","request_id":"abc12345","pod_id":"pod-1","index":"products","message":"search completed"}"#,
|
||||
];
|
||||
|
||||
// Extract all logs with request_id = "abc12345"
|
||||
let target_id = "abc12345";
|
||||
|
|
@ -229,9 +231,11 @@ fn test_no_document_content_in_logs() {
|
|||
fn test_log_volume_info_level() {
|
||||
// At INFO level, search requests produce 2 INFO log entries:
|
||||
// 1 from telemetry middleware (miroir.request) + 1 from search handler (miroir.search)
|
||||
let request_logs = [r#"{"timestamp":"2026-05-01T12:00:00.000Z","level":"info","target":"miroir.request","message":"GET /indexes/products/search 200"}"#,
|
||||
let request_logs = [
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.000Z","level":"info","target":"miroir.request","message":"GET /indexes/products/search 200"}"#,
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.001Z","level":"debug","target":"miroir.node","message":"node call"}"#,
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.002Z","level":"info","target":"miroir.search","message":"search completed"}"#];
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.002Z","level":"info","target":"miroir.search","message":"search completed"}"#,
|
||||
];
|
||||
|
||||
let info_count = request_logs
|
||||
.iter()
|
||||
|
|
@ -247,10 +251,12 @@ fn test_log_volume_info_level() {
|
|||
#[test]
|
||||
fn test_debug_level_has_more_logs() {
|
||||
// At DEBUG level, we get per-node logs in addition to the INFO logs
|
||||
let debug_logs = [r#"{"timestamp":"2026-05-01T12:00:00.000Z","level":"info","target":"miroir.request","message":"GET / 200"}"#,
|
||||
let debug_logs = [
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.000Z","level":"info","target":"miroir.request","message":"GET / 200"}"#,
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.001Z","level":"debug","target":"miroir.node","message":"node call started"}"#,
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.002Z","level":"debug","target":"miroir.node","message":"node call completed"}"#,
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.003Z","level":"info","target":"miroir.search","message":"search completed"}"#];
|
||||
r#"{"timestamp":"2026-05-01T12:00:00.003Z","level":"info","target":"miroir.search","message":"search completed"}"#,
|
||||
];
|
||||
|
||||
let debug_count = debug_logs
|
||||
.iter()
|
||||
|
|
@ -570,7 +576,7 @@ async fn test_request_id_appears_in_all_log_lines_within_request() {
|
|||
// Acceptance criterion: Every log line inside a request must carry request_id=<id>
|
||||
// This is achieved via tracing::Span with request_id recorded on span enter
|
||||
// and tracing_subscriber::fmt().with_current_span(true)
|
||||
|
||||
|
||||
use axum::{routing::get, Extension};
|
||||
use miroir_core::config::MiroirConfig;
|
||||
use miroir_proxy::middleware::{
|
||||
|
|
@ -659,7 +665,8 @@ async fn test_request_id_appears_in_all_log_lines_within_request() {
|
|||
|
||||
// Every log line should contain the request_id (either at top level or in span)
|
||||
for line in &log_lines {
|
||||
let json = parse_log_line(line).unwrap_or_else(|| panic!("Log line should be valid JSON: {line}"));
|
||||
let json =
|
||||
parse_log_line(line).unwrap_or_else(|| panic!("Log line should be valid JSON: {line}"));
|
||||
|
||||
// Verify request_id field exists and matches the response header
|
||||
// It can be at the top level OR nested in the span object
|
||||
|
|
|
|||
|
|
@ -164,7 +164,6 @@ fn test_feature_flag_exists() {
|
|||
{
|
||||
// If we're compiled with the tracing feature, the otel module should exist
|
||||
// This is verified by the fact that this test compiles and links
|
||||
assert!(true, "tracing feature is enabled");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
|
|
@ -173,7 +172,6 @@ fn test_feature_flag_exists() {
|
|||
let config = MiroirConfig::default();
|
||||
let _ = miroir_proxy::otel::init_otel_layer(&config);
|
||||
miroir_proxy::otel::shutdown_otel();
|
||||
assert!(true, "tracing feature is disabled, no-ops work");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +184,6 @@ fn test_shutdown_otel_is_safe_to_call() {
|
|||
// shutdown_otel should be safe to call regardless of feature flag
|
||||
// or whether tracing was initialized
|
||||
miroir_proxy::otel::shutdown_otel();
|
||||
assert!(true, "shutdown_otel completed without panic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -195,7 +192,6 @@ fn test_shutdown_multiple_times_is_safe() {
|
|||
miroir_proxy::otel::shutdown_otel();
|
||||
miroir_proxy::otel::shutdown_otel();
|
||||
miroir_proxy::otel::shutdown_otel();
|
||||
assert!(true, "Multiple shutdown_otel calls completed without panic");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -227,8 +223,6 @@ fn test_span_hierarchy_exists_in_code() {
|
|||
);
|
||||
|
||||
let _ = tracing::info_span!("merge", shard_count = 3, offset = 0, limit = 20);
|
||||
|
||||
assert!(true, "All span macros compile successfully");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ use std::collections::HashSet;
|
|||
|
||||
#[derive(Clone)]
|
||||
struct TestNode {
|
||||
#[allow(dead_code)]
|
||||
id: String,
|
||||
#[allow(dead_code)]
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +27,7 @@ impl TestNode {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn get(&self, path: &str) -> reqwest::Response {
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
|
|
@ -34,6 +37,7 @@ impl TestNode {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn post(&self, path: &str, body: serde_json::Value) -> reqwest::Response {
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
|
|
@ -44,6 +48,7 @@ impl TestNode {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn delete(&self, path: &str) -> reqwest::Response {
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
|
|
@ -56,6 +61,7 @@ impl TestNode {
|
|||
|
||||
struct TestCluster {
|
||||
proxy_url: String,
|
||||
#[allow(dead_code)]
|
||||
nodes: Vec<TestNode>,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue