swarm-logs
An agent's CLI for the swarm log store. One verb — swarm-logs query '<LogsQL>' — so reading logs pipes and greps like any other command
instead of being a hand-rolled token request plus a curl, per query.
Read-only and query-only. There is no ingest path here (that is the
collector's), and no tail: streaming is a different LogsQL endpoint with a
different response shape, left for a follow-up rather than folded in.
Distinct from hive-metric, the other half of the same surface from an
agent's seat: that one writes a custom metric, this one reads logs.
⚠️ There is no scoping
The gateway forwards the query unmodified, so any authenticated caller reads the whole swarm's logs — every hive's, not only its own. That is the rule in force rather than an omission here; when read permissions exist they attach at the gateway location, not in this binary.
How it authenticates
The agent container already holds one identity, the per-hive machine client
hive-<name>-agent, delivered as a systemd credential pair. This binary
presents a client_credentials access token minted from it as
Authorization: Bearer.
Minting is not implemented here — swarm_queue_client::mint_token_for_blocking
already owns the request, its error type and its CA handling, and a
token-endpoint fix has to be findable in one place. What this crate adds is
the agent-shaped half: the client id arrives as a file beside the secret, so
nothing outside nix/agent-modules/queue.nix spells hive-<name>-agent
twice. That is the same problem hive-agent/src/swarm_queue.rs solves, and
src/auth.rs is its decide restated over this binary's inputs.
🩸 Every credential here is a path. The secret's contents are read at the
moment of the request, inside swarm-queue-client, and are never bound to a
name in this crate, logged, or rendered into an error.
The audience is the URL
swarm-logs mints its token with the query URL as the requested audience,
because that is what authelia checks at the authz endpoint the gateway's
auth_request calls. The same rule swarm-otel.nix states over its own push
targets: one binding for the address and the audience, since two spellings of
one address present as a valid token refused at the store.
⚠️ That requires the agent client to be registered for that audience and to
hold authelia.bearer.authz. Both are set in swarm-authelia.nix's
agentClients; without them authelia answers invalid_target at the token
endpoint, or the gateway answers 401 with no explanation.
⚠️ Registration is not issuance, so the token request asks for both: the
audience and the scope are named in the client_credentials form, because a
client that is registered for a scope it does not request is handed a token
carrying none, and the gateway refuses that with the same bare 401.
Configuration
Supplied by nix/agent-modules/logs.nix, which wraps the binary — the same
shape swarmctl is configured in, and for the same reason: every value is
derived from an option that module owns, so a default here would be an address
we hope points at something.
| variable | what |
|---|---|
HIVE_AGENT_LOGS_QUERY_URL |
the LogsQL query endpoint; also the token's audience |
HIVE_AGENT_OIDC_TOKEN_ENDPOINT |
authelia's token endpoint |
HIVE_AGENT_OIDC_CLIENT_ID_FILE |
path of the delivered client-id credential |
HIVE_AGENT_OIDC_CLIENT_SECRET_FILE |
path of the delivered secret credential |
HIVE_AGENT_OIDC_CA_FILE |
optional extra trust anchor; unset in this tree |
HIVE_AGENT, the same prefix the harness reads, because it is the same
identity — an agent authenticates as its hive's agent client whether the
caller is the harness or a CLI the agent typed. The first four are all-or-none:
a half-set environment is a deployment bug, and this binary says so by name
rather than behaving like an unconfigured one.
Output
$ swarm-logs query 'atlas-otel-probe' --limit 5
One log message per line by default (the record's _msg), which is what a
grep pattern is written against. --format json passes the store's NDJSON
through unmodified, for _time, _stream and jq.
A record the projection cannot understand — no string _msg, or not JSON at
all — is printed whole rather than dropped. Silently losing a row would make
an incomplete answer indistinguishable from a complete one.
A non-200 is reported with its body and a non-zero exit, never as an empty result. That is the whole reason the gateway grew a separate machine route: the operator's browser route answers an unauthenticated caller with authelia's login page as a 200 with an HTML body, and a client that reads only the status code records a query that succeeded and matched nothing.