Skip to content

Commit 4351dc6

Browse files
committed
Add ServerTiming parsing
1 parent b13d9f6 commit 4351dc6

File tree

3 files changed

+238
-159
lines changed

3 files changed

+238
-159
lines changed

src/trace/server_timing/entry.rs

Lines changed: 6 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use std::str::FromStr;
21
use std::time::Duration;
32

43
use crate::headers::HeaderValue;
5-
use crate::Status;
6-
use crate::{ensure, format_err};
74

85
/// An individual `ServerTiming` entry.
96
//
@@ -17,11 +14,11 @@ use crate::{ensure, format_err};
1714
// 4. metric + value + desc cache;desc="Cache Read";dur=23.2
1815
//
1916
// Multiple different entries per line are supported; separated with a `,`.
20-
#[derive(Debug)]
17+
#[derive(Debug, Clone, Eq, PartialEq)]
2118
pub struct Entry {
22-
name: String,
23-
dur: Option<Duration>,
24-
desc: Option<String>,
19+
pub(crate) name: String,
20+
pub(crate) dur: Option<Duration>,
21+
pub(crate) desc: Option<String>,
2522
}
2623

2724
impl Entry {
@@ -76,90 +73,11 @@ impl From<Entry> for HeaderValue {
7673
}
7774
}
7875

