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 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}