Both blockers are done — #4548 (353 hits in hand-written docs) and #4549 (46 hits in generated CLI reference docs, hivectl/swarmctl/hive-forge's own clap help text) — and the previous commit suppresses the reviewed false-positive remainder. vale --minAlertLevel=error docs now returns 0 errors, matching CI's existing prose-lint-errors job. Closes #4546.
128 lines
4.9 KiB
Rust
128 lines
4.9 KiB
Rust
//! `swarm-logs` — an agent's CLI for the swarm log store.
|
|
//!
|
|
//! An agent can reach VictoriaLogs only through the gateway, and until this
|
|
//! existed the way to read it was to hand-roll a `client_credentials` token
|
|
//! request and a curl by hand, per query. That is the gap this closes: one
|
|
//! verb, a LogsQL string in, matched log lines out, so the answer pipes into
|
|
//! `grep` like any other command's.
|
|
//!
|
|
//! **Read-only and query-only.** It posts to the store's LogsQL query
|
|
//! endpoint and nothing else — there is no ingest path here, and no `tail`.
|
|
//! Streaming is a different endpoint with a different response shape and is
|
|
//! deliberately left for a follow-up rather than folded in.
|
|
//!
|
|
//! ⚠️ **The route applies no scoping.** `swarm-victorialogs.nix` forwards the
|
|
//! query unmodified, so any authenticated caller reads the whole swarm's
|
|
//! logs — every hive's, not just its own. That is the rule in force rather
|
|
//! than an omission on this binary's part; when read permissions exist they
|
|
//! attach at that location, not here.
|
|
//!
|
|
//! Distinct from `hive-metric`, which is the other half of the same
|
|
//! observability surface from an agent's seat: that one *writes* a custom
|
|
//! metric, this one *reads* logs.
|
|
|
|
mod auth;
|
|
mod query;
|
|
|
|
use anyhow::Result;
|
|
use clap::{Parser, Subcommand};
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "swarm-logs",
|
|
version,
|
|
about = "query the swarm's log store from an agent"
|
|
)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Verb,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Verb {
|
|
/// Run a LogsQL query and print the matched log lines.
|
|
Query {
|
|
/// The LogsQL query. A bare word is a full-text search for it.
|
|
///
|
|
/// Quote it: LogsQL uses characters the shell also claims, and an
|
|
/// unquoted `|` pipes this command into the rest of your query
|
|
/// instead of sending it.
|
|
logsql: String,
|
|
|
|
/// Stop after this many records.
|
|
///
|
|
/// Unset means the store's own default. Worth setting on a broad
|
|
/// query — nothing between here and the store narrows one, so a bare
|
|
/// word matches across every hive in the swarm.
|
|
#[arg(long)]
|
|
limit: Option<u64>,
|
|
|
|
/// What to print for each matched record.
|
|
#[arg(long, value_enum, default_value = "message")]
|
|
format: query::Format,
|
|
},
|
|
|
|
/// Print this CLI's reference as markdown (source of `docs/tools/swarm-logs-cli.md`).
|
|
///
|
|
/// No `completions` sibling, unlike `hivectl` and `swarmctl`: those are
|
|
/// installed on an operator's interactive host, where a completion script
|
|
/// has a shell to be sourced into. This one is run by an agent through a
|
|
/// tool call, which has no line editor to complete against — the verb
|
|
/// would exist only to be listed.
|
|
#[command(hide = true)]
|
|
MarkdownDocs,
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
match Cli::parse().command {
|
|
Verb::Query {
|
|
logsql,
|
|
limit,
|
|
format,
|
|
} => {
|
|
// Config resolved inside this arm and not up front: the two
|
|
// verbs below are pure functions of the command tree and must
|
|
// keep working on a machine that has no log store wired up at
|
|
// all — `markdown-docs` in particular runs in a nix check, where
|
|
// no credential exists and none should.
|
|
let cfg = auth::Config::from_env()?;
|
|
let stdout = std::io::stdout();
|
|
query::run(&cfg, &logsql, limit, format, &mut stdout.lock())
|
|
}
|
|
Verb::MarkdownDocs => {
|
|
// `show_footer(false)`: drop clap-markdown's own fixed
|
|
// "generated automatically by..." footer, same as
|
|
// hivectl/swarmctl/hive-forge — it's not our prose, and
|
|
// write-good.Passive has nothing to check it against.
|
|
let options = clap_markdown::MarkdownOptions::new().show_footer(false);
|
|
print!("{}", clap_markdown::help_markdown_custom::<Cli>(&options));
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use clap::CommandFactory as _;
|
|
|
|
/// clap's own consistency check over the command tree — catches a
|
|
/// duplicate long flag or a malformed `about` at test time rather than on
|
|
/// the first run of a verb nobody exercised.
|
|
#[test]
|
|
fn the_command_tree_is_well_formed() {
|
|
Cli::command().debug_assert();
|
|
}
|
|
|
|
/// The default matters: it is what an agent gets when it pipes into
|
|
/// `grep` without reading the flags first.
|
|
#[test]
|
|
fn query_defaults_to_projecting_the_message() {
|
|
let cli = Cli::try_parse_from(["swarm-logs", "query", "a-marker"]).expect("parses");
|
|
let Verb::Query { format, limit, .. } = cli.command else {
|
|
panic!("expected the query verb");
|
|
};
|
|
assert_eq!(format, query::Format::Message);
|
|
assert_eq!(limit, None, "no client-side default limit is imposed");
|
|
}
|
|
}
|