79-
impl FromStr for Entry {
80-
type Err = crate::Error;
81-
// Create an entry from a string. Parsing rules in ABNF are:
82-
//
83-
// ```
84-
// Server-Timing = #server-timing-metric
85-
// server-timing-metric = metric-name *( OWS ";" OWS server-timing-param )
86-
// metric-name = token
87-
// server-timing-param = server-timing-param-name OWS "=" OWS server-timing-param-value
88-
// server-timing-param-name = token
89-
// server-timing-param-value = token / quoted-string
90-
// ```
91-
//
92-
// Source: https://w3c.github.io/server-timing/#the-server-timing-header-field
93-
fn from_str(s: &str) -> Result<Self, Self::Err> {
94-
let mut parts = s.trim().split(';');
95-
96-
// Get the name. This is non-optional.
97-
let name = parts
98-
.next()
99-
.ok_or_else(|| format_err!("Server timing headers must include a name"))?
100-
.trim_end();
101-
102-
// We must extract these values from the k-v pairs that follow.
103-
let mut dur = None;
104-
let mut desc = None;
105-
106-
for mut part in parts {
107-
ensure!(
108-
!part.is_empty(),
109-
"Server timing params cannot end with a trailing `;`"
110-
);
111-
112-
part = part.trim_start();
113-
114-
let mut params = part.split('=');
115-
let name = params
116-
.next()
117-
.ok_or_else(|| format_err!("Server timing params must have a name"))?
118-
.trim_end();
119-
let mut value = params
120-
.next()
121-
.ok_or_else(|| format_err!("Server timing params must have a value"))?
122-
.trim_start();
123-
124-
match name {
125-
"dur" => {
126-
let millis: f64 = value.parse().status(400).map_err(|_| {
127-
format_err!("Server timing duration params must be a valid double-precision floating-point number.")
128-
})?;
129-
dur = Some(Duration::from_secs_f64(millis / 1000.0));
130-
}
131-
"desc" => {
132-
// Ensure quotes line up, and strip them from the resulting output
133-
if value.starts_with('"') {
134-
value = &value[1..value.len()];
135-
ensure!(
136-
value.ends_with('"'),
137-
"Server timing description params must use matching quotes"
138-
);
139-
value = &value[0..value.len() - 1];
140-
} else {
141-
ensure!(
142-
!value.ends_with('"'),
143-
"Server timing description params must use matching quotes"
144-
);
145-
}
146-
desc = Some(value.to_string());
147-
}
148-
_ => continue,
149-
}
150-
}
151-
152-
Ok(Entry {
153-
name: name.to_string(),
154-
dur,
155-
desc,
156-
})
157-
}
158-
}
159-
16076
#[cfg(test)]
16177
mod test {
16278
use super::*;
79+
use crate::headers::HeaderValue;
80+
use std::time::Duration;
16381

16482
#[test]
16583
fn encode() -> crate::Result<()> {
@@ -180,71 +98,4 @@ mod test {
18098
assert_eq!(val, r#"Server; dur=1000; desc="A server timing""#);
18199
Ok(())
182100
}
183-
184-
#[test]
185-
fn decode() -> crate::Result<()> {
186-
// Metric name only.
187-
assert_entry("Server", "Server", None, None)?;
188-
assert_entry("Server ", "Server", None, None)?;
189-
assert_entry_err(
190-
"Server ;",
191-
"Server timing params cannot end with a trailing `;`",
192-
);
193-
assert_entry_err(
194-
"Server; ",
195-
"Server timing params cannot end with a trailing `;`",
196-
);
197-
198-
// Metric name + param
199-
assert_entry("Server; dur=1000", "Server", Some(1000), None)?;
200-
assert_entry("Server; dur =1000", "Server", Some(1000), None)?;
201-
assert_entry("Server; dur= 1000", "Server", Some(1000), None)?;
202-
assert_entry("Server; dur = 1000", "Server", Some(1000), None)?;
203-
assert_entry_err(
204-
"Server; dur=1000;",
205-
"Server timing params cannot end with a trailing `;`",
206-
);
207-
208-
// Metric name + desc
209-
assert_entry(r#"DB; desc="a db""#, "DB", None, Some("a db"))?;
210-
assert_entry(r#"DB; desc ="a db""#, "DB", None, Some("a db"))?;
211-
assert_entry(r#"DB; desc= "a db""#, "DB", None, Some("a db"))?;
212-
assert_entry(r#"DB; desc = "a db""#, "DB", None, Some("a db"))?;
213-
assert_entry(r#"DB; desc=a_db"#, "DB", None, Some("a_db"))?;
214-
assert_entry_err(
215-
r#"DB; desc="db"#,
216-
"Server timing description params must use matching quotes",
217-
);
218-
assert_entry_err(
219-
"Server; desc=a_db;",
220-
"Server timing params cannot end with a trailing `;`",
221-
);
222-
223-
// Metric name + dur + desc
224-
assert_entry(
225-
r#"Server; dur=1000; desc="a server""#,
226-
"Server",
227-
Some(1000),
228-
Some("a server"),
229-
)?;
230-
assert_entry_err(
231-
r#"Server; dur=1000; desc="a server";"#,
232-
"Server timing params cannot end with a trailing `;`",
233-
);
234-
Ok(())
235-
}
236-
237-
fn assert_entry_err(s: &str, msg: &str) {
238-
let err = Entry::from_str(s).unwrap_err();
239-
assert_eq!(format!("{}", err), msg);
240-
}
241-
242-
/// Assert an entry and all of its fields.
243-
fn assert_entry(s: &str, n: &str, du: Option<u64>, de: Option<&str>) -> crate::Result<()> {
244-
let e = Entry::from_str(s)?;
245-
assert_eq!(e.name(), n);
246-
assert_eq!(e.duration(), du.map(|du| Duration::from_millis(du)));
247-
assert_eq!(e.description(), de);
248-
Ok(())
249-
}
250101
}

src/trace/server_timing/mod.rs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
//! Metrics and descriptions for the given request-response cycle.
22
33
mod entry;
4+
mod parse;
45

56
pub use entry::Entry;
7+
use parse::parse_header;
68

9+
use std::convert::AsMut;
710
use std::iter::Iterator;
811
use std::slice;
912

13+
use crate::headers::HeaderValue;
14+
use crate::Headers;
15+
1016
/// Metrics and descriptions for the given request-response cycle.
1117
///
1218
/// This is an implementation of the W3C [Server
@@ -19,6 +25,33 @@ pub struct ServerTiming {
1925
}
2026

2127
impl ServerTiming {
28+
/// Create a new instance of `ServerTiming`.
29+
pub fn new() -> Self {
30+
Self { timings: vec![] }
31+
}
32+
/// Create a new instance from headers.
33+
pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Self> {
34+
let mut timings = vec![];
35+
let values = headers.as_ref().get("server-timing");
36+
for value in values.iter().map(|h| h.iter()).flatten() {
37+
parse_header(value.as_str(), &mut timings)?;
38+
}
39+
Ok(Self { timings })
40+
}
41+
42+
/// Sets the `Server-Timing` header.
43+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
44+
for timing in &self.timings {
45+
let value: HeaderValue = timing.clone().into();
46+
headers.as_mut().insert("server-timing", value);
47+
}
48+
}
49+
50+
/// Push an entry into the list of entries.
51+
pub fn push(&mut self, entry: Entry) {
52+
self.timings.push(entry);
53+
}
54+
2255
/// An iterator visiting all server timings.
2356
pub fn into_iter(self) -> IntoIter {
2457
IntoIter {
@@ -128,7 +161,22 @@ impl<'a> Iterator for IterMut<'a> {
128161
}
129162
}
130163

131-
// mod test {
132-
// const CASE1: &str =
133-
// "Server-Timing: metric1; dur=1.1; desc=document, metric1; dur=1.2; desc=document";
134-
// }
164+
#[cfg(test)]
165+
mod test {
166+
use super::*;
167+
use crate::headers::Headers;
168+
169+
#[test]
170+
fn smoke() -> crate::Result<()> {
171+
let mut timings = ServerTiming::new();
172+
timings.push(Entry::new("server".to_owned(), None, None)?);
173+
174+
let mut headers = Headers::new();
175+
timings.apply(&mut headers);
176+
177+
let timings = ServerTiming::from_headers(headers)?;
178+
let entry = timings.iter().next().unwrap();
179+
assert_eq!(entry.name(), "server");
180+
Ok(())
181+
}
182+
}

0 commit comments

Comments
 (0)