use crate::utils::get_env_with_prefix;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RequestLoggingConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_include_headers")]
pub include_headers: bool,
#[serde(default)]
pub include_response_headers: bool,
#[serde(default = "default_body_preview_size")]
pub body_preview_size: usize,
#[serde(default = "default_success_level")]
pub success_level: LogLevel,
#[serde(default = "default_client_error_level")]
pub client_error_level: LogLevel,
#[serde(default = "default_server_error_level")]
pub server_error_level: LogLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl Default for RequestLoggingConfig {
fn default() -> Self {
Self {
enabled: default_enabled(),
include_headers: default_include_headers(),
include_response_headers: false,
body_preview_size: default_body_preview_size(),
success_level: LogLevel::Info,
client_error_level: LogLevel::Warn,
server_error_level: LogLevel::Error,
}
}
}
impl RequestLoggingConfig {
pub fn builder() -> RequestLoggingConfigBuilder {
RequestLoggingConfigBuilder::new()
}
pub fn from_env() -> Self {
let mut config = Self::default();
if let Some(enabled) = get_env_with_prefix("REQUEST_LOGGING_ENABLED") {
config.enabled = parse_bool_with_default(&enabled, config.enabled);
}
if let Some(include_headers) = get_env_with_prefix("REQUEST_LOGGING_INCLUDE_HEADERS") {
config.include_headers =
parse_bool_with_default(&include_headers, config.include_headers);
}
if let Some(include_response) =
get_env_with_prefix("REQUEST_LOGGING_INCLUDE_RESPONSE_HEADERS")
{
config.include_response_headers =
parse_bool_with_default(&include_response, config.include_response_headers);
}
if let Some(preview_size) = get_env_with_prefix("REQUEST_LOGGING_BODY_PREVIEW_SIZE") {
if let Ok(size) = preview_size.parse::<usize>() {
config.body_preview_size = size.min(MAX_BODY_PREVIEW_SIZE);
}
}
if let Some(level) = get_env_with_prefix("REQUEST_LOGGING_SUCCESS_LEVEL") {
config.success_level = parse_log_level(&level);
}
if let Some(level) = get_env_with_prefix("REQUEST_LOGGING_CLIENT_ERROR_LEVEL") {
config.client_error_level = parse_log_level(&level);
}
if let Some(level) = get_env_with_prefix("REQUEST_LOGGING_SERVER_ERROR_LEVEL") {
config.server_error_level = parse_log_level(&level);
}
config
}
}
fn parse_bool_with_default(value: &str, default: bool) -> bool {
value.parse().unwrap_or(default)
}
fn parse_log_level(s: &str) -> LogLevel {
match s.to_lowercase().as_str() {
"trace" => LogLevel::Trace,
"debug" => LogLevel::Debug,
"info" => LogLevel::Info,
"warn" => LogLevel::Warn,
"error" => LogLevel::Error,
_ => LogLevel::Info,
}
}
#[must_use = "builder does nothing until you call build()"]
pub struct RequestLoggingConfigBuilder {
config: RequestLoggingConfig,
}
impl RequestLoggingConfigBuilder {
pub fn new() -> Self {
Self {
config: RequestLoggingConfig::default(),
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.config.enabled = enabled;
self
}
pub fn include_headers(mut self, include: bool) -> Self {
self.config.include_headers = include;
self
}
pub fn include_response_headers(mut self, include: bool) -> Self {
self.config.include_response_headers = include;
self
}
pub fn body_preview_size(mut self, size: usize) -> Self {
self.config.body_preview_size = size;
self
}
pub fn success_level(mut self, level: LogLevel) -> Self {
self.config.success_level = level;
self
}
pub fn client_error_level(mut self, level: LogLevel) -> Self {
self.config.client_error_level = level;
self
}
pub fn server_error_level(mut self, level: LogLevel) -> Self {
self.config.server_error_level = level;
self
}
pub fn build(self) -> RequestLoggingConfig {
self.config
}
}
impl Default for RequestLoggingConfigBuilder {
fn default() -> Self {
Self::new()
}
}
fn default_enabled() -> bool {
true
}
fn default_include_headers() -> bool {
false
}
fn default_body_preview_size() -> usize {
0
}
const MAX_BODY_PREVIEW_SIZE: usize = 1024 * 1024;
fn default_success_level() -> LogLevel {
LogLevel::Info
}
fn default_client_error_level() -> LogLevel {
LogLevel::Warn
}
fn default_server_error_level() -> LogLevel {
LogLevel::Error
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = RequestLoggingConfig::default();
assert!(config.enabled);
assert!(!config.include_headers);
assert_eq!(config.body_preview_size, 0);
assert_eq!(config.success_level, LogLevel::Info);
}
#[test]
fn test_builder() {
let config = RequestLoggingConfig::builder()
.include_headers(true)
.body_preview_size(512)
.success_level(LogLevel::Debug)
.build();
assert!(config.include_headers);
assert_eq!(config.body_preview_size, 512);
assert_eq!(config.success_level, LogLevel::Debug);
}
#[test]
fn test_from_env_invalid_bool_falls_back_to_default() {
unsafe {
std::env::set_var("TIDEWAY_REQUEST_LOGGING_ENABLED", "yes");
std::env::set_var("TIDEWAY_REQUEST_LOGGING_INCLUDE_HEADERS", "nope");
}
let config = RequestLoggingConfig::from_env();
assert!(config.enabled);
assert!(!config.include_headers);
unsafe {
std::env::remove_var("TIDEWAY_REQUEST_LOGGING_ENABLED");
std::env::remove_var("TIDEWAY_REQUEST_LOGGING_INCLUDE_HEADERS");
}
}
#[test]
fn test_from_env_body_preview_size_clamps_to_maximum() {
let huge = MAX_BODY_PREVIEW_SIZE.saturating_add(1).to_string();
unsafe {
std::env::set_var("TIDEWAY_REQUEST_LOGGING_BODY_PREVIEW_SIZE", &huge);
}
let config = RequestLoggingConfig::from_env();
assert_eq!(config.body_preview_size, MAX_BODY_PREVIEW_SIZE);
unsafe {
std::env::remove_var("TIDEWAY_REQUEST_LOGGING_BODY_PREVIEW_SIZE");
}
}
}