Watch
0
0
Fork
You've already forked hyperhive
0

add swarm-metrics, an agent CLI for the metrics store

swarm-metrics reads the swarm's VictoriaMetrics from inside an agent
container: query, query-range, series, labels and label-values, each one
GET against the store's Prometheus read API through the gateway, printing
the store's JSON. It authenticates the way swarm-logs does, with the
agent's own queue secret read from bao under the agent's leaf, sent as
a swarm-agent bearer.

On the metrics store's host the read API's auth map gains an agent arm
ahead of the Grafana one. It routes to swarm-metrics-agent-auth, a
swarm-logs-auth instance logged in under the existing swarm-metrics-auth
leaf, whose bao grant now includes swarm/agents/+/queue read. The check is
rate-limited per address like the log store's
(deploy.victoriametrics.agentAuth{Rate,Burst,VerdictTtl}).

nix/agent-modules/metrics.nix installs the wrapped binary when the hive
forwards HIVE_AGENT_METRICS_URL (from swarm.victoriametrics.domain) and a
store address, the same gate as swarm-logs. Adds the base:swarm-metrics
skill and the CLI reference with its freshness check.

Fixes #5395
This commit is contained in:
atlas 2026-10-17 00:55:46 +02:00 • committed by mara
commit e92f118bda
30 changed files with 1417 additions and 88 deletions

247
swarm-metrics/src/main.rs Normal file
View file

