// `carlos availability` — the requests an app's instances could not answer, // with no box access (spec 2026-09-07-errors-and-analytics-design.md §3.2, // amendment 2026-09-24): edge 502/503/504 grouped by cause, and the // platform's events about the instance's lifecycle, read through the // console API. The Errors command's sibling; API mode only, like it. package main import ( "context" "flag" "log" "net/url" "github.com/carlosframework/platform/internal/apperr" ) const availabilityUsage = `usage: carlos availability --app [flags] Lists the requests the app's instances could not answer, newest first: one row per cause (the instance refused the connection, hung up, timed out, no fleet box connected, the app answered 503 itself...) with its count in the window and the hosts and releases it was seen on, plus the platform's own events about the instance (failed wakes, exits). A 502 on any page of a dead instance is one row, not one per page. --fp expands one cause to its occurrences, each with the path that was being asked for. ` func runAvailability(args []string) { fs := flag.NewFlagSet("availability", flag.ExitOnError) app := appFlag(fs, "app whose availability to read") since := fs.String("since", "24h", "window start: duration back from now, or RFC3339") until := fs.String("until", "", "window end (default: now)") host := fs.String("host", "", "only one instance (route host)") rel := fs.String("release", "", "only one release") fp := fs.String("fp", "", "expand one cause: its occurrences, oldest first") consoleFlag := consoleOption(fs, "console to read through (default: the only logged-in one)") acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account") fs.Usage = flagUsage(fs, availabilityUsage) fs.Parse(args) sess := requireAPISession("availability", *consoleFlag) acct := resolvedAccount(sess, acctRaw, acctIDRaw) app2 := resolvedApp(sess, acct, *app, availabilityUsage) q := url.Values{} for k, v := range map[string]string{"since": *since, "until": *until, "host": *host, "release": *rel, "fp": *fp} { if v != "" { q.Set(k, v) } } res, err := sess.availabilityGet(context.Background(), acct, app2, q) if err != nil { log.Fatalf("availability: %v", err) } if *fp != "" { printErrorRecords(stdout, res.Records) if len(res.Records) == 0 { mutedf(stdout, "no occurrences of %s in this window", *fp) } return } printErrorGroups(stdout, platformSourced(res.Groups)) if len(res.Groups) == 0 { mutedf(stdout, "no availability problems in this window — try --since 168h") } else { mutedf(stdout, "counts are to the nearest flush; the app's own 500s are under `carlos errors`") } if res.Truncated { mutedf(stdout, "(the window held more than could be read; counts are within what was read — narrow it)") } } // platformSourced names a log-derived group by what it is here: every log // line Availability shows is a platform event, and "log" would read as the // app's own output. func platformSourced(groups []apperr.Group) []apperr.Group { out := make([]apperr.Group, len(groups)) for i, g := range groups { if g.Source == apperr.SourceLog { g.Source = "platform" } out[i] = g } return out }