software_engineering/incident_metrics.rs
1//! # Incident Metrics
2//!
3//! Incident response time decomposes into distinct phases: **mean time to
4//! detect (MTTD)**, how long before the organization notices something is
5//! wrong; **mean time to acknowledge (MTTA)**, how long before someone takes
6//! ownership of responding; and **mean time to resolve or recover (MTTR)**,
7//! how long from ownership to genuine recovery. Reporting these separately,
8//! rather than only a single blended total, matters because each phase
9//! points to a different fix: slow detection points to a monitoring gap,
10//! slow acknowledgement points to an on-call process gap, and slow
11//! resolution points to a tooling or runbook gap.
12//!
13//! ## Formula
14//!
15//! ```text
16//! MTTD = mean(detection durations)
17//! MTTA = mean(acknowledgement durations)
18//! MTTR = mean(resolution durations)
19//! Mean duration = mean(total incident durations)
20//! ```
21//!
22//! ## Why it matters
23//!
24//! Track incident frequency and MTTR together, never in isolation, mirroring
25//! DORA's speed-and-stability pairing discipline: an improving MTTR
26//! alongside a rising incident frequency might indicate a team getting
27//! better at firefighting while underlying reliability actually degrades, and
28//! a falling frequency alongside a worsening MTTR might indicate rarer but
29//! more severe, harder-to-diagnose failures replacing frequent minor ones.
30//! Reviewing both together, rather than either alone, is what gives an
31//! honest combined picture.
32//!
33//! ## Example
34//!
35//! ```rust
36//! use software_engineering::incident_metrics::{
37//! mean_time_to_detect_minutes, mean_time_to_acknowledge_minutes,
38//! mean_time_to_resolve_minutes, mean_incident_duration_minutes,
39//! };
40//!
41//! let detection = [5.0, 15.0];
42//! let acknowledgement = [2.0, 4.0];
43//! let resolution = [30.0, 90.0];
44//! let total = [37.0, 109.0];
45//!
46//! assert_eq!(mean_time_to_detect_minutes(&detection), Some(10.0));
47//! assert_eq!(mean_time_to_acknowledge_minutes(&acknowledgement), Some(3.0));
48//! assert_eq!(mean_time_to_resolve_minutes(&resolution), Some(60.0));
49//! assert_eq!(mean_incident_duration_minutes(&total), Some(73.0));
50//! ```
51//!
52//! ## Pitfalls
53//!
54//! - **Reporting only a single blended total** instead of the three
55//! decomposed phases — hides which specific gap (monitoring, on-call
56//! process, or tooling) is driving a slow response.
57//! - **Reviewing incident frequency and MTTR in isolation** — misses the
58//! pattern where one metric's improvement masks the other's decline.
59//! - **Inconsistent severity classification across teams** — makes
60//! organization-wide incident data as unreliable for comparison as
61//! inconsistently classified defect data.
62//! - **Extracting no systemic action items from postmortems** — produces
63//! insight with no follow-through, wasting the organizational learning
64//! the process is meant to capture.
65//!
66//! ## Sources
67//!
68//! - Chapter 6.2, Incident metrics.
69//!
70//! Topic doc: software-engineering-metrics/locales/en-001/chapters/06-02-incident-metrics.md
71
72/// The arithmetic mean of a slice of per-incident durations, in minutes.
73///
74/// Shared by all four public functions in this module, each of which
75/// applies it to a different phase of incident response.
76fn mean_duration_minutes(durations_minutes: &[f64]) -> Option<f64> {
77 if durations_minutes.is_empty() {
78 return None;
79 }
80 let sum: f64 = durations_minutes.iter().sum();
81 // Incident counts never approach f64's precision limit, so this cast
82 // never loses precision in practice.
83 #[allow(clippy::cast_precision_loss)]
84 let count = durations_minutes.len() as f64;
85 Some(sum / count)
86}
87
88/// Mean time to detect (MTTD): the mean, across incidents, of the duration
89/// from a failure's actual onset to someone noticing it.
90///
91/// # Arguments
92///
93/// * `detection_durations_minutes` — per-incident detection durations, in
94/// minutes.
95///
96/// # Returns
97///
98/// The mean detection duration, or `None` if the slice is empty.
99///
100/// # Examples
101///
102/// ```rust
103/// use software_engineering::incident_metrics::mean_time_to_detect_minutes;
104///
105/// assert_eq!(mean_time_to_detect_minutes(&[5.0, 15.0]), Some(10.0));
106/// assert_eq!(mean_time_to_detect_minutes(&[]), None);
107/// ```
108#[must_use]
109pub fn mean_time_to_detect_minutes(detection_durations_minutes: &[f64]) -> Option<f64> {
110 mean_duration_minutes(detection_durations_minutes)
111}
112
113/// Mean time to acknowledge (MTTA): the mean, across incidents, of the
114/// duration from notification to someone taking ownership of the response.
115///
116/// # Arguments
117///
118/// * `acknowledgement_durations_minutes` — per-incident acknowledgement
119/// durations, in minutes.
120///
121/// # Returns
122///
123/// The mean acknowledgement duration, or `None` if the slice is empty.
124///
125/// # Examples
126///
127/// ```rust
128/// use software_engineering::incident_metrics::mean_time_to_acknowledge_minutes;
129///
130/// assert_eq!(mean_time_to_acknowledge_minutes(&[2.0, 4.0]), Some(3.0));
131/// assert_eq!(mean_time_to_acknowledge_minutes(&[]), None);
132/// ```
133#[must_use]
134pub fn mean_time_to_acknowledge_minutes(acknowledgement_durations_minutes: &[f64]) -> Option<f64> {
135 mean_duration_minutes(acknowledgement_durations_minutes)
136}
137
138/// Mean time to resolve or recover (MTTR): the mean, across incidents, of
139/// the duration from ownership to genuine recovery.
140///
141/// # Arguments
142///
143/// * `resolution_durations_minutes` — per-incident resolution durations, in
144/// minutes.
145///
146/// # Returns
147///
148/// The mean resolution duration, or `None` if the slice is empty.
149///
150/// # Examples
151///
152/// ```rust
153/// use software_engineering::incident_metrics::mean_time_to_resolve_minutes;
154///
155/// assert_eq!(mean_time_to_resolve_minutes(&[30.0, 90.0]), Some(60.0));
156/// assert_eq!(mean_time_to_resolve_minutes(&[]), None);
157/// ```
158#[must_use]
159pub fn mean_time_to_resolve_minutes(resolution_durations_minutes: &[f64]) -> Option<f64> {
160 mean_duration_minutes(resolution_durations_minutes)
161}
162
163/// Mean total incident duration, from detection start to full resolution —
164/// the blended total to report *alongside*, never instead of, the three
165/// decomposed phases above.
166///
167/// # Arguments
168///
169/// * `total_durations_minutes` — per-incident total durations, in minutes.
170///
171/// # Returns
172///
173/// The mean total duration, or `None` if the slice is empty.
174///
175/// # Examples
176///
177/// ```rust
178/// use software_engineering::incident_metrics::mean_incident_duration_minutes;
179///
180/// assert_eq!(mean_incident_duration_minutes(&[37.0, 109.0]), Some(73.0));
181/// assert_eq!(mean_incident_duration_minutes(&[]), None);
182/// ```
183#[must_use]
184pub fn mean_incident_duration_minutes(total_durations_minutes: &[f64]) -> Option<f64> {
185 mean_duration_minutes(total_durations_minutes)
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 // "mean time to detect (MTTD), how long before the organization
193 // notices something is wrong."
194 #[test]
195 fn mttd_computes_mean_of_detection_durations() {
196 let mttd = mean_time_to_detect_minutes(&[5.0, 15.0]).unwrap();
197 assert!((mttd - 10.0).abs() < 1e-9);
198 }
199
200 #[test]
201 fn mttd_is_none_for_empty_slice() {
202 assert_eq!(mean_time_to_detect_minutes(&[]), None);
203 }
204
205 // "mean time to acknowledge (MTTA), how long before someone takes
206 // ownership of responding."
207 #[test]
208 fn mtta_computes_mean_of_acknowledgement_durations() {
209 let mtta = mean_time_to_acknowledge_minutes(&[2.0, 4.0]).unwrap();
210 assert!((mtta - 3.0).abs() < 1e-9);
211 }
212
213 #[test]
214 fn mtta_is_none_for_empty_slice() {
215 assert_eq!(mean_time_to_acknowledge_minutes(&[]), None);
216 }
217
218 // "mean time to resolve or recover (MTTR), how long from ownership to
219 // genuine recovery."
220 #[test]
221 fn mttr_computes_mean_of_resolution_durations() {
222 let mttr = mean_time_to_resolve_minutes(&[30.0, 90.0]).unwrap();
223 assert!((mttr - 60.0).abs() < 1e-9);
224 }
225
226 #[test]
227 fn mttr_is_none_for_empty_slice() {
228 assert_eq!(mean_time_to_resolve_minutes(&[]), None);
229 }
230
231 #[test]
232 fn mean_incident_duration_computes_mean_of_totals() {
233 let mean_duration = mean_incident_duration_minutes(&[37.0, 109.0]).unwrap();
234 assert!((mean_duration - 73.0).abs() < 1e-9);
235 }
236
237 #[test]
238 fn mean_incident_duration_is_none_for_empty_slice() {
239 assert_eq!(mean_incident_duration_minutes(&[]), None);
240 }
241}