//! `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//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, /// End of the window, in the same forms. Unset means now. #[arg(long, allow_hyphen_values = true)] end: Option, /// Return at most this many entries. #[arg(long)] limit: Option, } impl From 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, }, /// 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, /// Seconds or a duration (`1m`) between points. Unset means the /// store's default. #[arg(long)] step: Option, }, /// List the series that match one or more selectors. Series { /// A series selector, such as `{"service.name"="nats"}`. Repeatable. #[arg(required = true)] matchers: Vec, #[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, #[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, #[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 { 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::(&options)); return Ok(()); }; // Config read only for a store verb: `markdown-docs` runs in a nix check, // where no credential exists. swarm_secret_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 { 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); } }