Add RADICLE_HTTPD_LOG_FORMAT runtime switch

- Select text (default) or json log format at startup, not at
  compile time
- Emit the access log as structured fields (client, method, uri,
  version, status, latency_ms, bytes) in json
- Log status and size as plain text, since tracing-subscriber
  escapes ANSI codes
- Remove the logfmt feature and tracing-logfmt dependency
- Drop the radicle-term dependency
- Split format setup from lib.rs into logger.rs

Co-Authored-By: Daniel Norman <daniel@norman.life>
This commit is contained in:
Yorgos Saslis
2026-09-24 14:55:09 +02:00
committed by Daniel Norman
co-authored by Daniel Norman
parent 45bd0a3787
commit b9ba3b7917
6 changed files with 302 additions and 206 deletions
Generated
+9 -138
View File
@@ -494,7 +494,7 @@ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
"libc", "libc",
"shlex 2.0.1", "shlex",
] ]
[[package]] [[package]]
@@ -1804,7 +1804,6 @@ dependencies = [
"crossterm", "crossterm",
"dyn-clone", "dyn-clone",
"fuzzy-matcher", "fuzzy-matcher",
"tempfile",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
] ]
@@ -2778,7 +2777,6 @@ dependencies = [
"radicle-job", "radicle-job",
"radicle-search", "radicle-search",
"radicle-surf", "radicle-surf",
"radicle-term",
"serde", "serde",
"serde_json", "serde_json",
"tempfile", "tempfile",
@@ -2788,7 +2786,6 @@ dependencies = [
"tower", "tower",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-logfmt",
"tracing-subscriber", "tracing-subscriber",
"url", "url",
"uuid", "uuid",
@@ -2852,17 +2849,6 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "radicle-signals"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "137929529ba88f1f82cce97d29c14bd26a234a747db23fc7f6643796a37cc956"
dependencies = [
"libc",
"signals_receipts",
"windows 0.62.2",
]
[[package]] [[package]]
name = "radicle-surf" name = "radicle-surf"
version = "0.29.0" version = "0.29.0"
@@ -2881,26 +2867,6 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "radicle-term"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f7059000b2441b45d30d0584d26c22dfd97907c22a329d281fb427002f9458e"
dependencies = [
"anstyle-query",
"crossterm",
"indicatif",
"inquire",
"libc",
"radicle-signals",
"shlex 1.3.0",
"thiserror 2.0.19",
"unicode-display-width",
"unicode-segmentation",
"winsplit",
"zeroize",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.7" version = "0.8.7"
@@ -3228,18 +3194,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "sem_safe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be9665e92f9b596cf2b20a8c0f55126cfee3514307740a859f1b25bc929e4947"
dependencies = [
"base64 0.22.1",
"errno",
"getrandom 0.2.17",
"libc",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.28" version = "1.0.28"
@@ -3387,12 +3341,6 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "2.0.1" version = "2.0.1"
@@ -3430,18 +3378,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "signals_receipts"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8400878c2b53f6e3e224fcbdf20d87743a5c6547129f0a6a227f8282b6c6910b"
dependencies = [
"cfg-if",
"errno",
"libc",
"sem_safe",
]
[[package]] [[package]]
name = "signature" name = "signature"
version = "1.6.4" version = "1.6.4"
@@ -3999,15 +3935,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tracing-logfmt" name = "tracing-serde"
version = "0.3.7" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a250055a3518b5efba928a18ffac8d32d42ea607a9affff4532144cd5b2e378e" checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [ dependencies = [
"time", "serde",
"tracing",
"tracing-core", "tracing-core",
"tracing-subscriber",
] ]
[[package]] [[package]]
@@ -4020,10 +3954,13 @@ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"once_cell", "once_cell",
"regex-automata", "regex-automata",
"serde",
"serde_json",
"sharded-slab", "sharded-slab",
"thread_local", "thread_local",
"tracing", "tracing",
"tracing-core", "tracing-core",
"tracing-serde",
] ]
[[package]] [[package]]
@@ -4061,15 +3998,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-display-width"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a43273b656140aa2bb8e65351fe87c255f0eca706b2538a9bd4a590a3490bf3"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
@@ -4358,27 +4286,6 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core 0.62.2",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core 0.62.2",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.58.0" version = "0.58.0"
@@ -4405,17 +4312,6 @@ dependencies = [
"windows-strings 0.5.1", "windows-strings 0.5.1",
] ]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.62.2",
"windows-link",
"windows-threading",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.58.0" version = "0.58.0"
@@ -4466,16 +4362,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.62.2",
"windows-link",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.2.0" version = "0.2.0"
@@ -4547,15 +4433,6 @@ dependencies = [
"windows_x86_64_msvc", "windows_x86_64_msvc",
] ]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -4614,15 +4491,9 @@ dependencies = [
"log", "log",
"rand 0.8.7", "rand 0.8.7",
"sync-ptr", "sync-ptr",
"windows 0.58.0", "windows",
] ]
[[package]]
name = "winsplit"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab703352da6a72f35c39a533526393725640575bb211f61987a2748323ad956"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.57.1" version = "0.57.1"
+1 -6
View File
@@ -14,9 +14,6 @@ default = ["artifacts"]
artifacts = [ artifacts = [
"dep:radicle-artifact", "dep:radicle-artifact",
] ]
logfmt = [
"tracing-logfmt",
]
[[bin]] [[bin]]
name = "radicle-httpd" name = "radicle-httpd"
@@ -40,7 +37,6 @@ radicle-artifact = { version = "0.18", optional = true }
radicle-job = { version = "0.6.0" } radicle-job = { version = "0.6.0" }
radicle-search = { path = "../radicle-search" } radicle-search = { path = "../radicle-search" }
radicle-surf = { workspace = true, features = ["serde"] } radicle-surf = { workspace = true, features = ["serde"] }
radicle-term = { version = "0.19.1", default-features = false }
serde.workspace = true serde.workspace = true
serde_json = { workspace = true, features = ["preserve_order"] } serde_json = { workspace = true, features = ["preserve_order"] }
thiserror.workspace = true thiserror.workspace = true
@@ -48,8 +44,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "
tokio-util = { version = "0.7.18", default-features = false, features = ["io"] } tokio-util = { version = "0.7.18", default-features = false, features = ["io"] }
tower-http = { version = "0.6.6", default-features = false, features = ["trace", "cors", "set-header", "decompression-full"] } tower-http = { version = "0.6.6", default-features = false, features = ["trace", "cors", "set-header", "decompression-full"] }
tracing.workspace = true tracing.workspace = true
tracing-logfmt = { version = "0.3.5", optional = true } tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
url = { version = "2.5.4", features = ["serde"] } url = { version = "2.5.4", features = ["serde"] }
uuid = { version = "1.13.1", features = ["serde"] } uuid = { version = "1.13.1", features = ["serde"] }
+7 -51
View File
@@ -32,12 +32,13 @@ use radicle::identity::RepoId;
use radicle::Profile; use radicle::Profile;
use crate::api::RADICLE_VERSION; use crate::api::RADICLE_VERSION;
use crate::tracing_extra::{tracing_middleware, ColoredStatus, Paint, RequestId, TracingInfo}; use crate::tracing_extra::{log_response, tracing_middleware, RequestId, TracingInfo};
mod api; mod api;
mod axum_extra; mod axum_extra;
mod cache; mod cache;
mod git; mod git;
pub mod logger;
mod raw; mod raw;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
@@ -142,27 +143,12 @@ pub async fn run(options: Options) -> anyhow::Result<()> {
.on_response( .on_response(
|response: &hyper::Response<Body>, latency: Duration, _span: &Span| { |response: &hyper::Response<Body>, latency: Duration, _span: &Span| {
if let Some(info) = response.extensions().get::<TracingInfo>() { if let Some(info) = response.extensions().get::<TracingInfo>() {
tracing::info!( log_response(
"{} \"{} {} {:?}\" {} {:?} {}", logger::format(),
match info.connect_info.0 { info,
DualAddr::Tcp(c) => c.to_string(), response.status(),
#[cfg(unix)]
DualAddr::Uds(_) => "unix-socket".into()
},
info.method,
info.uri,
info.version,
ColoredStatus(response.status()),
latency, latency,
Paint::dim( response.body().size_hint().exact(),
response
.body()
.size_hint()
.exact()
.map(|n| n.to_string())
.unwrap_or("0".to_string())
.into()
),
); );
} else { } else {
tracing::info!("Processed"); tracing::info!("Processed");
@@ -236,36 +222,6 @@ async fn root_index_handler() -> impl IntoResponse {
Json(response) Json(response)
} }
pub mod logger {
use tracing::dispatcher::Dispatch;
pub fn init() -> Result<(), tracing::subscriber::SetGlobalDefaultError> {
tracing::dispatcher::set_global_default(Dispatch::new(subscriber()))
}
#[cfg(feature = "logfmt")]
pub fn subscriber() -> impl tracing::Subscriber {
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::EnvFilter;
tracing_subscriber::Registry::default()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.with(tracing_logfmt::layer())
}
#[cfg(not(feature = "logfmt"))]
pub fn subscriber() -> impl tracing::Subscriber {
use tracing_subscriber::EnvFilter;
tracing_subscriber::FmtSubscriber::builder()
.with_target(false)
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.finish()
}
}
#[cfg(test)] #[cfg(test)]
mod routes { mod routes {
use std::collections::HashMap; use std::collections::HashMap;
+111
View File
@@ -0,0 +1,111 @@
use std::sync::OnceLock;
use tracing::dispatcher::Dispatch;
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::layer::{Layered, SubscriberExt as _};
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::{EnvFilter, Layer, Registry};
pub const LOG_FORMAT_VAR: &str = "RADICLE_HTTPD_LOG_FORMAT";
type Filtered = Layered<EnvFilter, Registry>;
type BoxedLayer = Box<dyn Layer<Filtered> + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogFormat {
#[default]
Text,
Json,
}
impl LogFormat {
pub fn parse(value: Option<&str>) -> LogFormat {
match value.map(str::trim).map(str::to_lowercase).as_deref() {
None | Some("") | Some("text") => LogFormat::Text,
Some("json") => LogFormat::Json,
Some(other) => {
eprintln!("{LOG_FORMAT_VAR}: unknown format {other:?}, falling back to \"text\"");
LogFormat::Text
}
}
}
pub fn from_env() -> LogFormat {
LogFormat::parse(std::env::var(LOG_FORMAT_VAR).ok().as_deref())
}
}
static FORMAT: OnceLock<LogFormat> = OnceLock::new();
pub fn format() -> LogFormat {
*FORMAT.get_or_init(LogFormat::from_env)
}
pub fn init() -> Result<(), tracing::subscriber::SetGlobalDefaultError> {
tracing::dispatcher::set_global_default(Dispatch::new(subscriber()))
}
pub fn subscriber() -> impl tracing::Subscriber {
let filtered = Registry::default()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")));
let layer: BoxedLayer = match format() {
LogFormat::Text => Box::new(text_layer(std::io::stdout)),
LogFormat::Json => Box::new(json_layer(std::io::stdout)),
};
filtered.with(layer)
}
pub fn text_layer<S, W>(writer: W) -> impl Layer<S>
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
{
tracing_subscriber::fmt::layer()
.with_target(false)
.with_writer(writer)
}
pub fn json_layer<S, W>(writer: W) -> impl Layer<S>
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
{
tracing_subscriber::fmt::layer()
.json()
.flatten_event(true)
.with_current_span(true)
.with_span_list(false)
.with_writer(writer)
}
#[cfg(test)]
mod tests {
use super::LogFormat;
#[test]
fn unset_variable_selects_text() {
assert_eq!(LogFormat::parse(None), LogFormat::Text);
}
#[test]
fn json_selects_json() {
assert_eq!(LogFormat::parse(Some("json")), LogFormat::Json);
}
#[test]
fn value_is_case_and_whitespace_insensitive() {
assert_eq!(LogFormat::parse(Some(" JSON \n")), LogFormat::Json);
}
#[test]
fn unrecognised_value_falls_back_to_text() {
assert_eq!(LogFormat::parse(Some("yaml")), LogFormat::Text);
}
#[test]
fn empty_value_selects_text() {
assert_eq!(LogFormat::parse(Some("")), LogFormat::Text);
}
}
+2
View File
@@ -35,6 +35,8 @@ Environment
RUST_LOG Set the log level (e.g. RUST_LOG=warn, or RUST_LOG Set the log level (e.g. RUST_LOG=warn, or
RUST_LOG=radicle_httpd=debug). Defaults to "info". RUST_LOG=radicle_httpd=debug). Defaults to "info".
RADICLE_HTTPD_LOG_FORMAT Log rendering: text (default) or json.
Unrecognised values fall back to "text".
"#; "#;
#[tokio::main] #[tokio::main]
+172 -11
View File
@@ -1,6 +1,6 @@
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use axum::body::Body; use axum::body::Body;
use axum::extract::ConnectInfo; use axum::extract::ConnectInfo;
@@ -11,7 +11,7 @@ use axum::Extension;
use axum_listener::DualAddr; use axum_listener::DualAddr;
use hyper::{Method, StatusCode, Uri, Version}; use hyper::{Method, StatusCode, Uri, Version};
pub use radicle_term::ansi::Paint; use crate::logger::LogFormat;
#[derive(Clone)] #[derive(Clone)]
pub struct RequestId(Arc<AtomicU64>); pub struct RequestId(Arc<AtomicU64>);
@@ -34,16 +34,40 @@ pub struct TracingInfo {
pub uri: Uri, pub uri: Uri,
} }
pub struct ColoredStatus(pub StatusCode); pub fn log_response(
format: LogFormat,
info: &TracingInfo,
status: StatusCode,
latency: Duration,
bytes: Option<u64>,
) {
let client = match info.connect_info.0 {
DualAddr::Tcp(addr) => addr.to_string(),
#[cfg(unix)]
DualAddr::Uds(_) => "unix-socket".to_string(),
};
impl fmt::Display for ColoredStatus { match format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { LogFormat::Json => tracing::info!(
match self.0.as_u16() { client = %client,
200..=299 => write!(f, "{}", Paint::green(self.0)), method = %info.method,
300..=399 => write!(f, "{}", Paint::blue(self.0)), uri = %info.uri,
400..=499 => write!(f, "{}", Paint::red(self.0)), version = ?info.version,
_ => write!(f, "{}", Paint::yellow(self.0)), status = status.as_u16(),
} latency_ms = latency.as_micros() as f64 / 1000.0,
bytes = bytes.unwrap_or(0),
"request completed",
),
LogFormat::Text => tracing::info!(
"{} \"{} {} {:?}\" {} {:?} {}",
client,
info.method,
info.uri,
info.version,
status,
latency,
bytes.unwrap_or(0),
),
} }
} }
@@ -69,3 +93,140 @@ pub async fn tracing_middleware(request: Request<Body>, next: Next) -> impl Into
(Extension(tracing_info), response) (Extension(tracing_info), response)
} }
#[cfg(test)]
mod tests {
use std::io;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::Layer;
use super::*;
use crate::logger::{json_layer, text_layer, LogFormat};
#[derive(Clone, Default)]
struct Buffer(Arc<Mutex<Vec<u8>>>);
impl Buffer {
fn contents(&self) -> String {
String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
}
}
impl io::Write for Buffer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for Buffer {
type Writer = Buffer;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
fn info() -> TracingInfo {
TracingInfo {
connect_info: ConnectInfo(DualAddr::Tcp(SocketAddr::from(([1, 2, 3, 4], 5678)))),
method: Method::GET,
version: hyper::Version::HTTP_11,
uri: "/api/v1/node".parse().unwrap(),
}
}
fn capture<L>(layer: L, f: impl FnOnce())
where
L: Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
{
let subscriber = tracing_subscriber::Registry::default().with(layer);
tracing::subscriber::with_default(subscriber, f);
}
#[test]
fn json_format_logs_the_response_as_fields() {
let buffer = Buffer::default();
capture(json_layer(buffer.clone()), || {
log_response(
LogFormat::Json,
&info(),
StatusCode::NOT_FOUND,
Duration::from_millis(7),
Some(1024),
);
});
let record: serde_json::Value = serde_json::from_str(buffer.contents().trim()).unwrap();
assert_eq!(record["client"], "1.2.3.4:5678");
assert_eq!(record["method"], "GET");
assert_eq!(record["uri"], "/api/v1/node");
assert_eq!(record["version"], "HTTP/1.1");
assert_eq!(record["status"], 404);
assert_eq!(record["latency_ms"], 7.0);
assert_eq!(record["bytes"], 1024);
}
#[test]
fn json_format_reports_a_missing_body_size_as_zero_bytes() {
let buffer = Buffer::default();
capture(json_layer(buffer.clone()), || {
log_response(
LogFormat::Json,
&info(),
StatusCode::OK,
Duration::from_millis(1),
None,
);
});
let record: serde_json::Value = serde_json::from_str(buffer.contents().trim()).unwrap();
assert_eq!(record["bytes"], 0);
}
#[test]
fn text_format_keeps_the_single_line_layout() {
let buffer = Buffer::default();
capture(text_layer(buffer.clone()), || {
log_response(
LogFormat::Text,
&info(),
StatusCode::NOT_FOUND,
Duration::from_millis(12),
Some(1024),
);
});
let out = buffer.contents();
assert!(
out.contains("1.2.3.4:5678 \"GET /api/v1/node HTTP/1.1\" 404 Not Found 12ms 1024"),
"got: {out:?}"
);
}
#[test]
fn text_format_adds_no_structured_fields() {
let buffer = Buffer::default();
capture(text_layer(buffer.clone()), || {
log_response(
LogFormat::Text,
&info(),
StatusCode::OK,
Duration::from_millis(1),
Some(7),
);
});
let out = buffer.contents();
assert!(!out.contains("status="), "got: {out:?}");
assert!(!out.contains("latency_ms="), "got: {out:?}");
}
}