daemon/events.bend
cswap-bend-cli@0.1.0.0 · 248 lines · 46 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3
4
5
6
7
8type Win is Data:
9 Win{name: String, pct: U32}
10
11
12
13type Row is Data:
14 Row{slot: String, headroom: Maybe<&2, U32>, windows: List<&2, Win>, error: Maybe<&2, String>}
15
16def sel(+c: Bool, +x: String, +y: String) -> String:
17 match c:
18 case True{}:
19 x
20 case False{}:
21 y
22
23def two(+n: U32) -> String:
24 sel(U32.is_lt(n, 10), "0" ++ U32.show(n), U32.show(n))
25
26
27def frac(+cents: U32) -> String:
28 sel(U32.is_eq((cents % 10 : U32), 0), U32.show((cents / 10 : U32)), two(cents))
29
30
31def float_text(+c: U32) -> String:
32 U32.show((c / 100 : U32)) ++ "." ++ frac((c % 100 : U32))
33
34def fl(+c: U32) -> Json.Json:
35 Json.JNum{float_text(c)}
36
37
38def pct_label(+c: U32) -> String:
39 sel(U32.is_eq((c % 100 : U32), 0), U32.show((c / 100 : U32)), float_text(c))
40
41def pair(+k: String, +v: Json.Json, +rest: Json.Json) -> Json.Json:
42 Json.JField{k, v, rest}
43
44def str(+s: String) -> Json.Json:
45 Json.JStr{s}
46
47
48def event(+kind: String, +ts: String, +fields: Json.Json) -> Json.Json:
49 Json.JObj{pair("schemaVersion", Json.JNum{"1"}, pair("event", str(kind), pair("ts", str(ts), fields)))}
50
51
52def ref(+slot: String, +email: String) -> Json.Json:
53 Json.JObj{pair("number", Json.JNum{slot}, pair("email", str(email), Json.JNil{}))}
54
55def ref_opt(+m: Maybe<&2, String>, +email: String) -> Json.Json:
56 match m:
57 case None{}:
58 Json.JNull{}
59 case Some{s}:
60 ref(s, email)
61
62
63
64def hr_json(+m: Maybe<&2, U32>) -> Json.Json:
65 match m:
66 case None{}:
67 Json.JNull{}
68 case Some{c}:
69 fl(c)
70
71def headroom_chain(+rows: List<&2, Row>) -> Json.Json:
72 match rows:
73 case Nil{}:
74 Json.JNil{}
75 case Con{Row{s, h, w, e}, t}:
76 pair(s, hr_json(h), headroom_chain(t))
77
78def windows_chain(+ws: List<&2, Win>) -> Json.Json:
79 match ws:
80 case Nil{}:
81 Json.JNil{}
82 case Con{Win{n, p}, t}:
83 pair(n, fl(p), windows_chain(t))
84
85def windows_pick(+empty: Bool, +s: String, +w: List<&2, Win>, +rest: Json.Json) -> Json.Json:
86 match empty:
87 case True{}:
88 rest
89 case False{}:
90 pair(s, Json.JObj{windows_chain(w)}, rest)
91
92
93def windows_all(+rows: List<&2, Row>) -> Json.Json:
94 match rows:
95 case Nil{}:
96 Json.JNil{}
97 case Con{Row{s, h, w, e}, t}:
98 windows_pick(List.is_empty(&2, Win, w), s, w, windows_all(t))
99
100
101def errors_pick(+h: Maybe<&2, U32>, +e: Maybe<&2, String>, +s: String, +rest: Json.Json) -> Json.Json:
102 match h e:
103 case None{} Some{m}:
104 pair(s, str(m), rest)
105 case _ _:
106 rest
107
108def errors_all(+rows: List<&2, Row>) -> Json.Json:
109 match rows:
110 case Nil{}:
111 Json.JNil{}
112 case Con{Row{s, h, w, e}, t}:
113 errors_pick(h, e, s, errors_all(t))
114
115def opt_obj(+c: Json.Json, +key: String, +rest: Json.Json) -> Json.Json:
116 match c:
117 case Json.JNil{}:
118 rest
119 case _:
120 pair(key, Json.JObj{c}, rest)
121
122
123def poll(+ts: String, +active: Json.Json, +rows: List<&2, Row>, +threshold: U32) -> Json.Json:
124 event("poll", ts, pair("active", active, pair("headroomPct", Json.JObj{headroom_chain(rows)}, pair("threshold", fl(threshold),
125 opt_obj(errors_all(rows), "fetchErrors", opt_obj(windows_all(rows), "windowsPct", Json.JNil{}))))))
126
127
128
129def strs(+xs: List<&2, String>) -> List<&2, Json.Json>:
130 match xs:
131 case Nil{}:
132 []
133 case Con{h, t}:
134 str(h) <> strs(t)
135
136def switch(+ts: String, +trigger: String, +from: Json.Json, +to: Json.Json, +warnings: List<&2, String>, +dry_run: Bool) -> Json.Json:
137 event("switch", ts, pair("trigger", str(trigger), pair("from", from, pair("to", to,
138 pair("warnings", Json.arr(strs(warnings)), pair("dryRun", Json.JBool{dry_run}, Json.JNil{}))))))
139
140def no_switch(+ts: String, +reason: String, +detail: String) -> Json.Json:
141 event("no-switch", ts, pair("reason", str(reason), pair("detail", str(detail), Json.JNil{})))
142
143def quarantined(+ts: String, +slot: String, +email: String, +reason: String) -> Json.Json:
144 event("account-quarantined", ts, pair("number", str(slot), pair("email", str(email), pair("reason", str(reason), Json.JNil{}))))
145
146def unquarantined(+ts: String, +slot: String, +email: String, +reason: String) -> Json.Json:
147 event("account-unquarantined", ts, pair("number", str(slot), pair("email", str(email), pair("reason", str(reason), Json.JNil{}))))
148
149def opt_str(+m: Maybe<&2, String>) -> Json.Json:
150 match m:
151 case None{}:
152 Json.JNull{}
153 case Some{x}:
154 str(x)
155
156def is_none(+m: Maybe<&2, String>) -> Bool:
157 match m:
158 case None{}:
159 True{}
160 case Some{x}:
161 False{}
162
163
164def all_exhausted(+ts: String, +earliest: Maybe<&2, String>) -> Json.Json:
165 event("all-exhausted", ts, pair("earliestResetAt", opt_str(earliest), Json.JNil{}))
166
167
168def sleep(+ts: String, +deci: U32, +until: String) -> Json.Json:
169 event("sleep", ts, pair("seconds", Json.JNum{U32.show((deci / 10 : U32)) ++ "." ++ U32.show((deci % 10 : U32))}, pair("until", str(until), Json.JNil{})))
170
171def error(+ts: String, +message: String, +transient: Bool) -> Json.Json:
172 event("error", ts, pair("message", str(message), pair("transient", Json.JBool{transient}, Json.JNil{})))
173
174def config_warning(+ts: String, +message: String) -> Json.Json:
175 event("config-warning", ts, pair("message", str(message), Json.JNil{}))
176
177
178def line(+e: Json.Json) -> String:
179 Json.emit_py(e)
180
181
182
183def s_of(+m: Maybe<&2, Json.Json>) -> String:
184 match m:
185 case Some{Json.JStr{s}}:
186 s
187 case Some{Json.JNum{t}}:
188 t
189 case _:
190 ""
191
192def f_of(+j: Json.Json, +k: String) -> String:
193 s_of(Json.get(k, j))
194
195def ref_text(+j: Json.Json) -> String:
196 match j:
197 case Json.JNull{}:
198 "(none)"
199 case _:
200 "Account-" ++ f_of(j, "number") ++ " (" ++ f_of(j, "email") ++ ")"
201
202def null_or(+m: Maybe<&2, Json.Json>) -> Json.Json:
203 match m:
204 case Some{v}:
205 v
206 case None{}:
207 Json.JNull{}
208
209def get_or_null(+j: Json.Json, +k: String) -> Json.Json:
210 null_or(Json.get(k, j))
211
212def poll_text2(+active: Json.Json, +e: Json.Json) -> String:
213 match active:
214 case Json.JNull{}:
215 "poll: no active account"
216 case _:
217 "poll: " ++ ref_text(active) ++ " (switch at " ++ f_of(e, "threshold") ++ "%)"
218
219def poll_text(+e: Json.Json) -> String:
220 poll_text2(get_or_null(e, "active"), e)
221
222def detail_text(+d: String) -> String:
223 sel(String.is_empty(d), "", " (" ++ d ++ ")")
224
225def is_dry(+m: Maybe<&2, Json.Json>) -> Bool:
226 match m:
227 case Some{Json.JBool{b}}:
228 b
229 case _:
230 False{}
231
232def switch_text(+e: Json.Json) -> String:
233 sel(is_dry(Json.get("dryRun", e)), "[dry-run] would switch ", "Switched ") ++ ref_text(get_or_null(e, "from")) ++ " -> " ++ ref_text(get_or_null(e, "to")) ++ " (" ++ f_of(e, "trigger") ++ ")"
234
235def human2(+kind: String, +e: Json.Json) -> String:
236 sel(String.eq(kind, "poll"), poll_text(e),
237 sel(String.eq(kind, "switch"), switch_text(e),
238 sel(String.eq(kind, "no-switch"), "no switch: " ++ f_of(e, "reason") ++ detail_text(f_of(e, "detail")),
239 sel(String.eq(kind, "all-exhausted"), "all accounts exhausted; earliest reset " ++ f_of(e, "earliestResetAt"),
240 sel(String.eq(kind, "account-quarantined"), "Account-" ++ f_of(e, "number") ++ " (" ++ f_of(e, "email") ++ ") quarantined: " ++ f_of(e, "reason"),
241 sel(String.eq(kind, "account-unquarantined"), "Account-" ++ f_of(e, "number") ++ " (" ++ f_of(e, "email") ++ ") back in rotation (" ++ f_of(e, "reason") ++ ")",
242 sel(String.eq(kind, "sleep"), "sleeping " ++ f_of(e, "seconds") ++ "s (until " ++ f_of(e, "until") ++ ")",
243 sel(String.eq(kind, "error"), "error: " ++ f_of(e, "message") ++ " (will retry)",
244 "warning: " ++ f_of(e, "message")))))))))
245
246
247def human(+e: Json.Json) -> String:
248 human2(f_of(e, "event"), e)