@ -0,0 +1,247 @@
//! `swarm-metrics` — an agent's CLI for the swarm metrics store.
//!
//! The metrics-store sibling of `swarm-logs`: the same credential, the same
//! wrapper shape, and the store's own query language. Each verb is one GET
//! against the store's Prometheus-compatible read API through the
//! gateway, and prints the store's JSON answer unchanged for `jq`.
//!
//! **Read-only.** The verbs map onto `/api/v1/query`, `query_range`,
//! `series`, `labels` and `label/<name>/values`; nothing here writes.
//!
//! ⚠️ **The route applies no scoping.** Any authenticated caller reads the
//! whole swarm's metrics, every hive's.
mod auth;
mod query;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use crate::query::{Request, Span};
#[derive(Parser)]
#[command(
name = "swarm-metrics",
version,
about = "query the swarm's metrics store from an agent"
)]
struct Cli {
#[command(subcommand)]
command: Verb,
}
/// Window and cap for the discovery verbs.
#[derive(Args)]
struct SpanArgs {
/// Start of the window: RFC 3339, a unix timestamp, or relative (`-1h`).
///
/// Unset means the store's default: the last day, from 00:00 UTC.
#[arg(long, allow_hyphen_values = true)]
start: Option<String>,
/// End of the window, in the same forms. Unset means now.
#[arg(long, allow_hyphen_values = true)]
end: Option<String>,
/// Return at most this many entries.
#[arg(long)]
limit: Option<u64>,
}
impl From<SpanArgs> for Span {
fn from(a: SpanArgs) -> Self {
Self {
start: a.start,
end: a.end,
limit: a.limit,
}
}
}
#[derive(Subcommand)]
enum Verb {
/// Evaluate a `MetricsQL` (`PromQL`) expression at one instant.
Query {
/// The expression. Quote it: `{`, `|` and `"` are the shell's too.
///
/// OTLP metric names keep their dots, so match them by
/// `{__name__="jobq.nodes"}` rather than as a bare name.
promql: String,
/// The instant: RFC 3339, a unix timestamp, or relative (`-5m`).
/// Unset means now.
#[arg(long, allow_hyphen_values = true)]
time: Option<String>,
},
/// Evaluate an expression over a window, one point per step.
QueryRange {
/// The expression, quoted as for `query`.
promql: String,
/// Start of the window: RFC 3339, a unix timestamp, or relative
/// (`-1h`). Required, so a range is always bounded.
#[arg(long, allow_hyphen_values = true)]
start: String,
/// End of the window, in the same forms. Unset means now.
#[arg(long, allow_hyphen_values = true)]
end: Option<String>,
/// Seconds or a duration (`1m`) between points. Unset means the
/// store's default.
#[arg(long)]
step: Option<String>,
},
/// List the series that match one or more selectors.
Series {
/// A series selector, such as `{"service.name"="nats"}`. Repeatable.
#[arg(required = true)]
matchers: Vec<String>,
#[command(flatten)]
span: SpanArgs,
},
/// List label names, optionally only those on matching series.
Labels {
/// A series selector to narrow to. Repeatable.
#[arg(long = "match")]
matchers: Vec<String>,
#[command(flatten)]
span: SpanArgs,
},
/// List one label's values; `__name__` lists every metric name.
LabelValues {
/// The label name.
name: String,
/// A series selector to narrow to. Repeatable.
#[arg(long = "match")]
matchers: Vec<String>,
#[command(flatten)]
span: SpanArgs,
},
/// Print this CLI's reference as markdown (source of `docs/tools/swarm-metrics-cli.md`).
#[command(hide = true)]
MarkdownDocs,
}
impl Verb {
/// The store request this verb makes; `None` for the local-only verb.
fn request(self) -> Option<Request> {
Some(match self {
Self::Query { promql, time } => Request::Query { promql, time },
Self::QueryRange {
promql,
start,
end,
step,
} => Request::QueryRange {
promql,
start,
end,
step,
},
Self::Series { matchers, span } => Request::Series {
matchers,
span: span.into(),
},
Self::Labels { matchers, span } => Request::Labels {
matchers,
span: span.into(),
},
Self::LabelValues {
name,
matchers,
span,
} => Request::LabelValues {
name,
matchers,
span: span.into(),
},
Self::MarkdownDocs => return None,
})
}
}
fn main() -> Result<()> {
let Some(request) = Cli::parse().command.request() else {
// `show_footer(false)`: same as swarm-logs, the footer is not our prose.
let options = clap_markdown::MarkdownOptions::new().show_footer(false);
print!("{}", clap_markdown::help_markdown_custom::<Cli>(&options));
return Ok(());
};
// Config read only for a store verb: `markdown-docs` runs in a nix check,
// where no credential exists.
swarm_queue_client::install_crypto_provider();
let cfg = auth::Config::from_env()?;
let stdout = std::io::stdout();
query::run(&cfg, &request, &mut stdout.lock())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory as _;
fn parse(args: &[&str]) -> Option<Request> {
Cli::try_parse_from(std::iter::once("swarm-metrics").chain(args.iter().copied()))
.expect("parses")
.command
.request()
}
#[test]
fn the_command_tree_is_well_formed() {
Cli::command().debug_assert();
}
#[test]
fn a_range_query_without_a_start_is_refused() {
assert!(Cli::try_parse_from(["swarm-metrics", "query-range", "up"]).is_err());
}
#[test]
fn series_needs_a_selector() {
assert!(Cli::try_parse_from(["swarm-metrics", "series"]).is_err());
}
#[test]
fn label_values_maps_name_matchers_and_span() {
assert_eq!(
parse(&[
"label-values",
"__name__",
"--match",
"{service.name=\"nats\"}",
"--start",
"-1d",
"--limit",
"5",
]),
Some(Request::LabelValues {
name: "__name__".into(),
matchers: vec!["{service.name=\"nats\"}".into()],
span: Span {
start: Some("-1d".into()),
end: None,
limit: Some(5),
},
})
);
}
#[test]
fn query_range_maps_its_window() {
assert_eq!(
parse(&["query-range", "up", "--start", "-1h", "--step", "1m"]),
Some(Request::QueryRange {
promql: "up".into(),
start: "-1h".into(),
end: None,
step: Some("1m".into()),
})
);
}
#[test]
fn markdown_docs_makes_no_request() {
assert_eq!(parse(&["markdown-docs"]), None);
}
}