axum_server_timings/
lib.rs

1#![deny(
2    clippy::all,
3    clippy::pedantic,
4    clippy::nursery,
5    clippy::cargo,
6    warnings
7)]
8#![cfg_attr(
9    hide_server_timings,
10    allow(
11        unused_variables,
12        unreachable_code,
13        unused_mut,
14        dead_code,
15        clippy::needless_pass_by_ref_mut,
16        clippy::unused_self,
17        clippy::needless_pass_by_value
18    )
19)]
20#![doc = include_str!("../README.md")]
21
22use std::{
23    borrow::Cow,
24    convert::Infallible,
25    fmt::{Display, Write},
26    time::Instant,
27};
28
29use axum_core::response::{IntoResponseParts, ResponseParts};
30use http::{HeaderName, HeaderValue};
31
32type TimingString = Cow<'static, str>;
33
34/// Tracker for server timings.
35/// Implements [`IntoResponseParts`], so it can be returned at the start of an axum response tuple.
36pub struct ServerTimings {
37    timings: Vec<Timing>,
38    last_timing: Instant,
39}
40
41impl ServerTimings {
42    #[must_use]
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Add a raw timing to the internal timing list. The internal duration tracker is **not** updated.
48    pub fn add_timing(&mut self, timing: Timing) {
49        #[cfg(not(hide_server_timings))]
50        self.timings.push(timing);
51    }
52
53    /// Record a timing event, with a name and description. These can be string literals or normal strings.
54    /// The time since the last event is recorded automatically.
55    pub fn record(&mut self, name: impl Into<TimingString>, desc: impl Into<TimingString>) {
56        let dur = self.advance_duration();
57        self.record_all(name.into(), Some(desc.into()), Some(dur));
58    }
59
60    /// Like [`Self::record`], but without a description. Useful for conserving bandwidth.
61    pub fn record_name(&mut self, name: impl Into<TimingString>) {
62        let dur = self.advance_duration();
63        self.record_all(name.into(), None, Some(dur));
64    }
65
66    /// Add a timing to the internal timing list. The internal duration tracker is **not** updated.
67    pub fn record_all(&mut self, name: TimingString, desc: Option<TimingString>, dur: Option<f64>) {
68        let timing = Timing { name, desc, dur };
69        self.add_timing(timing);
70    }
71
72    #[cfg(hide_server_timings)]
73    fn advance_duration(&mut self) -> f64 {
74        0.0
75    }
76
77    #[cfg(not(hide_server_timings))]
78    fn advance_duration(&mut self) -> f64 {
79        let now = Instant::now();
80        // Browsers want timings in ms
81        let dur_since = now.duration_since(self.last_timing).as_secs_f64() * 1000.0;
82        self.last_timing = now;
83        dur_since
84    }
85}
86
87impl Default for ServerTimings {
88    fn default() -> Self {
89        Self {
90            timings: Vec::new(),
91            last_timing: Instant::now(),
92        }
93    }
94}
95
96/// A representation of a server timing object.
97#[derive(Debug, Clone, PartialEq)]
98pub struct Timing {
99    pub name: Cow<'static, str>,
100    pub desc: Option<Cow<'static, str>>,
101    /// Time (in ms) operation took
102    pub dur: Option<f64>,
103}
104
105impl Display for Timing {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.write_str(&self.name)?;
108        if let Some(desc) = &self.desc {
109            f.write_str(";desc=\"")?;
110            for char in desc.chars() {
111                if char == '"' {
112                    f.write_str("\\\"")?;
113                } else {
114                    f.write_char(char)?;
115                }
116            }
117            f.write_char('"')?;
118        }
119        if let Some(dur) = self.dur {
120            write!(f, ";dur={dur}")?;
121        }
122        Ok(())
123    }
124}
125
126static TIMINGS_HEADER: HeaderName = HeaderName::from_static("server-timing");
127
128impl IntoResponseParts for ServerTimings {
129    type Error = Infallible;
130
131    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
132        #[cfg(hide_server_timings)]
133        return Ok(res);
134        let mut timing_string = String::new();
135        let mut had_error = false;
136        for timing in self.timings {
137            // we don't want to crash the server because there are no timings.
138            // thus, we have manual handling here.
139            had_error = had_error || write!(timing_string, "{timing},").is_err();
140            if had_error {
141                break;
142            }
143        }
144        if let Ok(header) = HeaderValue::from_str(timing_string.trim_end_matches(',')) {
145            res.headers_mut().append(TIMINGS_HEADER.clone(), header);
146        }
147        Ok(res)
148    }
149}