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 job's runs that write to the build cache find almost nothing in it: its cache keys differ from run to run.
103 ColdCache {
104 /// The branch those runs are on.
105 branch: Option<String>,
106 /// Lookups that found an entry, over those runs.
107 hits: u64,
108 /// Lookups over those runs.
109 lookups: u64,
110 },
111 /// A repository's jobs wait longer this week than the week before.
112 Waits {
113 /// The 95th percentile of waits the week before, in seconds.
114 before: u64,
115 /// The 95th percentile of waits this week, in seconds.
116 now: u64,
117 },
118}
119
120/// What auto sizing does about a job near its cap.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "kebab-case")]
123pub enum AutoNote {
124 /// It raises the profile's `memory` when a run meets the cap.
125 Raises,
126 /// The profile is at its `max_memory`: it cannot.
127 AtMax,
128 /// It is off.
129 Off,
130}
131
132/// One run of a job, to open in the history or on GitHub.
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub struct RunLink {
135 /// GitHub's job ID, the history's too.
136 pub job: u64,
137 /// The workflow run.
138 pub run_id: u64,
139 /// The run's attempt.
140 pub run_attempt: u32,
141 /// The branch.
142 pub branch: Option<String>,
143 /// The job's page on GitHub.
144 pub url: Option<String>,
145 /// When it finished.
146 pub at: u64,
147}
148
149/// A repository's waits this week against the week before.
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct RepositoryWaits {
152 /// `owner/name`.
153 pub repository: String,
154 /// The jobs that started in the last 7 days.
155 pub now: WeekWaits,
156 /// The jobs that started in the 7 days before.
157 pub before: WeekWaits,
158}
159
160/// The waits of the jobs that started in a week, from queued to started.
161#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
162pub struct WeekWaits {
163 /// Every job.
164 pub all: WaitSpread,
165 /// The jobs that found a runner already started: a warm runner (ADR 0050).
166 pub warm: WaitSpread,
167 /// The jobs whose runner started after they were queued.
168 pub cold: WaitSpread,
169}
170
171/// How long some jobs waited.
172#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
173pub struct WaitSpread {
174 /// How many.
175 pub jobs: u32,
176 /// The median, in seconds; `None` without jobs.
177 pub p50: Option<u64>,
178 /// The 95th percentile by nearest rank, in seconds; `None` without jobs.
179 pub p95: Option<u64>,
180}
181
182/// `GET /v1/digest`: the last 24 hours.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct Digest {
185 /// 24 hours before `until`.
186 pub since: u64,
187 /// When it was read.
188 pub until: u64,
189 /// Jobs that finished: completed, or lost with their runner.
190 pub finished: u64,
191 /// Of those, the ones that failed, timed out or were lost.
192 pub failed: u64,
193 /// The insights that hold now and did not 24 hours ago.
194 pub new: Vec<Insight>,
195 /// The three longest waits of the jobs that started, longest first.
196 pub waits: Vec<LongWait>,
197}
198
199/// A job that waited.
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct LongWait {
202 /// GitHub's job ID.
203 pub job: u64,
204 /// `owner/name`.
205 pub repository: String,
206 /// The workflow's name.
207 pub workflow: Option<String>,
208 /// The job's name.
209 pub name: String,
210 /// Seconds from queued to started.
211 pub waited: u64,
212 /// The job's page on GitHub.
213 pub url: Option<String>,
214}
215
216/// What a notification target is sent, and the events `[notify]` targets choose from.
217#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
218#[serde(rename_all = "kebab-case")]
219pub enum NotifyEvent {
220 /// A job failed or timed out.
221 Failed,
222 /// A job's runner vanished before GitHub said the job completed.
223 Lost,
224 /// A runner could not be registered or started, and its repository is held back.
225 Runner,
226 /// A new warning insight.
227 Insights,
228 /// The daily digest.
229 Digest,
230 /// `runnerctl notify test`; not one a target chooses.
231 Test,
232}
233
234/// How urgent a notification is, as ntfy ranks it.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
236#[serde(rename_all = "kebab-case")]
237pub enum Priority {
238 /// ntfy's 2: no sound.
239 Low,
240 /// ntfy's 3.
241 Default,
242 /// ntfy's 4: a longer vibration.
243 High,
244}
245
246/// What a webhook target is posted, as JSON; an ntfy target gets its title, message, priority and link.
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct Notification {
249 /// What it is about.
250 pub event: NotifyEvent,
251 /// One short line.
252 pub title: String,
253 /// A few lines.
254 pub message: String,
255 /// What to open: a job's page on GitHub, or nothing.
256 pub url: Option<String>,
257 /// How urgent.
258 pub priority: Priority,
259 /// The jobs it is about: failed or lost.
260 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub jobs: Vec<JobSummary>,
262 /// The runners that could not start.
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub runners: Vec<RunnerFailure>,
265 /// The insights it is about.
266 #[serde(default, skip_serializing_if = "Vec::is_empty")]
267 pub insights: Vec<Insight>,
268 /// The digest.
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub digest: Option<Digest>,
271}
272
273/// `POST /v1/notify/test` query parameters.
274#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
275pub struct NotifyTestQuery {
276 /// Only the target with this name; default every one.
277 pub target: Option<String>,
278}
279
280/// `POST /v1/notify/test`: what each target did with a test message.
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282pub struct NotifyTest {
283 /// In the config's order.
284 pub targets: Vec<Delivery>,
285}
286
287/// What one target did with a message.
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289pub struct Delivery {
290 /// The target's name.
291 pub target: String,
292 /// Why it was not delivered: an HTTP status or a connection error, never the URL. `None` when it was.
293 pub error: Option<String>,
294}