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