Skip to content

feat: make metrics and logs top-level verbs - #234

Merged
outofcoffee merged 4 commits into
mainfrom
top-level-metrics-and-logs
Sep 19, 2026
Merged

outofcoffee merged 4 commits into
mainfrom
top-level-metrics-and-logs

Conversation

@outofcoffee

Copy link
Copy Markdown
Collaborator

The last of the read verbs, and the last five duplicate command spellings. After this, no verb is spelled twice. Completes #155 and #156.

In progress — capabilities landed, verbs next.

Summary

  • metrics and logs become top-level commands, taking their target the way status and dashboard do: --env <name>, --fleet <path>, or the working directory's fleet.yaml.
  • BREAKING fleet metrics, fleet logs, remote status, remote metrics and remote logs are removed; each fails naming its replacement.
  • Three optional node capabilities carry the cloud-only facts: Coster (--cost), SourceLogger (--source/--since/--instance), Versioner.
  • A Spinloop given to a read verb is read for its ENV instructions only, never to select a target — the rule the remote subcommands already follow.

Implementation details

Why these two were held back. status and dashboard moved in #227 because they needed nothing new. These carry facts with no daemon counterpart, so the node contract has to say what a kind can and cannot answer.

remote status is in this change, not #227. It needs the Spinloop-ENV path and a version a fan-out does not produce — and remote metrics needs both too, so solving it once for all three beats twice.

The rule when a flag meets a target that cannot answer it (design D2): the flag applies to the nodes that can, and leaves the rest as they read without it. metrics --cost over a fleet of daemons shows no cost and succeeds. This is what these views already do for every fact only one kind reports — the readiness mark, the version — and a flag is not a different case. The accepted cost is that a blank column does not explain itself, so the requirement obliges each verb's page to document which flags apply to which kinds.

Three findings that shaped it:

  • statsFromRemote drops four fields the reply already carries — Version, InstanceType, InstanceID, Environment. The first two are exactly what cost and version need. Metrics now retains them on the node, so Version() costs nothing and Cost() pays only for the price lookup. That is why metrics can carry the version while status could not: on status it lives in a different endpoint.
  • metrics.Stats gains no field. Widening the shared reply with a cloud-only InstanceType and pricing it in the renderer would put a network call in a formatter and a blank field on every daemon. The node answers what did this cost, not what type are you.
  • The price lookup was already a network call on the remote metrics path; Coster moves it behind the node rather than adding one, still behind --cost.

@outofcoffee
outofcoffee force-pushed the top-level-metrics-and-logs branch 2 times, most recently from 657d5d8 to b535ade Compare September 19, 2026 19:38
@outofcoffee
outofcoffee marked this pull request as ready for review September 19, 2026 19:38
spinloop-agent added 4 commits September 19, 2026 22:02
metrics --cost and logs --source/--since/--instance report facts only a cloud
environment has, and the shared stats shape has no room for them. Three
optional capabilities join ProgressStarter and Keeper on fleet.Node: Coster
prices a running session, SourceLogger queries a log store, and Versioner
reports the release a node named with its last metrics reading.

A kind with no answer does not implement one, so a caller reaches them by
assertion and carries no branch on kind. remoteNode implements all three;
daemonNode none.

Metrics retains the instance type, uptime and version the stats reply carries
and statsFromRemote drops, so Cost pays only for the price lookup and Version
costs nothing — the reply that carried them is already paid for. metrics.Stats
gains no field: a cloud-only fact on the shape every node answers with would
leave a column every daemon reports empty.
Both take their target the way status and dashboard do — an environment, a
fleet file, or the working directory's fleet.yaml — and fleet metrics and
fleet logs are gone.

The cloud-only flags reach their capability through the fan-out rather than
through a caller that knows what kind a node is. PricedMetricsCall asks a node
that implements Coster and leaves the rest as MetricsCall left them;
QueriedLogsCall asks a node that implements SourceLogger and reads the rest by
offset, so --source against a fleet of daemons returns their output rather
than nothing. NodeResult carries what came back, so the renderers read one
value per node instead of holding the nodes.

MetricsCall now also carries the version, which fleet metrics never showed and
remote metrics did — free, because the node answers it from the reading just
taken.
…, close remaining test gaps

runFleetMetricsWatch never returned when a single-node target (an --env
watch) failed to read, unlike the old remote metrics --watch which exited
on a fetch error; two tests calling it synchronously with no way to
interrupt hung the whole test binary until the timeout killed it. It now
ends the watch when the one node it is asked about could not be read,
while a multi-node fleet still keeps drawing through a bad node.

Also:
- export internal/fleet's CloudWatch log fetch as FetchLogsFn so both its
  own tests and cmd/spinloop's can substitute it, closing the coverage gap
  on PricedMetricsCall, QueriedLogsCall and remoteNode.LogsMatching
- delete cmd/spinloop/remote_logs.go: dead code left behind when logs
  moved to the top-level verb, its tests already superseded by
  fleet_logs.go's; rebuild its still-needed coverage (--source/--since/
  --instance threading, the log-group/runner mapping) against the new path
- restore --since's old 1h default on `spinloop logs`, dropped when the
  flag moved from `remote logs`
- update stale test assertions (old key-value text, single-object JSON)
  left over from the metrics/logs/status merge
- add a test for the six-command movedSubcommands signpost map, previously
  only exercised for "fleet harness"
Merges the change's five delta specs (fleet-client, remote-endpoint,
remote-logs, remote-stats, remote-version-reporting) into the main specs
and moves the change under openspec/changes/archive/. All 17 tasks were
complete and the specs validate clean.

While merging, corrected a few leftover authoring mistakes in the delta
files themselves: a duplicated requirement title, a malformed example
with --env given twice on one command line, two scenarios describing a
default-environment fallback that a prior change already removed, and a
scenario claiming `spinloop remote metrics` still displays figures where
the same requirement's own next scenario says it fails with a signpost.
@outofcoffee
outofcoffee force-pushed the top-level-metrics-and-logs branch from 0593b83 to 74c7c88 Compare September 19, 2026 21:05
@outofcoffee
outofcoffee merged commit 192982d into main Sep 19, 2026
5 checks passed
@outofcoffee
outofcoffee deleted the top-level-metrics-and-logs branch September 19, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant