use std::time::Duration;
const REDACTED_HEADERS: [&str; 6] = [
"authorization",
"proxy-authorization",
"x-api-key",
"api-key",
"cookie",
"set-cookie",
];
fn is_redacted(name: &str) -> bool {
REDACTED_HEADERS.contains(&name.to_ascii_lowercase().as_str())
}
pub(crate) fn redact_headers(headers: &reqwest::header::HeaderMap) -> Vec<(String, String)> {
headers
.iter()
.map(|(name, value)| {
let name = name.as_str().to_owned();
let value = if is_redacted(&name) || value_is_secret(&name) {
"***".to_owned()
} else {
String::from_utf8_lossy(value.as_bytes()).into_owned()
};
(name, value)
})
.collect()
}
fn value_is_secret(name: &str) -> bool {
let lowered = name.to_ascii_lowercase();
lowered.contains("token") || lowered.contains("secret")
}
pub(crate) fn log_attempt_info(
method: &str,
path: &str,
status: u16,
duration: Duration,
request_id: Option<&str>,
) {
let duration_ms = duration.as_millis();
match request_id {
Some(request_id) => tracing::info!(
method,
path,
status,
duration_ms,
request_id,
"attempt finished"
),
None => tracing::info!(method, path, status, duration_ms, "attempt finished"),
}
}
pub(crate) fn log_attempt_error(method: &str, path: &str, message: &str) {
tracing::info!(method, path, error = %message, "attempt failed without a response");
}
pub(crate) fn log_retry_scheduled(
method: &str,
path: &str,
delay: Duration,
attempt: u32,
reason: &str,
) {
let delay_ms = delay.as_millis();
tracing::info!(method, path, attempt, delay_ms, reason, "retrying");
}
pub(crate) fn log_request_debug(
method: &str,
path: &str,
headers: &reqwest::header::HeaderMap,
body: Option<&serde_json::Value>,
) {
let headers = redact_headers(headers);
match body {
Some(body) => tracing::debug!(method, path, ?headers, %body, "request"),
None => tracing::debug!(method, path, ?headers, "request"),
}
}
pub(crate) fn log_response_debug(
method: &str,
path: &str,
status: u16,
headers: &reqwest::header::HeaderMap,
body: &str,
) {
let headers = redact_headers(headers);
tracing::debug!(method, path, status, ?headers, body, "response");
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
#[test]
fn redacts_secret_headers() {
let mut headers = HeaderMap::new();
for (name, value) in [
("Authorization", "Bearer sk-secret"),
("Proxy-Authorization", "Basic xyz"),
("X-Api-Key", "sk-secret"),
("Api-Key", "sk-secret"),
("Cookie", "session=1"),
("Set-Cookie", "session=1"),
("X-Token-Thing", "abc"),
("X-Kinda-Secret", "abc"),
("User-Agent", "typesafe-client-rust/0.1.0"),
("Accept", "application/json"),
] {
headers.insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(value).unwrap(),
);
}
let redacted = redact_headers(&headers);
let get = |name: &str| {
redacted
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.as_str())
.unwrap()
};
assert_eq!(get("authorization"), "***");
assert_eq!(get("proxy-authorization"), "***");
assert_eq!(get("x-api-key"), "***");
assert_eq!(get("api-key"), "***");
assert_eq!(get("cookie"), "***");
assert_eq!(get("set-cookie"), "***");
assert_eq!(get("x-token-thing"), "***");
assert_eq!(get("x-kinda-secret"), "***");
assert_eq!(get("user-agent"), "typesafe-client-rust/0.1.0");
assert_eq!(get("accept"), "application/json");
}
}