Skip to main content

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}