carlos / platform Public

carlos status --instances: who is using what right now

Discussion #11: "which account is using what, right now?" had no CLI or
API answer at all. Every door holding it gated on a browser session —
GET /fleet/metrics and GET /fleet/economics/accounts both check
s.session then IsOperator, so a CLI bearer token is not a session and
gets a bare 404, correctly, leaving no token-shaped way in. `carlos
economics backhaul` lists only accounts that backhauled something.
`carlos status --live` reports boxes, not tenants. `carlos instances`
reads /data/registry.db on the box and lists routes rather than what
they are consuming.

So the last time somebody asked whether one member was hogging the
flagship, the answer came out of the bucket with `aws s3 cp` over four
boxes' metrics objects and nine accounts' usage objects — which
AGENTS.md's "operate via API and CLI" rule names as a product gap rather
than a path, and which is how an agent ends up holding AWS credentials
for a question that should have cost one command.

    carlos status --instances
    carlos status --instances --account bac

Nothing new is measured or stored. The metrics loop already writes
per-unit cpu and mem every 60s and LoadMetrics already sweeps them for
the browser page; the account each unit belongs to is a registry join
the console can already do. All that was missing was the door.

- GET /api/cli/status/instances, registered through cliRoute with an
  EMPTY role set — shut to every service credential, because the answer
  names every account's instances on every box and no app-scoped machine
  token is the right holder of that. The operator check is in the
  handler, since cliRoute's Set only narrows SERVICE principals and a
  person always passes it: cliUser -> IsOperator -> byte-identical 404,
  the shape handleAPIEconomicsBackhaul and handleAPIEmailPause use.
- unitOwner's matching rule is split out as routeForUnit rather than
  reimplemented. This listing needs the route's HOST and not only its
  owner, and a second private notion of which route a cgroup belongs to
  is exactly how a listing ends up disagreeing with the economics pass
  about whose it is.
- A missing registry is a 503 naming the reason, not a 404: the caller
  IS an operator and the route DOES exist, and unattributed rows would
  be worse than a refusal when the whole question is whose they are.

Two things the answer deliberately says out loud. A unit matching no
route is labelled `(platform)` — the console's own unit, the agent,
system units — rather than dropped, because an operator asking what is
on the box wants the box's own overhead in the picture. And a
hibernating instance has no process, so no cgroup and no reading: it is
not in the list, its absence is not evidence, and the command prints
that under every answer rather than letting a short list read as the
whole tenancy.

Not included: how long an instance has been resident. Nothing publishes
a start time — not UnitSample, not BoxMetrics, not the route — so the
only available answer would be "first seen in the 48h sweep", which
under-reports and resets on any gap. A column that lies quietly is worse
than a column that is missing; a real one needs a field the box does not
yet sample.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVvK41AUKXndpcy5KkUZRS
Paul Campbell pushed by paul@keymail.dev b3ff9af63ab0269f45dca780da37533e4930883d parent 8635d54
8 files changed, +431 −5
  • cli.go +24 −0
  • docs/site/cli.md +15 −0
  • internal/console/apistatusinstances.go +136 −0
  • internal/console/apistatusinstances_test.go +72 −0
  • internal/console/authcli.go +7 −0
  • internal/console/economicspass.go +16 −3
  • internal/console/svcroutes_test.go +11 −2
  • status_instances.go +150 −0
