axum_server_timings/
lib.rs1#![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
34pub 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 pub fn add_timing(&mut self, timing: Timing) {
49 #[cfg(not(hide_server_timings))]
50 self.timings.push(timing);
51 }
52
53 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 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 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 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#[derive(Debug, Clone, PartialEq)]
98pub struct Timing {
99 pub name: Cow<'static, str>,
100 pub desc: Option<Cow<'static, str>>,
101 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 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}