mod alert;
mod api;
mod config;
#[cfg(test)]
mod e2e;
mod json;
mod mcp;
mod pipeline;
mod proxy;
mod receiver;
mod telemetry;
mod term;
mod tui;
mod ui;
mod update;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering::Relaxed;
use axum::Router;
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use config::Config;
const USAGE: &str = "mira [--config FILE] [--node NAME] [--grpc ADDR] [--http ADDR]
[--data-dir PATH] [--retention DURATION] [--offload URI]
[--max-request-bytes SIZE] [--queue N] [--shards N] [--wal]
[--self-telemetry] [--telemetry-interval DURATION]
[--alerts FILE] [--version]
mira mira [--config FILE] [--data-dir PATH] [--addr HOST[:PORT]]
mira proxy [--config FILE] [--http ADDR] [--max-request-bytes SIZE]
--replica http://HOST:PORT [--replica ...]
mira offload list [--config FILE] --offload URI
mira offload restore [--config FILE] --offload URI [--data-dir PATH]
mira offload push [--config FILE] --offload URI [--data-dir PATH]
mira update [--version VERSION] [--dry-run]
Flags override the config file, which overrides the defaults. Every value can
also come from the file via ${env:VAR} — see https://miradb.dev/config/.
`mira mira` opens the terminal UI. With --data-dir it reads a block directory
in-process and needs no server running; with --addr it queries one over HTTP.
`mira tui` is the same thing, for anyone who guesses that first.
--offload sends a block to an object store just before retention deletes it,
under the same directory name it had locally — so the store's own listing is
the catalog and there is nothing else to keep in sync. `mira offload list`
reads that listing; `mira offload restore` copies every block in it that is not
already local back into --data-dir, and is safe to re-run.
`mira offload push` is the same copy in the other direction and deletes
nothing. It is how a volume a scale-down left behind is re-homed: push it, then
restore it into a node that is still running. Give it a URI of its own —
`file:///archive/${node}` — so the restore pulls back one node's blocks rather
than the whole archive.
`mira proxy` is one OTLP and query surface in front of N storage nodes. It
stores nothing: exports are split by entity and forwarded, and `/api/v1/query`
is answered by merging every replica's page on the cursor order. The reads it
cannot merge — correlate, map, metrics and entities — answer 501 naming
themselves rather than returning one node's share of the answer.
`mira update` replaces this binary with the latest GitHub release, using the
same installer as the curl one-liner at https://miradb.dev/install/.";
fn load_from(argv: Vec<String>) -> Result<Config, String> {
let mut cfg = match argv.iter().position(|a| a == "--config") {
Some(i) => Config::load(Path::new(argv.get(i + 1).ok_or("--config needs a value")?))?,
None => Config::default(),
};
let mut it = argv.into_iter();
while let Some(flag) = it.next() {
let mut value = || it.next().ok_or_else(|| format!("{flag} needs a value"));
match flag.as_str() {
"--config" => {
value()?;
}
"--node" => cfg.node = value()?,
"--grpc" => cfg.grpc = value()?.parse().map_err(|e| format!("--grpc: {e}"))?,
"--http" => cfg.http = value()?.parse().map_err(|e| format!("--http: {e}"))?,
"--data-dir" => cfg.data_dir = PathBuf::from(value()?),
"--retention" => cfg.retention = config::duration(&value()?)?,
"--offload" => cfg.offload = Some(value()?),
"--max-request-bytes" => cfg.max_request_bytes = config::bytes(&value()?)?,
"--queue" => cfg.queue = config::positive(&value()?)?,
"--shards" => cfg.shards = config::whole(&value()?)?,
"--telemetry-interval" => cfg.telemetry_interval = config::duration(&value()?)?,
"--alerts" => cfg.alerts = Some(PathBuf::from(value()?)),
"--replica" => cfg.replicas.extend(config::replicas(&value()?)?),
"--wal" => cfg.wal = true,
"--self-telemetry" => cfg.self_telemetry = true,
other => return Err(format!("unknown flag {other}\n\n{USAGE}")),
}
}
Ok(cfg)
}
fn offload_cmd(argv: &[String]) -> Result<(), String> {
let verb = argv.first().map(String::as_str).unwrap_or("");
if !matches!(verb, "list" | "restore" | "push") {
return Err(format!(
"mira offload takes `list`, `restore` or `push`\n\n{USAGE}"
));
}
let cfg = load_from(argv[1..].to_vec())?;
let uri = cfg
.offload
.as_deref()
.ok_or_else(|| format!("mira offload needs --offload URI\n\n{USAGE}"))?;
let target = mira_core::offload::Target::parse(uri).map_err(|e| e.to_string())?;
let node = mira_core::block::node_id(&cfg.node);
if verb == "restore" {
mira_core::block::check_filesystem(&cfg.data_dir).map_err(|e| e.to_string())?;
}
let (mut blocks, mut bytes) = (0u64, 0u64);
for signal in pipeline::SIGNALS {
let source = match verb {
"push" => mira_core::block::scan(&cfg.data_dir, signal),
_ => target.list(signal),
};
for b in source.map_err(|e| e.to_string())? {
let name = b.dir.file_name().unwrap_or_default().to_string_lossy();
let size = block_bytes(&b.dir);
blocks += 1;
bytes += size;
let what = match verb {
"restore" => match target
.pull(signal, &b, &cfg.data_dir, node)
.map_err(|e| e.to_string())?
{
true => "restored",
false => "present",
},
"push" => match target.push(signal, &b).map_err(|e| e.to_string())? {
true => "pushed",
false => "present",
},
_ => "",
};
println!(
"{signal:<8} {} .. {} {:>10} {name} {what}",
tui::stamp(b.min_ts),
tui::stamp(b.max_ts),
size,
);
}
}
println!("{blocks} blocks, {bytes} bytes");
Ok(())
}
fn block_bytes(dir: &Path) -> u64 {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.filter_map(|e| e.metadata().ok())
.filter(|m| m.is_file())
.map(|m| m.len())
.sum()
}
fn spawn_probes() {
tokio::spawn(async {
loop {
let t = std::time::Instant::now();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
mira_core::diag::RUNTIME_LAG
.record(t.elapsed().as_nanos().saturating_sub(50_000_000) as u64);
}
});
tokio::spawn(async {
let mut t = tokio::time::interval(std::time::Duration::from_secs(5));
loop {
t.tick().await;
tracing::debug!(target: "mira::probe", "{}", mira_core::diag::dump());
}
});
}
fn tui_source(argv: &[String]) -> Result<tui::Source, String> {
let mut cfg = match argv.iter().position(|a| a == "--config") {
Some(i) => Config::load(Path::new(argv.get(i + 1).ok_or("--config needs a value")?))?,
None => Config::default(),
};
let mut addr = None;
let mut it = argv.iter().cloned();
while let Some(flag) = it.next() {
let mut value = || it.next().ok_or_else(|| format!("{flag} needs a value"));
match flag.as_str() {
"--config" => {
value()?;
}
"--data-dir" => cfg.data_dir = PathBuf::from(value()?),
"--addr" => addr = Some(tui::parse_addr(&value()?)?),
other => return Err(format!("unknown flag {other}\n\n{USAGE}")),
}
}
Ok(match addr {
Some(a) => tui::Source::Remote(a),
None => tui::Source::Local(cfg.data_dir),
})
}
fn check_source(src: &tui::Source, on_a_tty: bool) -> Result<(), Box<dyn std::error::Error>> {
let tui::Source::Local(dir) = src else {
return Ok(());
};
if !on_a_tty {
return Ok(());
}
if !dir.is_dir() {
return Err(format!(
"{} is not a directory. `mira mira --data-dir` reads an existing block \
directory in place and creates nothing, so this is a volume that never \
mounted, a typo, or the path a different replica writes to. An empty \
but real directory is fine and shows no rows.",
dir.display()
)
.into());
}
let to_stderr = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_ansi(std::io::stderr().is_terminal())
.without_time()
.finish();
tracing::subscriber::with_default(to_stderr, || mira_core::block::check_filesystem(dir))?;
Ok(())
}
fn main() {
if let Err(e) = run() {
eprintln!("mira: {e}");
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let argv: Vec<String> = std::env::args().skip(1).collect();
if argv.first().is_some_and(|a| a == "update") {
return update::run(&argv[1..]).map_err(Into::into);
}
if argv.iter().any(|a| a == "-h" || a == "--help") {
println!("{USAGE}");
return Ok(());
}
if argv.iter().any(|a| a == "-V" || a == "--version") {
println!("mira {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if argv.first().is_some_and(|a| a == "offload") {
return offload_cmd(&argv[1..]).map_err(|e| -> Box<dyn std::error::Error> { e.into() });
}
if argv.first().is_some_and(|a| a == "mira" || a == "tui") {
let src = tui_source(&argv[1..]).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
let on_a_tty = std::io::stdin().is_terminal() && std::io::stdout().is_terminal();
check_source(&src, on_a_tty)?;
return tui::run(src).map_err(Into::into);
}
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "mira=info,mira_core=info".into()),
)
.with_ansi(std::io::stdout().is_terminal())
.init();
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
match argv.first().is_some_and(|a| a == "proxy") {
true => rt.block_on(proxy_cmd(argv[1..].to_vec())),
false => rt.block_on(serve()),
}
}
async fn proxy_cmd(argv: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
let cfg = load_from(argv).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
let p = proxy::Proxy::new(cfg.replicas.clone(), cfg.max_request_bytes)
.map_err(|e| -> Box<dyn std::error::Error> { format!("{e}\n\n{USAGE}").into() })?;
let socket = tokio::net::TcpListener::bind(cfg.http).await?;
let addr = socket.local_addr()?;
tracing::info!(
http = %addr,
replicas = %cfg.replicas.join(" "),
max_request_bytes = cfg.max_request_bytes,
"mira proxy listening"
);
axum::serve(socket, proxy::router(p))
.with_graceful_shutdown(shutdown())
.await?;
Ok(())
}
async fn serve() -> Result<(), Box<dyn std::error::Error>> {
let argv = std::env::args().skip(1).collect();
let cfg = load_from(argv).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
serve_with(cfg, shutdown()).await
}
async fn serve_with(
cfg: Config,
stop_signal: impl std::future::Future<Output = ()>,
) -> Result<(), Box<dyn std::error::Error>> {
if !cfg.replicas.is_empty() {
return Err(format!(
"--replica / proxy.replicas is read by `mira proxy`; this is a storage node\n\n{USAGE}"
)
.into());
}
let rules = match &cfg.alerts {
Some(p) => alert::Rules::load(p)?,
None => alert::Rules::off(),
};
std::fs::create_dir_all(&cfg.data_dir).map_err(|e| {
format!(
"cannot create data directory {}: {e}. Mira makes this path on first \
start, so this is a parent that is not writable or something that is \
not a directory already sitting there (in Kubernetes: a subPath that \
names a file, or a volume mounted readOnly).",
cfg.data_dir.display()
)
})?;
mira_core::block::check_filesystem(&cfg.data_dir)?;
mira_core::block::check_writable(&cfg.data_dir)?;
let node = mira_core::block::node_id(&cfg.node);
let taken = |addr: std::net::SocketAddr, what: &str, e: std::io::Error| {
format!(
"cannot bind {addr} for {what}: {e}. Nothing has started yet, so this \
is another process on the port — most often the previous instance \
still draining (in Kubernetes: a terminationGracePeriodSeconds \
shorter than the drain takes, or two replicas sharing a hostPort)."
)
};
let grpc_socket = tonic::transport::server::TcpIncoming::bind(cfg.grpc)
.map_err(|e| taken(cfg.grpc, "OTLP/gRPC", e))?
.with_nodelay(Some(true));
let http_socket = tokio::net::TcpListener::bind(cfg.http)
.await
.map_err(|e| taken(cfg.http, "OTLP/HTTP and the query API", e))?;
let (grpc_addr, http_addr) = (grpc_socket.local_addr()?, http_socket.local_addr()?);
let data_dir = std::sync::Arc::new(cfg.data_dir.clone());
let _ = *START;
let wal = match cfg.wal {
true => Some(std::sync::Arc::new(mira_core::wal::Wal::open(
&cfg.data_dir,
node,
)?)),
false => None,
};
let node_name = cfg.node.clone();
let pcfg = std::sync::Arc::new(pipeline::Config {
data_dir: cfg.data_dir,
node,
retention: cfg.retention,
offload: cfg
.offload
.as_deref()
.map(mira_core::offload::Target::parse)
.transpose()?,
queue: cfg.queue,
shards: pipeline::shard_count(
cfg.shards,
std::thread::available_parallelism().map_or(1, |n| n.get()),
),
wal: wal.clone(),
..Default::default()
});
let (logs, o_logs, h_logs) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
let (traces, o_traces, h_traces) = pipeline::spawn::<mira_core::traces::TracesBuilder>(&pcfg);
let (metrics, o_metrics, h_metrics) =
pipeline::spawn::<mira_core::metrics::MetricsBuilder>(&pcfg);
let flushers = [h_logs, h_traces, h_metrics];
let open_blocks = [o_logs, o_traces, o_metrics];
if wal.is_some() {
replay(
&pcfg.data_dir,
node,
logs.clone(),
traces.clone(),
metrics.clone(),
)
.await?;
}
let sampler = cfg.self_telemetry.then(|| {
tracing::info!(
interval_s = cfg.telemetry_interval.as_secs(),
"storing this node's own telemetry in this node"
);
tokio::spawn(telemetry::run(
node_name,
pcfg.data_dir.clone(),
cfg.telemetry_interval,
metrics.clone(),
))
});
let recv = receiver::Receivers {
logs,
traces,
metrics,
max_request_bytes: cfg.max_request_bytes,
};
pipeline::spawn_retention(pcfg);
let (stop, stop_rx) = tokio::sync::watch::channel(());
let stopped = |mut rx: tokio::sync::watch::Receiver<()>| async move {
let _ = rx.changed().await;
};
let grpc = tokio::spawn(
tonic::transport::Server::builder()
.add_service(recv.logs_server())
.add_service(recv.traces_server())
.add_service(recv.metrics_server())
.serve_with_incoming_shutdown(grpc_socket, stopped(stop_rx.clone())),
);
let api = api::Api {
data_dir: std::sync::Arc::clone(&data_dir),
open: open_blocks,
alerts: std::sync::Arc::new(alert::Engine::new(rules)),
};
alert::spawn(api.clone());
if tracing::enabled!(target: "mira::probe", tracing::Level::DEBUG) {
spawn_probes();
}
let serve = axum::serve(
http_socket,
receiver::http_router(recv)
.merge(api::router(api.clone()).layer(axum::middleware::from_fn(timed)))
.merge(mcp::router(api.clone()))
.merge(alert::router(api))
.merge(ops_router(std::sync::Arc::clone(&data_dir)))
.merge(ui::router()),
)
.with_graceful_shutdown(stopped(stop_rx));
let http = tokio::spawn(async move { serve.await });
tracing::info!(
grpc = %grpc_addr, http = %http_addr,
ui = %format!("http://{http_addr}/"),
node = %cfg.node, node_id = format!("{node:08x}"),
data_dir = %data_dir.display(),
retention = %format!("{}s", cfg.retention.as_secs()),
max_request_bytes = cfg.max_request_bytes,
"mira listening"
);
let (mut grpc, mut http) = (grpc, http);
let mut flushers = flushers;
let mut wedged = false;
tokio::select! {
r = &mut grpc => r??,
r = &mut http => r??,
_ = first_stopped(&mut flushers) => wedged = true,
_ = stop_signal => tracing::info!("draining"),
}
if let Some(s) = sampler {
s.abort();
let _ = s.await;
}
drain(stop, grpc, http, flushers, DRAIN_GRACE).await;
tracing::info!("stopped");
if wedged {
return Err("a flusher stopped, so one signal can no longer be stored; \
exiting for the supervisor to restart (the cause is logged above)"
.into());
}
Ok(())
}
const DRAIN_GRACE: std::time::Duration = std::time::Duration::from_secs(15);
async fn drain<G, H>(
stop: tokio::sync::watch::Sender<()>,
grpc: tokio::task::JoinHandle<G>,
http: tokio::task::JoinHandle<H>,
flushers: [pipeline::Flushers; 3],
grace: std::time::Duration,
) {
let _ = stop.send(());
let landed = async move {
let _ = grpc.await;
let _ = http.await;
for h in flushers {
let _ = h.await;
}
};
if tokio::time::timeout(grace, landed).await.is_err() {
tracing::warn!(?grace, "did not drain in time; exiting anyway");
}
}
async fn replay(
dir: &Path,
node: u32,
logs: pipeline::Ingest<mira_proto::collector::logs::v1::ExportLogsServiceRequest>,
traces: pipeline::Ingest<mira_proto::collector::trace::v1::ExportTraceServiceRequest>,
metrics: pipeline::Ingest<mira_proto::collector::metrics::v1::ExportMetricsServiceRequest>,
) -> Result<(), Box<dyn std::error::Error>> {
use mira_core::wal::Signal;
let dir = dir.to_path_buf();
let started = std::time::Instant::now();
let done = tokio::task::spawn_blocking(move || {
let watermarks = mira_core::block::wal_watermarks(&dir, node)?;
let mut undecodable = 0u64;
let out = mira_core::wal::Wal::replay(&dir, node, watermarks, |signal, seq, body| {
let pushed = match signal {
Signal::Logs => logs.replay(body, seq),
Signal::Traces => traces.replay(body, seq),
Signal::Metrics => metrics.replay(body, seq),
};
match pushed {
Ok(()) | Err(pipeline::Rejected::Failed(_)) => {
undecodable += u64::from(pushed.is_err());
Ok(())
}
Err(_) => Err(mira_core::Error::WalCorrupt {
path: dir.clone(),
why: "the flusher for this signal stopped during replay",
}),
}
})?;
Ok::<_, mira_core::Error>((out, undecodable))
})
.await??;
let (out, undecodable) = done;
if undecodable > 0 {
tracing::error!(
frames = undecodable,
"write-ahead log frames passed their checksum and would not decode as OTLP; \
those exports are gone"
);
}
if out.torn_segments > 0 {
tracing::info!(
segments = out.torn_segments,
"write-ahead log segments ended in a torn frame; that is what a crash looks like"
);
}
if out.replayed > 0 || out.skipped > 0 {
tracing::info!(
replayed = out.replayed,
skipped = out.skipped,
bytes = out.bytes,
elapsed_ms = started.elapsed().as_millis() as u64,
"recovered from the write-ahead log"
);
}
Ok(())
}
async fn first_stopped(flushers: &mut [pipeline::Flushers; 3]) {
let [logs, traces, metrics] = flushers;
tokio::select! {
_ = logs => {}
_ = traces => {}
_ = metrics => {}
}
}
fn ops_router(data_dir: std::sync::Arc<PathBuf>) -> Router {
Router::new()
.route("/health", get(health))
.route("/readyz", get(readyz))
.route("/api/v1/stats", get(stats))
.with_state(data_dir)
}
async fn health() -> Response {
let mut j = mira_core::json::Json::new();
j.obj(|j| {
j.key("status");
j.str("ok");
for r in &pipeline::REJECTS {
j.key(r.signal);
j.obj(|j| {
j.key("shed");
j.u64(r.shed.load(Relaxed));
j.key("failed");
j.u64(r.failed.load(Relaxed));
});
}
});
json_ok(j.into_string())
}
async fn readyz() -> Response {
ready(pipeline::stalled())
}
fn ready(stalled: Option<(&'static str, u64)>) -> Response {
let mut j = mira_core::json::Json::new();
j.obj(|j| match stalled {
None => {
j.key("status");
j.str("ok");
}
Some((signal, secs)) => {
j.key("status");
j.str("unavailable");
j.key("signal");
j.str(signal);
j.key("stalled_s");
j.u64(secs);
j.key("reason");
j.str(
"this node has not been able to store an export for this signal; \
the usual cause is a full or unwritable volume",
);
}
});
let code = match stalled {
None => axum::http::StatusCode::OK,
Some(_) => axum::http::StatusCode::SERVICE_UNAVAILABLE,
};
(
code,
[(axum::http::header::CONTENT_TYPE, "application/json")],
j.into_string(),
)
.into_response()
}
static QUERIES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static QUERY_NANOS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static QUERY_MAX_NANOS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static START: std::sync::LazyLock<std::time::Instant> =
std::sync::LazyLock::new(std::time::Instant::now);
async fn timed(req: axum::extract::Request, next: axum::middleware::Next) -> Response {
let t = std::time::Instant::now();
let res = next.run(req).await;
let ns = t.elapsed().as_nanos() as u64;
QUERIES.fetch_add(1, Relaxed);
QUERY_NANOS.fetch_add(ns, Relaxed);
QUERY_MAX_NANOS.fetch_max(ns, Relaxed);
res
}
async fn stats(
axum::extract::State(dir): axum::extract::State<std::sync::Arc<PathBuf>>,
) -> Response {
let disk = tokio::task::spawn_blocking(move || {
(
mira_core::block::free_fraction(&dir).ok(),
pipeline::SIGNALS.map(|s| mira_core::block::scan(&dir, s).ok().map(|b| b.len() as u64)),
)
})
.await;
let (free, on_disk) = disk.unwrap_or((None, [None; 3]));
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let age = |since: u64| (since != 0).then_some(now.saturating_sub(since));
let queries = QUERIES.load(Relaxed);
let mut j = mira_core::json::Json::new();
j.obj(|j| {
j.key("uptime_s");
j.u64(START.elapsed().as_secs());
j.key("peak_rss_bytes");
j.u64(peak_rss());
j.key("free_fraction");
match free {
Some(f) => j.f64(f),
None => j.null(),
}
j.key("degraded_syncs");
j.u64(mira_core::degraded_syncs());
j.key("queries");
j.obj(|j| {
j.key("count");
j.u64(queries);
j.key("mean_ms");
j.f64(QUERY_NANOS.load(Relaxed) as f64 / queries.max(1) as f64 / 1e6);
j.key("max_ms");
j.f64(QUERY_MAX_NANOS.load(Relaxed) as f64 / 1e6);
});
j.key("signals");
j.obj(|j| {
for (r, blocks) in pipeline::REJECTS.iter().zip(on_disk) {
j.key(r.signal);
j.obj(|j| {
for (k, v) in [
("shed", r.shed.load(Relaxed)),
("failed", r.failed.load(Relaxed)),
("refused", r.refused.load(Relaxed)),
("blocks_published", r.published.load(Relaxed)),
("rows", r.rows.load(Relaxed)),
("bytes", r.bytes.load(Relaxed)),
] {
j.key(k);
j.u64(v);
}
for (k, v) in [
("blocks_on_disk", blocks),
("open_block_age_s", age(r.open_since.load(Relaxed))),
("stalled_s", age(r.stalled_since.load(Relaxed))),
] {
j.key(k);
match v {
Some(v) => j.u64(v),
None => j.null(),
}
}
});
}
});
});
json_ok(j.into_string())
}
fn peak_rss() -> u64 {
let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) };
#[cfg(target_os = "macos")]
const UNIT: u64 = 1;
#[cfg(not(target_os = "macos"))]
const UNIT: u64 = 1024;
ru.ru_maxrss.max(0) as u64 * UNIT
}
fn json_ok(body: String) -> Response {
(
[(axum::http::header::CONTENT_TYPE, "application/json")],
body,
)
.into_response()
}
#[cfg(unix)]
async fn shutdown() {
let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("a SIGTERM handler on the serving runtime");
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
#[cfg(not(unix))]
async fn shutdown() {
let _ = tokio::signal::ctrl_c().await;
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(s: &str) -> Vec<String> {
s.split_whitespace().map(str::to_owned).collect()
}
fn tmp(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("mira-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
async fn stop_edge(immediately: bool) {
if !immediately {
std::future::pending::<()>().await;
}
}
#[tokio::test]
async fn the_stop_edge_fires_when_it_is_told_to_and_never_otherwise() {
let zero = std::time::Duration::ZERO;
let never = tokio::time::timeout(zero, stop_edge(false));
assert!(never.await.is_err(), "the never-stop edge stopped");
tokio::time::timeout(zero, stop_edge(true))
.await
.expect("the immediate edge did not fire");
}
#[tokio::test(start_paused = true)]
async fn the_probe_tasks_sample_the_runtime_and_dump_it() {
use std::sync::atomic::Ordering::Relaxed;
let lag = &mira_core::diag::RUNTIME_LAG;
let before = lag.n.load(Relaxed);
spawn_probes();
tokio::time::sleep(std::time::Duration::from_millis(120)).await;
assert_eq!(
lag.n.load(Relaxed),
before + 2,
"the lag sampler did not run once per 50 ms period"
);
assert_eq!(
lag.max_ns.load(Relaxed),
0,
"a wake that cost no real time was reported as late"
);
}
#[test]
fn offload_list_and_restore_read_the_store_and_nothing_else() {
let dir = tmp("offload-cmd");
let (store, data) = (dir.join("cold"), dir.join("data"));
std::fs::create_dir_all(&data).unwrap();
let uri = format!("file://{}", store.display());
for (args, want) in [
("", "list"),
("sync --offload file:///x", "restore"),
("list", "--offload"),
("list --offload s3://bucket", "s3://bucket"),
] {
let e = offload_cmd(&argv(args)).unwrap_err();
assert!(e.contains(want), "`mira offload {args}` said: {e}");
}
offload_cmd(&argv(&format!("list --offload {uri}"))).unwrap();
let name = format!("{:020}-{:020}-{:08x}-{:012}-{:020}", 1_000, 2_000, 7, 1, 0);
let block = store.join("logs").join("p=0").join(&name);
std::fs::create_dir_all(&block).unwrap();
std::fs::write(block.join("logs.arrow"), b"bytes").unwrap();
offload_cmd(&argv(&format!("list --offload {uri}"))).unwrap();
assert_eq!(
std::fs::read_dir(&data).unwrap().count(),
0,
"`list` copies nothing"
);
let restore = format!("restore --offload {uri} --data-dir {}", data.display());
offload_cmd(&argv(&restore)).unwrap();
let local = data.join("logs").join("p=0").join(&name);
assert_eq!(std::fs::read(local.join("logs.arrow")).unwrap(), b"bytes");
std::fs::write(local.join("logs.arrow"), b"local edit").unwrap();
offload_cmd(&argv(&restore)).unwrap();
assert_eq!(
std::fs::read(local.join("logs.arrow")).unwrap(),
b"local edit",
"a block already present is left alone"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn offload_push_copies_out_of_the_data_dir_and_unlinks_nothing() {
let dir = tmp("offload-push");
let (store, data) = (dir.join("cold"), dir.join("data"));
let uri = format!("file://{}", store.display());
let name = format!("{:020}-{:020}-{:08x}-{:012}-{:020}", 1_000, 2_000, 7, 1, 0);
let local = data.join("logs").join("p=0").join(&name);
std::fs::create_dir_all(&local).unwrap();
std::fs::write(local.join("logs.arrow"), b"bytes").unwrap();
let push = format!("push --offload {uri} --data-dir {}", data.display());
offload_cmd(&argv(&push)).unwrap();
let remote = store.join("logs").join("p=0").join(&name);
assert_eq!(std::fs::read(remote.join("logs.arrow")).unwrap(), b"bytes");
assert!(local.exists(), "push copies the block, it does not move it");
offload_cmd(&argv(&push)).unwrap();
std::fs::write(local.join("logs.arrow"), b"local edit").unwrap();
offload_cmd(&argv(&push)).unwrap_err();
assert_eq!(
std::fs::read(remote.join("logs.arrow")).unwrap(),
b"bytes",
"a block already in the store is left alone"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn flags_override_the_file_which_overrides_the_defaults() {
let dir = tmp("load");
let file = dir.join("mira.yaml");
std::fs::write(
&file,
r#"{ "node": "from-file",
"listen": { "grpc": "127.0.0.1:1", "http": "127.0.0.1:2" },
"storage": { "dir": "/from/file", "retention": "3h" },
"ingest": { "max_request_bytes": "1MiB" } }"#,
)
.unwrap();
let f = file.display();
let c = load_from(argv(&format!("--config {f}"))).unwrap();
assert_eq!(c.node, "from-file");
assert_eq!(c.data_dir, PathBuf::from("/from/file"));
assert_eq!(c.retention, std::time::Duration::from_secs(3 * 3600));
assert_eq!(c.max_request_bytes, 1 << 20);
assert_eq!(c.http.port(), 2);
let c = load_from(argv(&format!(
"--config {f} --node cli --grpc 127.0.0.1:3 --http 127.0.0.1:4 \
--data-dir /from/cli --retention 30s --max-request-bytes 2MiB"
)))
.unwrap();
assert_eq!(c.node, "cli");
assert_eq!(c.grpc.port(), 3);
assert_eq!(c.http.port(), 4);
assert_eq!(c.data_dir, PathBuf::from("/from/cli"));
assert_eq!(c.retention, std::time::Duration::from_secs(30));
assert_eq!(c.max_request_bytes, 2 << 20);
let c = load_from(argv(
"--queue 4096 --self-telemetry --telemetry-interval 1m --wal",
))
.unwrap();
assert_eq!(c.queue, 4096);
assert!(c.self_telemetry);
assert!(c.wal);
assert_eq!(c.telemetry_interval, std::time::Duration::from_secs(60));
let c = load_from(argv("--offload file:///srv/cold")).unwrap();
assert_eq!(c.offload.as_deref(), Some("file:///srv/cold"));
let d = load_from(vec![]).unwrap();
assert_eq!(d.node, Config::default().node);
assert!(!d.self_telemetry, "self-telemetry is opt-in");
for (args, want) in [
("--nope", "unknown flag --nope"),
("--peers a:1", "unknown flag --peers"),
("--node", "--node needs a value"),
("--config", "--config needs a value"),
("--grpc nope", "--grpc:"),
("--http nope", "--http:"),
("--retention nope", "not a duration"),
("--max-request-bytes nope", "not a size"),
("--queue nope", "not a whole number"),
("--queue 0", "at least 1"),
("--shards nope", "not a whole number"),
("--shards -1", "not a whole number"),
("--telemetry-interval nope", "not a duration"),
("--config /no/such/file.yaml", "/no/such/file.yaml"),
] {
let e = load_from(argv(args)).unwrap_err();
assert!(e.contains(want), "{args:?} said {e:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_tui_reads_the_directory_its_config_writes_to() {
let dir = tmp("tui-src");
let file = dir.join("mira.yaml");
std::fs::write(&file, r#"{ "storage": { "dir": "/from/file" } }"#).unwrap();
let f = file.display();
let src = |a: &str| match tui_source(&argv(a)) {
Ok(tui::Source::Local(p)) => format!("local {}", p.display()),
Ok(tui::Source::Remote(a)) => format!("remote {a}"),
Err(e) => format!("error {}", e.lines().next().unwrap_or_default()),
};
let default_dir = Config::default().data_dir;
for (args, want) in [
(format!("--config {f}"), "local /from/file".to_owned()),
(
format!("--config {f} --data-dir /from/cli"),
"local /from/cli".to_owned(),
),
(String::new(), format!("local {}", default_dir.display())),
("--addr host:9999".into(), "remote host:9999".to_owned()),
("--nope".into(), "error unknown flag --nope".to_owned()),
(
"--data-dir".into(),
"error --data-dir needs a value".to_owned(),
),
("--config".into(), "error --config needs a value".to_owned()),
] {
assert_eq!(src(&args), want, "{args:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn the_server_starts_from_a_config_and_drains_when_stopped() {
let dir = tmp("serve");
let cfg = Config {
data_dir: dir.join("data"),
grpc: "127.0.0.1:0".parse().unwrap(),
http: "127.0.0.1:0".parse().unwrap(),
self_telemetry: true,
telemetry_interval: std::time::Duration::from_secs(3600),
..Config::default()
};
let log = dir.join("start.log");
let _logging = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_writer(std::fs::File::create(&log).unwrap())
.without_time()
.with_ansi(false)
.finish(),
);
let t = std::time::Instant::now();
serve_with(cfg.clone(), stop_edge(true)).await.unwrap();
assert!(
t.elapsed() < std::time::Duration::from_secs(15),
"timed out"
);
assert!(cfg.data_dir.is_dir());
let logged = std::fs::read_to_string(&log).unwrap();
assert!(logged.contains("mira listening"), "{logged}");
assert!(!logged.contains("127.0.0.1:0"), "{logged}");
assert!(logged.contains("ui=http://127.0.0.1:"), "{logged}");
assert!(
logged.contains("storing this node's own telemetry"),
"{logged}"
);
assert!(
logged.contains(&format!("retention={}s", cfg.retention.as_secs())),
"{logged}"
);
let held = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = held.local_addr().unwrap();
for taken in [
Config {
http: addr,
..cfg.clone()
},
Config {
grpc: addr,
..cfg.clone()
},
] {
let e = serve_with(taken, stop_edge(false))
.await
.unwrap_err()
.to_string();
assert!(e.contains(&addr.to_string()), "{e}");
assert!(e.to_lowercase().contains("address"), "{e}");
}
let file = dir.join("a-file");
std::fs::write(&file, b"").unwrap();
let e = serve_with(
Config {
data_dir: file.clone(),
..cfg
},
stop_edge(false),
)
.await
.unwrap_err()
.to_string();
assert!(e.contains(&file.display().to_string()), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_node_refuses_to_start_on_a_rules_file_or_a_log_it_cannot_open() {
let dir = tmp("boot-guards");
let rules = dir.join("alerts.kyaml");
let cfg = |alerts: Option<PathBuf>, data_dir: PathBuf| Config {
data_dir,
grpc: "127.0.0.1:0".parse().unwrap(),
http: "127.0.0.1:0".parse().unwrap(),
alerts,
..Config::default()
};
std::fs::write(
&rules,
r#"{ "rules": [ { "name": "any-log", "over": "1m", "when": "count >= 1",
"query": { "signal": "logs" } } ] }"#,
)
.unwrap();
serve_with(
cfg(Some(rules.clone()), dir.join("ok")),
std::future::ready(()),
)
.await
.expect("a node with rules starts");
std::fs::write(&rules, "{ rules: nope }").unwrap();
let never_made = dir.join("not-made");
let e = serve_with(
cfg(Some(rules.clone()), never_made.clone()),
std::future::pending(),
)
.await
.unwrap_err()
.to_string();
assert!(e.contains("alerts.kyaml"), "{e}");
assert!(!never_made.exists(), "the boot got past the rules file");
let wal_blocked = dir.join("wal-blocked");
std::fs::create_dir_all(&wal_blocked).unwrap();
std::fs::write(wal_blocked.join(".wal"), b"not a directory").unwrap();
let cfg = Config {
wal: true,
..cfg(None, wal_blocked.clone())
};
let e = serve_with(cfg, std::future::pending())
.await
.unwrap_err()
.to_string();
assert!(e.contains(".wal"), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_flusher_that_cannot_start_stops_the_server() {
let dir = tmp("wedged");
std::fs::write(dir.join("logs"), b"not a directory").unwrap();
let cfg = Config {
data_dir: dir.clone(),
grpc: "127.0.0.1:0".parse().unwrap(),
http: "127.0.0.1:0".parse().unwrap(),
wal: false,
..Config::default()
};
let e = serve_with(cfg, stop_edge(false))
.await
.unwrap_err()
.to_string();
assert!(e.contains("flusher"), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_drain_that_never_lands_leaves_anyway() {
let (stop, rx) = tokio::sync::watch::channel(());
let wedged = || tokio::spawn(std::future::pending::<()>());
drain(
stop,
wedged(),
wedged(),
std::array::from_fn(|_| pipeline::Flushers::wedged()),
std::time::Duration::from_millis(1),
)
.await;
assert!(
rx.has_changed().is_err(),
"drain owns the sender to the end"
);
}
async fn text(r: Response) -> (axum::http::StatusCode, String) {
let (parts, body) = r.into_parts();
let body = axum::body::to_bytes(body, 1 << 20).await.unwrap();
(parts.status, String::from_utf8(body.to_vec()).unwrap())
}
#[tokio::test]
async fn health_reports_every_signals_rejections() {
let (status, body) = text(health().await).await;
assert_eq!(status, axum::http::StatusCode::OK);
assert!(body.starts_with(r#"{"status":"ok""#), "{body}");
for signal in pipeline::SIGNALS {
assert!(body.contains(&format!(r#""{signal}":{{"shed":"#)), "{body}");
}
}
#[tokio::test]
async fn readiness_fails_only_once_a_signal_has_been_unable_to_store() {
let (status, body) = text(ready(None)).await;
assert_eq!(status, axum::http::StatusCode::OK);
assert_eq!(body, r#"{"status":"ok"}"#);
let (status, body) = text(ready(Some(("logs", 300)))).await;
assert_eq!(status, axum::http::StatusCode::SERVICE_UNAVAILABLE);
assert!(body.contains(r#""signal":"logs""#), "{body}");
assert!(body.contains(r#""stalled_s":300"#), "{body}");
let (status, _) = text(readyz().await).await;
assert_eq!(status, axum::http::StatusCode::OK);
}
#[tokio::test]
async fn stats_reports_what_this_node_is_doing_with_its_disk() {
let dir = tmp("stats");
let (status, body) =
text(stats(axum::extract::State(std::sync::Arc::new(dir.clone()))).await).await;
assert_eq!(status, axum::http::StatusCode::OK);
for key in [
r#""uptime_s":"#,
r#""free_fraction":"#,
r#""queries":{"count":"#,
r#""mean_ms":"#,
r#""max_ms":"#,
] {
assert!(body.contains(key), "{key} missing from {body}");
}
for signal in pipeline::SIGNALS {
assert!(body.contains(&format!(r#""{signal}":{{"shed":"#)), "{body}");
}
for key in [
"refused",
"blocks_published",
"rows",
"bytes",
"blocks_on_disk",
"open_block_age_s",
"stalled_s",
] {
assert!(body.contains(&format!(r#""{key}":"#)), "{key}: {body}");
}
assert!(body.contains(r#""blocks_on_disk":0"#), "{body}");
assert!(body.contains(r#""open_block_age_s":null"#), "{body}");
let free = api::parse(&body).expect("the document is KYAML")["free_fraction"]
.as_f64()
.expect("a readable volume reports a fraction");
assert!(
free > 0.0 && free <= 1.0,
"free_fraction is a fraction of the volume: {free}"
);
let gone = std::sync::Arc::new(dir.join("no-such-volume"));
let (status, body) = text(stats(axum::extract::State(gone)).await).await;
assert_eq!(status, axum::http::StatusCode::OK);
assert!(body.contains(r#""free_fraction":null"#), "{body}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_peak_resident_set_is_reported_in_bytes() {
let rss = peak_rss();
assert!(rss > 1 << 20, "{rss} bytes is below a running process");
assert!(rss < 100 << 30, "{rss} bytes is a unit mistake, not an RSS");
}
#[test]
fn the_tui_refuses_a_data_directory_the_server_would_have_refused() {
let dir = tmp("tui-guard");
let local = |p: &Path| tui::Source::Local(p.to_path_buf());
check_source(&local(&dir), true).unwrap();
for bad in [dir.join("nope"), {
let f = dir.join("a-file");
std::fs::write(&f, b"").unwrap();
f
}] {
let e = check_source(&local(&bad), true).unwrap_err().to_string();
assert!(e.contains(&bad.display().to_string()), "{e}");
assert!(e.contains("not a directory"), "{e}");
check_source(&local(&bad), false).unwrap();
let remote = tui::Source::Remote(bad.display().to_string());
check_source(&remote, true).unwrap();
}
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_boot_replays_every_frame_no_block_claims() {
use mira_core::wal::Signal;
use mira_proto::collector::metrics::v1::ExportMetricsServiceRequest;
use mira_proto::collector::trace::v1::ExportTraceServiceRequest;
use mira_proto::metrics::v1::metric::Data;
use mira_proto::metrics::v1::number_data_point::Value as NumValue;
use mira_proto::metrics::v1::{
Gauge, Metric, NumberDataPoint, ResourceMetrics, ScopeMetrics,
};
use mira_proto::trace::v1::{ResourceSpans, ScopeSpans, Span};
use prost::Message as _;
let dir = tmp("replay");
let node = mira_core::block::node_id("replaynode");
let spans = ExportTraceServiceRequest {
resource_spans: vec![ResourceSpans {
scope_spans: vec![ScopeSpans {
spans: vec![Span {
trace_id: vec![0x11; 16].into(),
span_id: vec![0x22; 8].into(),
name: "GET /".into(),
start_time_unix_nano: 3_000,
end_time_unix_nano: 3_500,
..Default::default()
}],
..Default::default()
}],
..Default::default()
}],
};
let points = ExportMetricsServiceRequest {
resource_metrics: vec![ResourceMetrics {
scope_metrics: vec![ScopeMetrics {
metrics: vec![Metric {
name: "process.cpu".into(),
data: Some(Data::Gauge(Gauge {
data_points: vec![NumberDataPoint {
time_unix_nano: 4_000,
value: Some(NumValue::AsDouble(0.5)),
..Default::default()
}],
})),
..Default::default()
}],
..Default::default()
}],
..Default::default()
}],
};
{
let wal = mira_core::wal::Wal::open(&dir, node).unwrap();
let logs = e2e::logs_export("checkout", 2_000, 4).encode_to_vec();
wal.append(Signal::Logs, &logs).unwrap();
wal.append(Signal::Traces, &spans.encode_to_vec()).unwrap();
wal.append(Signal::Metrics, &points.encode_to_vec())
.unwrap();
wal.append(Signal::Logs, b"\x08").unwrap();
}
let wal = std::sync::Arc::new(mira_core::wal::Wal::open(&dir, node).unwrap());
let pcfg = std::sync::Arc::new(pipeline::Config {
data_dir: dir.clone(),
node,
wal: Some(wal),
max_block_age: std::time::Duration::from_millis(50),
..Default::default()
});
let (logs, _ol, h_logs) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
let (traces, _ot, h_traces) = pipeline::spawn::<mira_core::traces::TracesBuilder>(&pcfg);
let (metrics, _om, h_metrics) =
pipeline::spawn::<mira_core::metrics::MetricsBuilder>(&pcfg);
replay(&dir, node, logs.clone(), traces.clone(), metrics.clone())
.await
.unwrap();
drop((logs, traces, metrics));
for h in [h_logs, h_traces, h_metrics] {
h.await.unwrap();
}
for signal in pipeline::SIGNALS {
let published = mira_core::block::scan(&dir, signal).unwrap();
assert_eq!(published.len(), 1, "{signal} did not store its frame");
}
assert_eq!(
mira_core::block::wal_watermarks(&dir, node).unwrap(),
[4, 4, 4]
);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_boot_after_a_hard_kill_reports_the_torn_tail_and_what_it_recovered() {
use mira_core::wal::Signal;
use prost::Message as _;
let dir = tmp("torn-replay");
let node = mira_core::block::node_id("tornnode");
{
let wal = mira_core::wal::Wal::open(&dir, node).unwrap();
for i in 0..2 {
let body = e2e::logs_export("checkout", 2_000 + i, 1).encode_to_vec();
wal.append(Signal::Logs, &body).unwrap();
}
wal.sync().unwrap();
}
let seg = dir
.join(".wal")
.join(format!("{node:08x}-{:020}.wal", 0u64));
let len = std::fs::metadata(&seg).unwrap().len();
std::fs::OpenOptions::new()
.write(true)
.open(&seg)
.unwrap()
.set_len(len - 4)
.unwrap();
let pcfg = std::sync::Arc::new(pipeline::Config {
data_dir: dir.clone(),
node,
max_block_age: std::time::Duration::from_millis(50),
..Default::default()
});
let (logs, _ol, h_logs) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
let (traces, _ot, h_traces) = pipeline::spawn::<mira_core::traces::TracesBuilder>(&pcfg);
let (metrics, _om, h_metrics) =
pipeline::spawn::<mira_core::metrics::MetricsBuilder>(&pcfg);
let (guard, log) = e2e::capture();
replay(&dir, node, logs.clone(), traces.clone(), metrics.clone())
.await
.expect("a torn tail is a recovery, not a refusal");
drop(guard);
let text = log.text();
assert!(text.contains("torn frame"), "{text}");
assert!(text.contains("segments=1"), "{text}");
assert!(
text.contains("recovered from the write-ahead log"),
"{text}"
);
assert!(text.contains("replayed=1"), "{text}");
assert!(text.contains("elapsed_ms="), "{text}");
drop((logs, traces, metrics));
for h in [h_logs, h_traces, h_metrics] {
h.await.unwrap();
}
assert_eq!(mira_core::block::scan(&dir, "logs").unwrap().len(), 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn a_replay_with_nowhere_to_put_a_frame_refuses_to_finish_the_boot() {
use mira_core::wal::Signal;
use prost::Message as _;
let dir = tmp("replay-closed");
let node = mira_core::block::node_id("closednode");
{
let wal = mira_core::wal::Wal::open(&dir, node).unwrap();
wal.append(
Signal::Logs,
&e2e::logs_export("checkout", 2_000, 1).encode_to_vec(),
)
.unwrap();
wal.sync().unwrap();
}
let pcfg = std::sync::Arc::new(pipeline::Config {
data_dir: dir.clone(),
node,
..Default::default()
});
let (logs, _ol, mut h_logs) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
let (traces, _ot, _ht) = pipeline::spawn::<mira_core::traces::TracesBuilder>(&pcfg);
let (metrics, _om, _hm) = pipeline::spawn::<mira_core::metrics::MetricsBuilder>(&pcfg);
h_logs.abort();
let _ = h_logs.await;
let e = replay(&dir, node, logs, traces, metrics)
.await
.expect_err("a frame with nowhere to go must stop the boot");
let e = e.to_string();
assert!(e.contains("flusher"), "{e}");
assert!(e.contains(&dir.display().to_string()), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
}