carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// `carlos availability` — the requests an app's instances could not answer,
2// with no box access (spec 2026-09-07-errors-and-analytics-design.md §3.2,
3// amendment 2026-09-24): edge 502/503/504 grouped by cause, and the
4// platform's events about the instance's lifecycle, read through the
5// console API. The Errors command's sibling; API mode only, like it.
6package main
7
8import (
9 "context"
10 "flag"
11 "log"
12 "net/url"
13
14 "github.com/carlosframework/platform/internal/apperr"
15)
16
17const availabilityUsage = `usage: carlos availability --app <a> [flags]
18
19Lists the requests the app's instances could not answer, newest first: one
20row per cause (the instance refused the connection, hung up, timed out, no
21fleet box connected, the app answered 503 itself...) with its count in the
22window and the hosts and releases it was seen on, plus the platform's own
23events about the instance (failed wakes, exits). A 502 on any page of a
24dead instance is one row, not one per page. --fp <id> expands one cause to
25its occurrences, each with the path that was being asked for.
26`
27
28func runAvailability(args []string) {
29 fs := flag.NewFlagSet("availability", flag.ExitOnError)
30 app := appFlag(fs, "app whose availability to read")
31 since := fs.String("since", "24h", "window start: duration back from now, or RFC3339")
32 until := fs.String("until", "", "window end (default: now)")
33 host := fs.String("host", "", "only one instance (route host)")
34 rel := fs.String("release", "", "only one release")
35 fp := fs.String("fp", "", "expand one cause: its occurrences, oldest first")
36 consoleFlag := consoleOption(fs, "console to read through (default: the only logged-in one)")
37 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
38 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account")
39 fs.Usage = flagUsage(fs, availabilityUsage)
40 fs.Parse(args)
41
42 sess := requireAPISession("availability", *consoleFlag)
43 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
44 app2 := resolvedApp(sess, acct, *app, availabilityUsage)
45
46 q := url.Values{}
47 for k, v := range map[string]string{"since": *since, "until": *until, "host": *host, "release": *rel, "fp": *fp} {
48 if v != "" {
49 q.Set(k, v)
50 }
51 }
52 res, err := sess.availabilityGet(context.Background(), acct, app2, q)
53 if err != nil {
54 log.Fatalf("availability: %v", err)
55 }
56 if *fp != "" {
57 printErrorRecords(stdout, res.Records)
58 if len(res.Records) == 0 {
59 mutedf(stdout, "no occurrences of %s in this window", *fp)
60 }
61 return
62 }
63 printErrorGroups(stdout, platformSourced(res.Groups))
64 if len(res.Groups) == 0 {
65 mutedf(stdout, "no availability problems in this window — try --since 168h")
66 } else {
67 mutedf(stdout, "counts are to the nearest flush; the app's own 500s are under `carlos errors`")
68 }
69 if res.Truncated {
70 mutedf(stdout, "(the window held more than could be read; counts are within what was read — narrow it)")
71 }
72}
73
74// platformSourced names a log-derived group by what it is here: every log
75// line Availability shows is a platform event, and "log" would read as the
76// app's own output.
77func platformSourced(groups []apperr.Group) []apperr.Group {
78 out := make([]apperr.Group, len(groups))
79 for i, g := range groups {
80 if g.Source == apperr.SourceLog {
81 g.Source = "platform"
82 }
83 out[i] = g
84 }
85 return out
86}
87