The rustls provider install is process setup every swarm binary runs, and swarm-secret-client depends on no other swarm crate, so a binary that only needs the provider and the store no longer links the queue client. - swarm-secret-client/src/lib.rs, Cargo.toml: the function, its test, a `//!` line on why it lives there, and the rustls dependency; rustls `ring` as a dev-dependency feature, so the test builds both providers as the binaries do and fails if the install is dropped. - swarm-queue-client/src/lib.rs, Cargo.toml: function and test removed; `connect()` calls the secret client's; rustls dropped (no other use), swarm-secret-client added. - swarm-metrics-auth/Cargo.toml: drop swarm-queue-client, whose only use was the provider; the binary no longer links async-nats. - swarm-logs-auth, swarm-logs, swarm-metrics, swarm-ci-metrics Cargo.toml: the dep comment no longer names the provider (they keep the dep for agent_token / the OIDC mint). - Cargo.toml: the rustls comment names the provider's new home. - 14 call sites (hive-agent, hive-c0re, hive-subagent-mcp x2, swarm-builder, swarm-ci-metrics, swarm-claude-login, swarm-controller, swarm-logs-auth, swarm-logs, swarm-metrics-auth, swarm-metrics, swarm-nats-auth, swarm-review-classifier): one-line path change. - Cargo.lock: the dependency edges above.
247 lines
7.3 KiB
Rust
247 lines
7.3 KiB
Rust
//! `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_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<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);
|
|
}
|
|
}
|