Skip to content

Commit faf19a6

Browse files
cursoragentlovasoa
andcommitted
feat: Add Server-Timing header to track request performance
Co-authored-by: contact <contact@ophir.dev>
1 parent 8e248b9 commit faf19a6

File tree

5 files changed

+123
-40
lines changed

5 files changed

+123
-40
lines changed

src/render.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -368,13 +368,15 @@ impl HeaderContext {
368368
Ok(PageContext::Header(self))
369369
}
370370

371-
async fn start_body(mut self, data: JsonValue) -> anyhow::Result<PageContext> {
372-
if let Some(ref timing) = self.request_context.server_timing {
373-
let header_value = timing.as_header_value();
374-
if !header_value.is_empty() {
375-
self.response.insert_header(("Server-Timing", header_value));
376-
}
371+
fn add_server_timing_header(&mut self) {
372+
let header_value = self.request_context.server_timing.as_header_value();
373+
if !header_value.is_empty() {
374+
self.response.insert_header(("Server-Timing", header_value));
377375
}
376+
}
377+
378+
async fn start_body(mut self, data: JsonValue) -> anyhow::Result<PageContext> {
379+
self.add_server_timing_header();
378380
let html_renderer =
379381
HtmlRenderContext::new(self.app_state, self.request_context, self.writer, data)
380382
.await
@@ -388,12 +390,7 @@ impl HeaderContext {
388390
}
389391

390392
pub fn close(mut self) -> HttpResponse {
391-
if let Some(ref timing) = self.request_context.server_timing {
392-
let header_value = timing.as_header_value();
393-
if !header_value.is_empty() {
394-
self.response.insert_header(("Server-Timing", header_value));
395-
}
396-
}
393+
self.add_server_timing_header();
397394
self.response.finish()
398395
}
399396
}

src/webserver/database/execute_queries.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ pub fn stream_query_results_with_conn<'a>(
5252
for res in &sql_file.statements {
5353
match res {
5454
ParsedStatement::CsvImport(csv_import) => {
55-
let connection = take_connection(&request.app_state.db, db_connection).await?;
55+
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
5656
log::debug!("Executing CSV import: {csv_import:?}");
5757
run_csv_import(connection, csv_import, request).await.with_context(|| format!("Failed to import the CSV file {:?} into the table {:?}", csv_import.uploaded_file, csv_import.table_name))?;
5858
},
5959
ParsedStatement::StmtWithParams(stmt) => {
6060
let query = bind_parameters(stmt, request, db_connection).await?;
61-
let connection = take_connection(&request.app_state.db, db_connection).await?;
61+
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
6262
log::trace!("Executing query {:?}", query.sql);
6363
let mut stream = connection.fetch_many(query);
6464
let mut error = None;
@@ -192,7 +192,7 @@ async fn execute_set_variable_query<'a>(
192192
source_file: &Path,
193193
) -> anyhow::Result<()> {
194194
let query = bind_parameters(statement, request, db_connection).await?;
195-
let connection = take_connection(&request.app_state.db, db_connection).await?;
195+
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
196196
log::debug!(
197197
"Executing query to set the {variable:?} variable: {:?}",
198198
query.sql
@@ -276,13 +276,15 @@ fn vars_and_name<'a, 'b>(
276276
async fn take_connection<'a>(
277277
db: &'a Database,
278278
conn: &'a mut DbConn,
279+
request: &RequestInfo,
279280
) -> anyhow::Result<&'a mut PoolConnection<sqlx::Any>> {
280281
if let Some(c) = conn {
281282
return Ok(c);
282283
}
283284
match db.connection.acquire().await {
284285
Ok(c) => {
285286
log::debug!("Acquired a database connection");
287+
request.server_timing.borrow_mut().record("db_conn");
286288
*conn = Some(c);
287289
Ok(conn.as_mut().unwrap())
288290
}

src/webserver/http.rs

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub struct RequestContext {
4747
pub is_embedded: bool,
4848
pub source_path: PathBuf,
4949
pub content_security_policy: ContentSecurityPolicy,
50-
pub server_timing: Option<ServerTiming>,
50+
pub server_timing: ServerTiming,
5151
}
5252

5353
async fn stream_response(stream: impl Stream<Item = DbItem>, mut renderer: AnyRenderBodyContext) {
@@ -106,16 +106,12 @@ async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
106106
let writer = ResponseWriter::new(sender);
107107
let mut head_context = HeaderContext::new(app_state, request_context, writer);
108108
let mut stream = Box::pin(database_entries);
109-
let mut first_row = true;
110109
while let Some(item) = stream.next().await {
111-
if first_row {
112-
if let Some(ref mut timing) = head_context.request_context.server_timing {
113-
timing.record("query");
114-
}
115-
first_row = false;
116-
}
117110
let page_context = match item {
118-
DbItem::Row(data) => head_context.handle_row(data).await?,
111+
DbItem::Row(data) => {
112+
head_context.request_context.server_timing.record("row");
113+
head_context.handle_row(data).await?
114+
}
119115
DbItem::FinishedQuery => {
120116
log::debug!("finished query");
121117
continue;
@@ -172,36 +168,31 @@ enum ResponseWithWriter<S> {
172168
async fn render_sql(
173169
srv_req: &mut ServiceRequest,
174170
sql_file: Arc<ParsedSqlFile>,
171+
server_timing: ServerTiming,
175172
) -> actix_web::Result<HttpResponse> {
176173
let app_state = srv_req
177174
.app_data::<web::Data<AppState>>()
178175
.ok_or_else(|| ErrorInternalServerError("no state"))?
179176
.clone()
180177
.into_inner();
181178

182-
let mut server_timing = if !app_state.config.environment.is_prod() {
183-
Some(ServerTiming::new())
184-
} else {
185-
None
186-
};
187-
188-
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state))
189-
.await
190-
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
179+
let mut req_param =
180+
extract_request_info(srv_req, Arc::clone(&app_state), server_timing.clone())
181+
.await
182+
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
191183
log::debug!("Received a request with the following parameters: {req_param:?}");
192184

193-
if let Some(ref mut timing) = server_timing {
194-
timing.record("parse");
195-
}
185+
req_param.server_timing.borrow_mut().record("parse");
196186

197187
let (resp_send, resp_recv) = tokio::sync::oneshot::channel::<HttpResponse>();
198188
let source_path: PathBuf = sql_file.source_path.clone();
199189
actix_web::rt::spawn(async move {
190+
let server_timing_for_context = req_param.server_timing.borrow().clone();
200191
let request_context = RequestContext {
201192
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
202193
source_path,
203194
content_security_policy: ContentSecurityPolicy::with_random_nonce(),
204-
server_timing,
195+
server_timing: server_timing_for_context,
205196
};
206197
let mut conn = None;
207198
let database_entries_stream =
@@ -295,13 +286,18 @@ async fn process_sql_request(
295286
sql_path: PathBuf,
296287
) -> actix_web::Result<HttpResponse> {
297288
let app_state: &web::Data<AppState> = req.app_data().expect("app_state");
289+
let mut server_timing = ServerTiming::new(!app_state.config.environment.is_prod());
290+
server_timing.record("request");
291+
298292
let sql_file = app_state
299293
.sql_file_cache
300294
.get_with_privilege(app_state, &sql_path, false)
301295
.await
302296
.with_context(|| format!("Unable to read SQL file \"{}\"", sql_path.display()))
303297
.map_err(|e| anyhow_err_to_actix(e, app_state))?;
304-
render_sql(req, sql_file).await
298+
server_timing.record("sql_file");
299+
300+
render_sql(req, sql_file, server_timing).await
305301
}
306302

307303
async fn serve_file(

src/webserver/http_request_info.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub struct RequestInfo {
4242
pub clone_depth: u8,
4343
pub raw_body: Option<Vec<u8>>,
4444
pub oidc_claims: Option<OidcClaims>,
45+
pub server_timing: std::cell::RefCell<super::server_timing::ServerTiming>,
4546
}
4647

4748
impl RequestInfo {
@@ -62,6 +63,7 @@ impl RequestInfo {
6263
clone_depth: self.clone_depth + 1,
6364
raw_body: self.raw_body.clone(),
6465
oidc_claims: self.oidc_claims.clone(),
66+
server_timing: std::cell::RefCell::new(self.server_timing.borrow().clone()),
6567
}
6668
}
6769
}
@@ -78,6 +80,7 @@ impl Clone for RequestInfo {
7880
pub(crate) async fn extract_request_info(
7981
req: &mut ServiceRequest,
8082
app_state: Arc<AppState>,
83+
server_timing: super::server_timing::ServerTiming,
8184
) -> anyhow::Result<RequestInfo> {
8285
let (http_req, payload) = req.parts_mut();
8386
let method = http_req.method().clone();
@@ -123,6 +126,7 @@ pub(crate) async fn extract_request_info(
123126
clone_depth: 0,
124127
raw_body,
125128
oidc_claims,
129+
server_timing: std::cell::RefCell::new(server_timing),
126130
})
127131
}
128132

src/webserver/server_timing.rs

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::time::Instant;
22

33
#[derive(Debug, Clone)]
44
pub struct ServerTiming {
5+
enabled: bool,
56
start: Instant,
67
events: Vec<TimingEvent>,
78
}
@@ -14,20 +15,32 @@ struct TimingEvent {
1415

1516
impl ServerTiming {
1617
#[must_use]
17-
pub fn new() -> Self {
18+
pub fn new(enabled: bool) -> Self {
1819
Self {
20+
enabled,
1921
start: Instant::now(),
2022
events: Vec::new(),
2123
}
2224
}
2325

2426
pub fn record(&mut self, name: &'static str) {
27+
if !self.enabled {
28+
return;
29+
}
2530
let duration_ms = self.start.elapsed().as_secs_f64() * 1000.0;
2631
self.events.push(TimingEvent { name, duration_ms });
2732
}
2833

34+
#[must_use]
35+
pub fn is_enabled(&self) -> bool {
36+
self.enabled
37+
}
38+
2939
#[must_use]
3040
pub fn as_header_value(&self) -> String {
41+
if !self.enabled {
42+
return String::new();
43+
}
3144
self.events
3245
.iter()
3346
.map(|event| format!("{};dur={:.2}", event.name, event.duration_ms))
@@ -38,6 +51,77 @@ impl ServerTiming {
3851

3952
impl Default for ServerTiming {
4053
fn default() -> Self {
41-
Self::new()
54+
Self::new(false)
55+
}
56+
}
57+
58+
#[cfg(test)]
59+
mod tests {
60+
use super::*;
61+
use std::thread;
62+
use std::time::Duration;
63+
64+
#[test]
65+
fn test_disabled_timing() {
66+
let mut timing = ServerTiming::new(false);
67+
assert!(!timing.is_enabled());
68+
timing.record("event1");
69+
timing.record("event2");
70+
assert_eq!(timing.as_header_value(), "");
71+
}
72+
73+
#[test]
74+
fn test_enabled_timing() {
75+
let mut timing = ServerTiming::new(true);
76+
assert!(timing.is_enabled());
77+
timing.record("event1");
78+
thread::sleep(Duration::from_millis(10));
79+
timing.record("event2");
80+
let header = timing.as_header_value();
81+
assert!(header.contains("event1;dur="));
82+
assert!(header.contains("event2;dur="));
83+
assert!(header.contains(", "));
84+
}
85+
86+
#[test]
87+
fn test_timing_values_increase() {
88+
let mut timing = ServerTiming::new(true);
89+
timing.record("first");
90+
thread::sleep(Duration::from_millis(5));
91+
timing.record("second");
92+
assert_eq!(timing.events.len(), 2);
93+
assert!(timing.events[1].duration_ms > timing.events[0].duration_ms);
94+
}
95+
96+
#[test]
97+
fn test_default_is_disabled() {
98+
let timing = ServerTiming::default();
99+
assert!(!timing.is_enabled());
100+
}
101+
102+
#[test]
103+
fn test_header_format() {
104+
let mut timing = ServerTiming::new(true);
105+
timing.events.push(TimingEvent {
106+
name: "test",
107+
duration_ms: 123.456,
108+
});
109+
let header = timing.as_header_value();
110+
assert_eq!(header, "test;dur=123.46");
111+
}
112+
113+
#[test]
114+
fn test_multiple_events_format() {
115+
let mut timing = ServerTiming::new(true);
116+
timing.events.push(TimingEvent {
117+
name: "first",
118+
duration_ms: 10.5,
119+
});
120+
timing.events.push(TimingEvent {
121+
name: "second",
122+
duration_ms: 25.75,
123+
});
124+
let header = timing.as_header_value();
125+
assert_eq!(header, "first;dur=10.50, second;dur=25.75");
42126
}
43127
}

0 commit comments

Comments
 (0)