runnerd_core/api/insights.rs
1//! Insights as clients read them: what got slower, what is flaky, what nears its memory cap and how long jobs wait,
2//! computed from the job history (ADR 0051); the daily digest; and what the daemon's notifications carry. Times are
3//! seconds since the Unix epoch, durations seconds, sizes bytes.
4
5use serde::{Deserialize, Serialize};
6
7use super::RunnerFailure;
8use super::jobs::JobSummary;
9
10/// The window `GET /v1/insights` reads by default, in days.
11pub const INSIGHT_DAYS: u32 = 14;
12
13/// The longest window `GET /v1/insights` reads, in days.
14pub const INSIGHT_DAYS_MAX: u32 = 90;
15
16/// `GET /v1/insights` query parameters.
17#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
18pub struct InsightsQuery {
19 /// Days of history to read; default [`INSIGHT_DAYS`], at most [`INSIGHT_DAYS_MAX`].
20 pub days: Option<u32>,
21}
22
23/// `GET /v1/insights`: what the history says about the window.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct Insights {
26 /// The start of the window.
27 pub since: u64,
28 /// When it was read: the end of the window.
29 pub until: u64,
30 /// Warnings first, then by repository, workflow and job.
31 pub insights: Vec<Insight>,
32 /// Each repository's waits this week and the week before, by repository. Always the last 14 days, whatever the
33 /// window.
34 pub waits: Vec<RepositoryWaits>,
35}
36
37/// How much an insight matters.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
39#[serde(rename_all = "kebab-case")]
40pub enum Severity {
41 /// Worth acting on: notifications send these.
42 Warning,
43 /// Worth knowing.
44 Notice,
45}
46
47/// One thing the history says, about one job or one repository.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct Insight {
50 /// The same while it holds, such as `slower:halyard/api/CI/test`.
51 pub key: String,
52 /// How much it matters.
53 pub severity: Severity,
54 /// `owner/name`.
55 pub repository: String,
56 /// The workflow's name; `None` for a repository's waits, or when unknown.
57 pub workflow: Option<String>,
58 /// The job's name; `None` for a repository's waits.
59 pub job: Option<String>,
60 /// What it is, in one line.
61 pub sentence: String,
62 /// What it is, with its figures.
63 #[serde(flatten)]
64 pub kind: InsightKind,
65 /// The runs that show it, newest first, at most 5.
66 pub runs: Vec<RunLink>,
67}
68
69/// What an insight is, tagged by `kind`.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "kebab-case", tag = "kind")]
72pub enum InsightKind {
73 /// A job's successful runs on its busiest branch take longer than they did, and stayed so.
74 Slower {
75 /// The branch.
76 branch: Option<String>,
77 /// The median run time before, in seconds.
78 before: u64,
79 /// The median run time of the last runs, in seconds.
80 now: u64,
81 /// The first slow run after which no two runs in a row were fast.
82 since: Option<RunLink>,
83 },
84 /// A job failed and then passed on a re-run of the same run.
85 Flaky {
86 /// Runs in which it did.
87 flaky: u32,
88 /// The job's runs in the window.
89 total: u32,
90 },
91 /// A job's memory peaks near its profile's cap.
92 NearCap {
93 /// The profile.
94 profile: String,
95 /// The 95th percentile of its last runs' memory peaks.
96 peak: u64,
97 /// The profile's `memory`.
98 cap: u64,
99 /// What auto sizing does about it.
100 auto: AutoNote,
101 },
102 /// A repository's jobs wait longer this week than the week before.
103 Waits {
104 /// The 95th percentile of waits the week before, in seconds.
105 before: u64,
106 /// The 95th percentile of waits this week, in seconds.
107 now: u64,
108 },
109}
110
111/// What auto sizing does about a job near its cap.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "kebab-case")]
114pub enum AutoNote {
115 /// It raises the profile's `memory` when a run meets the cap.
116 Raises,
117 /// The profile is at its `max_memory`: it cannot.
118 AtMax,
119 /// It is off.
120 Off,
121}
122
123/// One run of a job, to open in the history or on GitHub.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct RunLink {
126 /// GitHub's job ID, the history's too.
127 pub job: u64,
128 /// The workflow run.
129 pub run_id: u64,
130 /// The run's attempt.
131 pub run_attempt: u32,
132 /// The branch.
133 pub branch: Option<String>,
134 /// The job's page on GitHub.
135 pub url: Option<String>,
136 /// When it finished.
137 pub at: u64,
138}
139
140/// A repository's waits this week against the week before.
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct RepositoryWaits {
143 /// `owner/name`.
144 pub repository: String,
145 /// The jobs that started in the last 7 days.
146 pub now: WeekWaits,
147 /// The jobs that started in the 7 days before.
148 pub before: WeekWaits,
149}
150
151/// The waits of the jobs that started in a week, from queued to started.
152#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
153pub struct WeekWaits {
154 /// Every job.
155 pub all: WaitSpread,
156 /// The jobs that found a runner already started: a warm runner (ADR 0050).
157 pub warm: WaitSpread,
158 /// The jobs whose runner started after they were queued.
159 pub cold: WaitSpread,
160}
161
162/// How long some jobs waited.
163#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
164pub struct WaitSpread {
165 /// How many.
166 pub jobs: u32,
167 /// The median, in seconds; `None` without jobs.
168 pub p50: Option<u64>,
169 /// The 95th percentile by nearest rank, in seconds; `None` without jobs.
170 pub p95: Option<u64>,
171}
172
173/// `GET /v1/digest`: the last 24 hours.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct Digest {
176 /// 24 hours before `until`.
177 pub since: u64,
178 /// When it was read.
179 pub until: u64,
180 /// Jobs that finished: completed, or lost with their runner.
181 pub finished: u64,
182 /// Of those, the ones that failed, timed out or were lost.
183 pub failed: u64,
184 /// The insights that hold now and did not 24 hours ago.
185 pub new: Vec<Insight>,
186 /// The three longest waits of the jobs that started, longest first.
187 pub waits: Vec<LongWait>,
188}
189
190/// A job that waited.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct LongWait {
193 /// GitHub's job ID.
194 pub job: u64,
195 /// `owner/name`.
196 pub repository: String,
197 /// The workflow's name.
198 pub workflow: Option<String>,
199 /// The job's name.
200 pub name: String,
201 /// Seconds from queued to started.
202 pub waited: u64,
203 /// The job's page on GitHub.
204 pub url: Option<String>,
205}
206
207/// What a notification target is sent, and the events `[notify]` targets choose from.
208#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
209#[serde(rename_all = "kebab-case")]
210pub enum NotifyEvent {
211 /// A job failed or timed out.
212 Failed,
213 /// A job's runner vanished before GitHub said the job completed.
214 Lost,
215 /// A runner could not be registered or started, and its repository is held back.
216 Runner,
217 /// A new warning insight.
218 Insights,
219 /// The daily digest.
220 Digest,
221 /// `runnerctl notify test`; not one a target chooses.
222 Test,
223}
224
225/// How urgent a notification is, as ntfy ranks it.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "kebab-case")]
228pub enum Priority {
229 /// ntfy's 2: no sound.
230 Low,
231 /// ntfy's 3.
232 Default,
233 /// ntfy's 4: a longer vibration.
234 High,
235}
236
237/// What a webhook target is posted, as JSON; an ntfy target gets its title, message, priority and link.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct Notification {
240 /// What it is about.
241 pub event: NotifyEvent,
242 /// One short line.
243 pub title: String,
244 /// A few lines.
245 pub message: String,
246 /// What to open: a job's page on GitHub, or nothing.
247 pub url: Option<String>,
248 /// How urgent.
249 pub priority: Priority,
250 /// The jobs it is about: failed or lost.
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
252 pub jobs: Vec<JobSummary>,
253 /// The runners that could not start.
254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
255 pub runners: Vec<RunnerFailure>,
256 /// The insights it is about.
257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
258 pub insights: Vec<Insight>,
259 /// The digest.
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub digest: Option<Digest>,
262}
263
264/// `POST /v1/notify/test` query parameters.
265#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
266pub struct NotifyTestQuery {
267 /// Only the target with this name; default every one.
268 pub target: Option<String>,
269}
270
271/// `POST /v1/notify/test`: what each target did with a test message.
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct NotifyTest {
274 /// In the config's order.
275 pub targets: Vec<Delivery>,
276}
277
278/// What one target did with a message.
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct Delivery {
281 /// The target's name.
282 pub target: String,
283 /// Why it was not delivered: an HTTP status or a connection error, never the URL. `None` when it was.
284 pub error: Option<String>,
285}