diff --git a/cli.go b/cli.go
index 0652d5ac7..a9d9aebbe 100644
--- a/cli.go
+++ b/cli.go
@@ -3021,6 +3021,7 @@ func runKeygen(args []string) {
// "carlos status publish" here; --live only reads what those ticks wrote.
const statusUsage = `usage: carlos status --dry-run <dir>
carlos status --live
+ carlos status --instances [--account <sqid>]
--dry-run <dir> build a throwaway fleet stuffed with fixture secrets
(hostnames, a machine id, mid-band fractions), run it
@@ -3039,6 +3040,14 @@ const statusUsage = `usage: carlos status --dry-run <dir>
last readings. Needs bucket credentials and
CARLOS_STATUS_BUCKET (or CARLOS_DEPLOYMENT_BUCKET, the same
defaulting the agent's own status loop applies).
+
+ --instances who is using what right now: one row per AWAKE instance
+ across the fleet, with the account and app it belongs to
+ and what it is consuming. Signed in through the console
+ (carlos auth login), operator only — a non-operator token
+ gets the same bare 404 an unknown route gets. --account
+ <sqid> narrows it to one account. A sleeping instance has
+ no process and therefore no reading, so it does not appear.
`
// RunStatus is the "carlos status" verb: a fixture dry-run and a live read of
@@ -3049,9 +3058,24 @@ func RunStatus(args []string) {
fs := flag.NewFlagSet("status", flag.ExitOnError)
dryRunDir := fs.String("dry-run", "", "build a fixture fleet, publish it into this directory, and verify no leaks")
liveFlag := fs.Bool("live", false, "read status/boxes/*.json from the status bucket and print per-host metrics")
+ instancesFlag := fs.Bool("instances", false, "one row per awake instance across the fleet, with the account and app it belongs to (operator only, through the console)")
+ acctFilter := fs.String("account", "", "narrow --instances to one account (sqid)")
+ consoleFlag := fs.String("console", "", "console to ask (default: the only logged-in one)")
fs.Usage = flagUsage(fs, statusUsage)
fs.Parse(args)
+ if *instancesFlag {
+ // Console-backed, unlike --live: the per-unit readings live in the
+ // deployment's metrics objects, which the console already sweeps, and
+ // the account each one belongs to is a registry join only the console
+ // can do. --live reads the status bucket directly with the operator's
+ // own bucket credentials; this deliberately needs none, which is the
+ // whole point (discussion #11: the alternative was `aws s3 cp`).
+ if err := statusInstances(os.Stdout, *consoleFlag, *acctFilter); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
if *liveFlag {
if err := statusLive(os.Stdout); err != nil {
log.Fatal(err)
diff --git a/docs/site/cli.md b/docs/site/cli.md
index 7c2997622..2854f1bb0 100644
--- a/docs/site/cli.md
+++ b/docs/site/cli.md
@@ -818,6 +818,21 @@ its stale readings.
carlos status --live
```
+`--instances` answers the other question an operator has about a fleet: who is
+using what, right now. One row per awake instance across every box, with the
+account and app it belongs to and what it is consuming.
+
+```sh
+carlos status --instances --account bac
+```
+
+It goes through the console rather than the status bucket, so it needs no AWS
+credentials — which is the point of it existing. Operator only: a token
+without the fleet-operator bit gets the same bare 404 an unknown route gets,
+because the answer names other people's accounts. A sleeping instance has no
+process and so no reading; it does not appear, and the command says so rather
+than letting a short list read as the whole tenancy.
+
`--dry-run <dir>` is the self-hoster's leak check. It builds a throwaway fleet
stuffed with fixture secrets, runs it through the real publish path into a
scratch directory, reads every published object back, and fails if any
diff --git a/internal/console/apistatusinstances.go b/internal/console/apistatusinstances.go
new file mode 100644
index 000000000..550d2fe66
--- /dev/null
+++ b/internal/console/apistatusinstances.go
@@ -0,0 +1,136 @@
+// The live-tenancy read (`carlos status --instances`, discussion #11): which
+// account is running what, right now, across the fleet.
+//
+// The question had no CLI or API answer at all. `GET /fleet/metrics` and
+// `GET /fleet/economics/accounts` hold it, and both gate on s.session — a
+// browser cookie — so a terminal cannot reach them however operator it is.
+// `carlos economics backhaul` lists only accounts that backhauled something.
+// `carlos status --live` reports boxes, not tenants. `carlos instances` reads
+// /data/registry.db on the box and lists routes rather than what they are
+// consuming.
+//
+// So the last time somebody asked "is this member hogging the flagship?", the
+// answer came out of the bucket with `aws s3 cp` over four boxes' metrics and
+// nine accounts' usage objects — which AGENTS.md's "operate via API and CLI"
+// rule names as a product gap rather than a path. This is the door that
+// closes it. Nothing new is measured or stored: the metrics loop already
+// writes per-unit cpu and mem every 60s, and LoadMetrics already sweeps them
+// for the browser page.
+//
+// GATE: cliUser + IsOperator, then a byte-identical 404 — the same shape
+// handleAPIEconomicsBackhaul and handleAPIEmailPause use. A non-operator must
+// not be able to tell this route from one that does not exist, because the
+// answer names other people's accounts. Registered through cliRoute with an
+// EMPTY role set, which closes it to every service credential outright: this
+// is an operator's view of the whole box, and no app-scoped machine token has
+// any business holding it.
+//
+// SCOPE: the console reads the metrics its own deployment publishes, which is
+// fleet-wide (every box writes metrics/boxes/<box>/), so unlike `carlos
+// routes` this answer is not box-local. What it CANNOT see is an instance
+// that is asleep — a hibernating instance has no cgroup and therefore no
+// sample. That is the right shape for the question ("who is using what RIGHT
+// NOW") and the CLI says so rather than letting a short list read as the
+// whole tenancy.
+package console
+
+import (
+ "log"
+ "net/http"
+ "sort"
+)
+
+// apiStatusInstance is one awake instance. Account and app are joined on from
+// the registry through routeForUnit — the same matching rule the economics
+// pass attributes cost with, deliberately shared rather than reimplemented.
+type apiStatusInstance struct {
+ Box string `json:"box"`
+ Unit string `json:"unit"`
+ Host string `json:"host,omitempty"`
+ Account string `json:"account"`
+ App string `json:"app,omitempty"`
+ // CPU is a fraction of one core (0.017 = 1.7% of a core), Mem is bytes.
+ // Both are pointers because a sample can legitimately lack a reading —
+ // a unit seen for the first time has no CPU delta yet — and a missing
+ // reading must print as "no answer" rather than as zero, which is a
+ // number an operator would act on.
+ CPU *float64 `json:"cpu,omitempty"`
+ Mem *int64 `json:"mem_bytes,omitempty"`
+ // Platform marks a unit that matched no route: the console's own unit,
+ // system units, the agent itself. They are charged to the ops account by
+ // the economics pass and they are shown here for the same reason — an
+ // operator asking what is on the box wants the box's own overhead in the
+ // picture, clearly labelled rather than silently dropped.
+ Platform bool `json:"platform,omitempty"`
+}
+
+func (s *Server) handleAPIStatusInstances(w http.ResponseWriter, r *http.Request) {
+ addr, ok := s.cliUser(w, r)
+ if !ok {
+ return
+ }
+ isOp, err := s.store.IsOperator(addr)
+ if err != nil {
+ log.Printf("console: api status instances: is-operator: %v", err)
+ http.NotFound(w, r)
+ return
+ }
+ if !isOp {
+ http.NotFound(w, r)
+ return
+ }
+ reg, err := s.openPassRegistry()
+ if err != nil {
+ // Not a 404: the caller IS an operator and the route DOES exist, so
+ // the honest answer is that this console cannot join the readings to
+ // their owners. Answering with unattributed rows would be worse than
+ // refusing — the whole question is whose they are.
+ log.Printf("console: api status instances: registry: %v", err)
+ jsonError(w, http.StatusServiceUnavailable,
+ "this console serves no route registry, so it cannot say which account an instance belongs to")
+ return
+ }
+ defer reg.Close()
+ routes, err := reg.List()
+ if err != nil {
+ log.Printf("console: api status instances: registry list: %v", err)
+ jsonError(w, http.StatusBadGateway, "could not read the route registry — try again")
+ return
+ }
+ boxes, _, err := s.LoadMetrics(r.Context())
+ if err != nil {
+ log.Printf("console: api status instances: metrics: %v", err)
+ jsonError(w, http.StatusBadGateway, "could not read the fleet's metrics — try again")
+ return
+ }
+
+ want := r.URL.Query().Get("account")
+ rows := []apiStatusInstance{}
+ for _, b := range boxes {
+ for _, u := range b.Units {
+ row := apiStatusInstance{Box: b.Box, Unit: u.Unit, CPU: u.CPUFrac, Mem: u.MemBytes}
+ if rt, ok := routeForUnit(routes, u.Unit); ok {
+ row.Host, row.App = rt.Host, rt.App
+ } else {
+ row.Platform = true
+ }
+ row.Account, _, _ = unitOwner(routes, u.Unit)
+ if want != "" && row.Account != want {
+ continue
+ }
+ rows = append(rows, row)
+ }
+ }
+ // Sorted by box then host then unit: the sweep's order is not stable
+ // between calls, and a listing that reshuffles is one nobody can diff.
+ sort.Slice(rows, func(i, j int) bool {
+ if rows[i].Box != rows[j].Box {
+ return rows[i].Box < rows[j].Box
+ }
+ if rows[i].Host != rows[j].Host {
+ return rows[i].Host < rows[j].Host
+ }
+ return rows[i].Unit < rows[j].Unit
+ })
+ writeJSON(w, http.StatusOK, map[string]any{"instances": rows})
+}
diff --git a/internal/console/apistatusinstances_test.go b/internal/console/apistatusinstances_test.go
new file mode 100644
index 000000000..9d18a856f
--- /dev/null
+++ b/internal/console/apistatusinstances_test.go
@@ -0,0 +1,72 @@
+package console
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+)
+
+// TestAPIStatusInstancesGate: operator only, with the same posture every
+// operator door here has — anyone else sees the byte-identical 404 an unknown
+// route gets. It matters more than usual on this door: the answer names other
+// people's accounts and what they are running, so "you are not an operator"
+// must not be distinguishable from "there is no such route".
+func TestAPIStatusInstancesGate(t *testing.T) {
+ fx := newConsoleFixture(t)
+ token := cliToken(t, fx, "paul@keymail.dev", "code-apistatusinstances")
+ ghostStatus, ghostBody := getBearer(t, fx.ts.URL+"/this-api-route-does-not-exist", token)
+
+ if status, _ := getBearer(t, fx.ts.URL+"/api/cli/status/instances", ""); status != http.StatusUnauthorized {
+ t.Fatalf("unauthenticated = %d; want the CLI door's 401", status)
+ }
+ if status, body := getBearer(t, fx.ts.URL+"/api/cli/status/instances", token); status != ghostStatus || string(body) != string(ghostBody) {
+ t.Fatalf("non-operator = %d %q; want the ghost 404 (%d %q)", status, body, ghostStatus, ghostBody)
+ }
+
+ if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
+ t.Fatal(err)
+ }
+ status, body := getBearer(t, fx.ts.URL+"/api/cli/status/instances", token)
+ if status != http.StatusOK {
+ t.Fatalf("operator = %d %q", status, body)
+ }
+ var out struct {
+ Instances []apiStatusInstance `json:"instances"`
+ }
+ if err := json.Unmarshal(body, &out); err != nil {
+ t.Fatalf("unreadable: %v: %s", err, body)
+ }
+ // Never null: a client that decodes null gets a nil slice and prints
+ // nothing, which reads the same as "no instances" for the wrong reason.
+ if out.Instances == nil {
+ t.Error("instances decoded as null; want an empty array")
+ }
+}
+
+// TestAPIStatusInstancesAccountFilterNarrows: --account is the "is THIS member
+// hogging the box?" form of the question, which is the one that prompted the
+// door (discussion #11). An unknown account narrows to nothing rather than
+// falling back to everything — a filter that silently ignores itself is how
+// an operator reads another account's rows as the one they asked about.
+func TestAPIStatusInstancesAccountFilterNarrows(t *testing.T) {
+ fx := newConsoleFixture(t)
+ token := cliToken(t, fx, "paul@keymail.dev", "code-apistatusfilter")
+ if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
+ t.Fatal(err)
+ }
+ status, body := getBearer(t, fx.ts.URL+"/api/cli/status/instances?account=nosuchacct", token)
+ if status != http.StatusOK {
+ t.Fatalf("operator with filter = %d %q", status, body)
+ }
+ var out struct {
+ Instances []apiStatusInstance `json:"instances"`
+ }
+ if err := json.Unmarshal(body, &out); err != nil {
+ t.Fatalf("unreadable: %v: %s", err, body)
+ }
+ for _, r := range out.Instances {
+ if r.Account != "nosuchacct" {
+ t.Errorf("row for account %q survived a filter for %q", r.Account, "nosuchacct")
+ }
+ }
+}
diff --git a/internal/console/authcli.go b/internal/console/authcli.go
index 2d1243302..9875f2b64 100644
--- a/internal/console/authcli.go
+++ b/internal/console/authcli.go
@@ -469,6 +469,13 @@ func (s *Server) registerCLIRoutes() {
// (/api/cli/) from the identity routes above (/auth/cli/), which must not
// move now that CLIs depend on them.
s.cliRoute("GET /api/cli/apps", svcrole.Allow(svcrole.Publish, svcrole.Instance, svcrole.Operate), s.handleAPIApps)
+ // The fleet-wide live tenancy read (discussion #11). An EMPTY role set:
+ // no service credential of any role reaches it, because the answer names
+ // every account's instances on every box, and that is an operator's view
+ // rather than any workload's. The operator check itself is inside the
+ // handler, since cliRoute's role set can only narrow SERVICE principals
+ // and a person always passes through it.
+ s.cliRoute("GET /api/cli/status/instances", svcrole.Set{}, s.handleAPIStatusInstances)
// The literal `new` mirrors the browser door (/a/{sqid}/apps/new); no
// bare POST .../{app} route exists to collide with, and the mux prefers
// the literal if one ever does.
diff --git a/internal/console/economicspass.go b/internal/console/economicspass.go
index 08df9644b..e42932060 100644
--- a/internal/console/economicspass.go
+++ b/internal/console/economicspass.go
@@ -209,15 +209,28 @@ func (s *Server) storagePass(ctx context.Context, date string) error {
// own unit, system units — attributes to release.OpsAccount, app "": platform
// overhead.
func unitOwner(routes []registry.Route, unit string) (acct, app string, ok bool) {
+ rt, ok := routeForUnit(routes, unit)
+ if !ok {
+ return release.OpsAccount, "", false
+ }
+ return release.RouteAccount(rt), rt.App, true
+}
+
+// routeForUnit is the matching rule itself, split out of unitOwner so that a
+// second reader of the same sampled units — `carlos status --instances`,
+// which needs the route's HOST and not only its owner — cannot end up with a
+// private second notion of which route a cgroup belongs to. The comment above
+// unitOwner is the rationale for the rule; this is the rule.
+func routeForUnit(routes []registry.Route, unit string) (registry.Route, bool) {
for _, rt := range routes {
if rt.Unit != "" && rt.Unit == unit {
- return release.RouteAccount(rt), rt.App, true
+ return rt, true
}
if rt.Host != "" && (unit == activator.UnitName(rt.Host) || unit == activator.ExecName(rt.Host)) {
- return release.RouteAccount(rt), rt.App, true
+ return rt, true
}
}
- return release.OpsAccount, "", false
+ return registry.Route{}, false
}
// usageSweep is sweepUsage's result: the box- and account-shaped meters
diff --git a/internal/console/svcroutes_test.go b/internal/console/svcroutes_test.go
index 0f3d30e82..d738e5573 100644
--- a/internal/console/svcroutes_test.go
+++ b/internal/console/svcroutes_test.go
@@ -92,6 +92,15 @@ var closedDoors = []string{
// the registration says so where the role table can be read.
"GET /api/cli/apps/{account}/{app}/routes",
"PUT /api/cli/apps/{account}/{app}/instances-manifest",
+ // The fleet-wide live tenancy read (discussion #11). Shut to every
+ // credential for a reason that is about the door's SUBJECT rather than
+ // any role: the answer names every account's instances on every box.
+ // There is no app-scoped reading of it — narrowing it to the caller's
+ // own account would be a different door answering a different question —
+ // so no service role can be the right holder, whatever roles exist
+ // later. The operator check itself is in the handler, because cliRoute's
+ // Set only narrows service principals and a person always passes it.
+ "GET /api/cli/status/instances",
// Enrolling a BOX into a fleet, and rotating or detaching one, is a
// human act with a machine's credentials as its output. Same cut.
"POST /api/cli/fleets/{account}/new",
@@ -693,8 +702,8 @@ func TestEveryClosedDoorRefusesAServiceCredential(t *testing.T) {
}
seen++
}
- if seen != len(closedDoors) || seen != 18 {
- t.Errorf("walked %d closed doors, want all %d (and 18 today: instance-token, the routes door, instances-manifest, the route-channel move, six fleets, four services, two email credentials, email pause, the build probe)",
+ if seen != len(closedDoors) || seen != 19 {
+ t.Errorf("walked %d closed doors, want all %d (and 19 today: instance-token, the routes door, instances-manifest, the route-channel move, six fleets, four services, two email credentials, email pause, the build probe, the live tenancy read)",
seen, len(closedDoors))
}
}
diff --git a/status_instances.go b/status_instances.go
new file mode 100644
index 000000000..e7f269979
--- /dev/null
+++ b/status_instances.go
@@ -0,0 +1,150 @@
+package main
+
+// `carlos status --instances` — who is using what right now (discussion #11).
+//
+// The question "which account is using what on this box?" had no terminal
+// answer. Every door holding it gated on a browser session, and the last
+// person who needed it read four boxes' metrics objects and nine accounts'
+// usage objects out of the bucket with `aws s3 cp` — which AGENTS.md's
+// "operate via API and CLI" rule names as a product gap rather than a path.
+//
+// Console-backed, unlike `--live`. `--live` reads the status bucket with the
+// operator's own bucket credentials, which is exactly the move this verb
+// exists to make unnecessary: the console already sweeps the per-unit
+// readings for its own browser page, and the account each unit belongs to is
+// a registry join only the console can do.
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+)
+
+// statusInstanceRow is one awake instance, as the console answers it
+// (internal/console/apistatusinstances.go). Declared here rather than
+// imported, the convention every other CLI-side wire struct follows.
+//
+// CPU and Mem are pointers because a missing reading is a real state — a unit
+// seen for the first time has no CPU delta yet — and printing it as 0 would
+// be printing a number an operator might act on.
+type statusInstanceRow struct {
+ Box string `json:"box"`
+ Unit string `json:"unit"`
+ Host string `json:"host"`
+ Account string `json:"account"`
+ App string `json:"app"`
+ CPU *float64 `json:"cpu"`
+ Mem *int64 `json:"mem_bytes"`
+ Platform bool `json:"platform"`
+}
+
+func (s *apiSession) statusInstanceList(ctx context.Context, account string) ([]statusInstanceRow, error) {
+ path := "/api/cli/status/instances"
+ if account != "" {
+ path += "?account=" + url.QueryEscape(account)
+ }
+ status, body, err := s.getJSON(ctx, path)
+ if err != nil {
+ return nil, err
+ }
+ if status != http.StatusOK {
+ if status == http.StatusNotFound {
+ // The same bare 404 an unknown route gives, and deliberately so:
+ // the answer names other people's accounts, so a non-operator
+ // must not be able to tell this door from one that is not there.
+ // Which means this message has to cover both readings.
+ return nil, errors.New("not found — this needs the fleet-operator bit on your account, " +
+ "or this console predates `carlos status --instances` (deploy a console built from current main)")
+ }
+ return nil, apiErrorFromBody(status, body)
+ }
+ var out struct {
+ Instances []statusInstanceRow `json:"instances"`
+ }
+ if err := json.Unmarshal(body, &out); err != nil {
+ return nil, fmt.Errorf("status instances: unreadable response: %w", err)
+ }
+ return out.Instances, nil
+}
+
+func statusInstances(w io.Writer, consoleFlag, account string) error {
+ sess, err := resolveMode(consoleFlag)
+ if err != nil {
+ return err
+ }
+ if sess == nil {
+ return errors.New("carlos status --instances works through a console, and this shell is in direct bucket mode\n" +
+ "(CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR is set). Unset the store env and run:\n" +
+ "carlos auth login --console <url>")
+ }
+ rows, err := sess.statusInstanceList(context.Background(), account)
+ if err != nil {
+ return fmt.Errorf("status --instances: %w", err)
+ }
+ printStatusInstances(w, rows, account)
+ return nil
+}
+
+func printStatusInstances(w io.Writer, rows []statusInstanceRow, account string) {
+ if len(rows) == 0 {
+ if account != "" {
+ mutedf(w, "no awake instances for %s right now", account)
+ } else {
+ mutedf(w, "no awake instances right now")
+ }
+ return
+ }
+ t := newTable(w)
+ fmt.Fprintln(t, "BOX\tHOST\tACCOUNT\tAPP\tCPU\tMEM")
+ for _, r := range rows {
+ host, app := r.Host, r.App
+ if r.Platform {
+ // A unit that matched no route: the console's own unit, the
+ // agent, system units. Named as what it is rather than left as
+ // two blank cells a reader would take for missing data — an
+ // operator asking what is on the box wants the box's own
+ // overhead in the picture.
+ host, app = r.Unit, "(platform)"
+ }
+ fmt.Fprintf(t, "%s\t%s\t%s\t%s\t%s\t%s\n",
+ r.Box, dashIfEmpty(host), dashIfEmpty(r.Account), dashIfEmpty(app),
+ cpuCell(r.CPU), memCell(r.Mem))
+ }
+ t.Flush()
+ // Said every time. A hibernating instance has no process, so it has no
+ // cgroup and no reading — it is not in this list and its absence is not
+ // evidence that it does not exist. The question this verb answers is
+ // "right now", and that limit belongs next to the answer.
+ mutedf(w, "(awake instances only — a hibernating instance has no process to measure)")
+}
+
+// cpuCell renders a fraction of one core. A nil reading prints as a dash: the
+// unit was sampled but has no CPU delta yet, which is not the same as idle,
+// and 0.000 is a claim about the process rather than about the sample.
+func cpuCell(v *float64) string {
+ if v == nil {
+ return "—"
+ }
+ return fmt.Sprintf("%.3f", *v)
+}
+
+// memCell renders bytes the way an operator scans them. Same nil rule.
+func memCell(v *int64) string {
+ if v == nil {
+ return "—"
+ }
+ b := float64(*v)
+ switch {
+ case b >= 1<<30:
+ return fmt.Sprintf("%.1fGB", b/(1<<30))
+ case b >= 1<<20:
+ return fmt.Sprintf("%.0fMB", b/(1<<20))
+ case b >= 1<<10:
+ return fmt.Sprintf("%.0fKB", b/(1<<10))
+ }
+ return fmt.Sprintf("%.0fB", b)
+}