#[cfg(test)]
mod logging_tests {
use super::super::*;
use crate::logging::config::LoggingConfig;
use crate::logging::middleware::{LoggingMiddleware, ResponseFutureExt};
use crate::logging::structured::{LogFormat, LoggerConfig, RequestInfo, generate_trace_id};
use crate::logging::test_logger;
use bytes::Bytes;
use http_body_util::Empty;
use hyper::{Method, Request, Response};
use log::LevelFilter;
use serial_test::serial;
use std::collections::HashMap;
use std::future::Future;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::pin::Pin;
use std::sync::atomic::Ordering;
use std::task::{Context, Poll};
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
fn reset_logging_state() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
}
#[test]
#[serial]
fn test_init_with_config_structured_logging() {
let config = LoggingConfig {
structured: true,
level: "debug".to_string(),
format: "json".to_string(),
include_location: true,
include_thread_id: true,
include_trace_id: true,
propagate_trace_id: true,
trace_id_header: "X-Trace-ID".to_string(),
static_fields: std::collections::HashMap::new(),
};
init_with_config(LevelFilter::Debug, &config);
}
#[test]
#[serial]
fn test_init_with_config_env_logger() {
reset_logging_state();
let config = LoggingConfig {
structured: false,
level: "info".to_string(),
format: "terminal".to_string(),
include_location: false,
include_thread_id: false,
include_trace_id: false,
propagate_trace_id: false,
trace_id_header: "X-Trace-ID".to_string(),
static_fields: std::collections::HashMap::new(),
};
init_with_config(LevelFilter::Info, &config);
let current_level = log::max_level();
assert!(
current_level >= LevelFilter::Info,
"Expected log level to be at least Info, but got {current_level:?}"
);
}
#[test]
fn test_init_with_config_different_levels() {
let levels = vec![
LevelFilter::Error,
LevelFilter::Warn,
LevelFilter::Info,
LevelFilter::Debug,
LevelFilter::Trace,
];
for level in levels {
let config = LoggingConfig {
structured: false,
level: level.to_string(),
format: "terminal".to_string(),
include_location: true,
include_thread_id: true,
include_trace_id: true,
propagate_trace_id: true,
trace_id_header: "X-Trace-ID".to_string(),
static_fields: std::collections::HashMap::new(),
};
init_with_config(level, &config);
}
}
#[test]
#[serial]
fn test_is_structured_logging() {
let initial_state = is_structured_logging();
assert!(matches!(initial_state, true | false));
USING_STRUCTURED.store(true, Ordering::SeqCst);
assert!(is_structured_logging());
USING_STRUCTURED.store(false, Ordering::SeqCst);
assert!(!is_structured_logging());
}
#[test]
#[serial]
fn test_log_error_structured() {
test_logger::init_test_logger();
USING_STRUCTURED.store(true, Ordering::SeqCst);
let test_error = "Test error message";
let context = "TestContext";
let returned_error = log_error(context, test_error);
assert_eq!(returned_error, test_error);
}
#[test]
#[serial]
fn test_log_error_non_structured() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
let test_error = "Test error message";
let context = "TestContext";
let returned_error = log_error(context, test_error);
assert_eq!(returned_error, test_error);
}
#[test]
#[serial]
fn test_log_warning_structured() {
test_logger::init_test_logger();
USING_STRUCTURED.store(true, Ordering::SeqCst);
let test_warning = "Test warning message";
let context = "TestContext";
log_warning(context, test_warning);
}
#[test]
#[serial]
fn test_log_warning_non_structured() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
let test_warning = "Test warning message";
let context = "TestContext";
log_warning(context, test_warning);
}
#[test]
#[serial]
fn test_log_debug_structured() {
test_logger::init_test_logger();
USING_STRUCTURED.store(true, Ordering::SeqCst);
let test_message = "Test debug message";
let context = "TestContext";
log_debug(context, test_message);
}
#[test]
#[serial]
fn test_log_debug_non_structured() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
let test_message = "Test debug message";
let context = "TestContext";
log_debug(context, test_message);
}
#[test]
#[serial]
fn test_log_trace_structured() {
test_logger::init_test_logger();
USING_STRUCTURED.store(true, Ordering::SeqCst);
let test_message = "Test trace message";
let context = "TestContext";
log_trace(context, test_message);
}
#[test]
#[serial]
fn test_log_trace_non_structured() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
let test_message = "Test trace message";
let context = "TestContext";
log_trace(context, test_message);
}
#[test]
#[serial]
fn test_log_info_structured() {
test_logger::init_test_logger();
USING_STRUCTURED.store(true, Ordering::SeqCst);
let test_message = "Test info message";
let context = "TestContext";
log_info(context, test_message);
}
#[test]
#[serial]
fn test_log_info_non_structured() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
let test_message = "Test info message";
let context = "TestContext";
log_info(context, test_message);
}
#[test]
#[serial]
fn test_log_with_context_all_levels_structured() {
test_logger::init_test_logger();
USING_STRUCTURED.store(true, Ordering::SeqCst);
let message = "Test message";
let context = "TestContext";
let fields = vec![
("field1", "value1".to_string()),
("field2", "value2".to_string()),
];
let levels = vec![
log::Level::Error,
log::Level::Warn,
log::Level::Info,
log::Level::Debug,
log::Level::Trace,
];
for level in levels {
log_with_context(level, message, context, &fields);
}
}
#[test]
#[serial]
fn test_log_with_context_all_levels_non_structured() {
let _ = env_logger::builder().is_test(true).try_init();
let drain = slog::Discard;
let logger = slog::Logger::root(drain, slog::o!());
USING_STRUCTURED.store(false, Ordering::SeqCst);
let message = "Test message";
let context = "TestContext";
let fields = vec![
("field1", "value1".to_string()),
("field2", "value2".to_string()),
];
let levels = vec![
log::Level::Error,
log::Level::Warn,
log::Level::Info,
log::Level::Debug,
log::Level::Trace,
];
slog_scope::scope(&logger, || {
for level in levels {
log_with_context(level, message, context, &fields);
}
});
}
#[test]
#[serial]
fn test_log_with_context_empty_fields() {
test_logger::init_test_logger();
USING_STRUCTURED.store(true, Ordering::SeqCst);
let message = "Test message";
let context = "TestContext";
let empty_fields: Vec<(&'static str, String)> = vec![];
log_with_context(log::Level::Info, message, context, &empty_fields);
}
#[test]
fn test_add_fields_to_logger() {
let drain = slog::Discard;
let logger = slog::Logger::root(drain, slog::o!());
let fields = vec![
("test_field1", "value1".to_string()),
("test_field2", "value2".to_string()),
("test_field3", "value3".to_string()),
];
let result_logger = add_fields_to_logger(logger, &fields);
assert!(!std::ptr::addr_of!(result_logger).is_null());
}
#[test]
fn test_add_fields_to_logger_empty_fields() {
let drain = slog::Discard;
let logger = slog::Logger::root(drain, slog::o!());
let empty_fields: Vec<(&'static str, String)> = vec![];
let result_logger = add_fields_to_logger(logger, &empty_fields);
assert!(!std::ptr::addr_of!(result_logger).is_null());
}
#[test]
fn test_add_fields_to_logger_single_field() {
let drain = slog::Discard;
let logger = slog::Logger::root(drain, slog::o!());
let fields = vec![("single_field", "single_value".to_string())];
let result_logger = add_fields_to_logger(logger, &fields);
assert!(!std::ptr::addr_of!(result_logger).is_null());
}
#[test]
fn test_add_fields_to_logger_many_fields() {
let drain = slog::Discard;
let logger = slog::Logger::root(drain, slog::o!());
let fields = vec![
("field_1", "value_1".to_string()),
("field_2", "value_2".to_string()),
("field_3", "value_3".to_string()),
("field_4", "value_4".to_string()),
("field_5", "value_5".to_string()),
];
let result_logger = add_fields_to_logger(logger, &fields);
assert!(!std::ptr::addr_of!(result_logger).is_null());
}
#[test]
#[serial]
#[allow(clippy::approx_constant)]
fn test_logging_functions_with_different_types() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
log_error("context", 42);
log_error("context", "string error");
log_error("context", format!("formatted {}", "error"));
log_warning("context", 3.14);
log_warning("context", true);
log_debug("context", "debug message");
log_info("context", "info message");
log_trace("context", "trace message");
}
#[test]
#[serial]
fn test_logging_functions_with_empty_context() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
log_error("", "error message");
log_warning("", "warning message");
log_debug("", "debug message");
log_info("", "info message");
log_trace("", "trace message");
}
#[test]
#[serial]
fn test_logging_functions_with_special_characters() {
USING_STRUCTURED.store(false, Ordering::SeqCst);
log_error("Context with spaces", "Error with\nnewlines");
log_warning("Context-with-dashes", "Warning with\ttabs");
log_debug("Context_with_underscores", "Debug with \"quotes\"");
log_info("Context.with.dots", "Info with 'single quotes'");
log_trace("Context/with/slashes", "Trace with unicode: 🦀");
}
#[test]
#[serial]
fn test_log_with_context_special_field_values() {
test_logger::init_test_logger();
USING_STRUCTURED.store(true, Ordering::SeqCst);
let message = "Test message";
let context = "TestContext";
let special_fields = vec![
("empty_field", "".to_string()),
("unicode_field", "🦀 Rust".to_string()),
("json_like_field", r#"{"key": "value"}"#.to_string()),
("newline_field", "line1\nline2".to_string()),
("tab_field", "col1\tcol2".to_string()),
];
log_with_context(log::Level::Info, message, context, &special_fields);
}
#[test]
#[serial]
fn test_init_with_config_multiple_calls() {
let config1 = LoggingConfig {
structured: false,
level: "error".to_string(),
format: "terminal".to_string(),
include_location: false,
include_thread_id: false,
include_trace_id: false,
propagate_trace_id: false,
trace_id_header: "X-Trace-ID".to_string(),
static_fields: std::collections::HashMap::new(),
};
let config2 = LoggingConfig {
structured: true,
level: "trace".to_string(),
format: "json".to_string(),
include_location: true,
include_thread_id: true,
include_trace_id: true,
propagate_trace_id: true,
trace_id_header: "X-Trace-ID".to_string(),
static_fields: std::collections::HashMap::new(),
};
init_with_config(LevelFilter::Error, &config1);
init_with_config(LevelFilter::Trace, &config2);
init_with_config(LevelFilter::Debug, &config1);
}
#[test]
#[serial]
fn test_logging_config_with_static_fields() {
let mut static_fields = std::collections::HashMap::new();
static_fields.insert("service".to_string(), "test-service".to_string());
static_fields.insert("version".to_string(), "1.0.0".to_string());
static_fields.insert("environment".to_string(), "test".to_string());
let config = LoggingConfig {
structured: true,
level: "info".to_string(),
format: "json".to_string(),
include_location: true,
include_thread_id: true,
include_trace_id: true,
propagate_trace_id: true,
trace_id_header: "X-Trace-ID".to_string(),
static_fields,
};
init_with_config(LevelFilter::Info, &config);
}
#[test]
#[serial]
fn test_atomic_operations_thread_safety() {
use std::thread;
let handles: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
let state = is_structured_logging();
USING_STRUCTURED.store(i % 2 == 0, Ordering::SeqCst);
let new_state = is_structured_logging();
(state, new_state)
})
})
.collect();
for handle in handles {
let _ = handle.join();
}
}
#[test]
#[serial]
fn test_log_error_return_value_preservation() {
#[derive(Debug, PartialEq)]
struct CustomError {
code: i32,
message: String,
}
impl std::fmt::Display for CustomError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error {}: {}", self.code, self.message)
}
}
let original_error = CustomError {
code: 404,
message: "Not found".to_string(),
};
let returned_error = log_error("TestContext", original_error);
assert_eq!(returned_error.code, 404);
assert_eq!(returned_error.message, "Not found");
}
#[test]
fn test_logger_config_default() {
let config = LoggerConfig::default();
assert_eq!(config.format, LogFormat::Terminal);
assert_eq!(config.level, slog::Level::Info);
assert!(config.include_location);
assert!(config.include_thread_id);
assert!(config.static_fields.is_empty());
}
#[test]
fn test_logger_config_custom() {
let mut static_fields = HashMap::new();
static_fields.insert("service".to_string(), "foxy".to_string());
static_fields.insert("version".to_string(), "1.0.0".to_string());
let config = LoggerConfig {
format: LogFormat::Json,
level: slog::Level::Debug,
include_location: false,
include_thread_id: false,
static_fields,
};
assert_eq!(config.format, LogFormat::Json);
assert_eq!(config.level, slog::Level::Debug);
assert!(!config.include_location);
assert!(!config.include_thread_id);
assert_eq!(config.static_fields.len(), 2);
assert_eq!(
config.static_fields.get("service"),
Some(&"foxy".to_string())
);
assert_eq!(
config.static_fields.get("version"),
Some(&"1.0.0".to_string())
);
}
#[test]
fn test_logger_config_debug() {
let config = LoggerConfig::default();
let debug_str = format!("{config:?}");
assert!(debug_str.contains("LoggerConfig"));
assert!(debug_str.contains("format"));
assert!(debug_str.contains("level"));
assert!(debug_str.contains("include_location"));
assert!(debug_str.contains("include_thread_id"));
assert!(debug_str.contains("static_fields"));
}
#[test]
fn test_logger_config_clone() {
let mut config = LoggerConfig::default();
config
.static_fields
.insert("test".to_string(), "value".to_string());
let cloned = config.clone();
assert_eq!(config.format, cloned.format);
assert_eq!(config.level, cloned.level);
assert_eq!(config.include_location, cloned.include_location);
assert_eq!(config.include_thread_id, cloned.include_thread_id);
assert_eq!(config.static_fields, cloned.static_fields);
}
#[test]
fn test_log_format_equality() {
assert_eq!(LogFormat::Terminal, LogFormat::Terminal);
assert_eq!(LogFormat::Json, LogFormat::Json);
assert_ne!(LogFormat::Terminal, LogFormat::Json);
assert_ne!(LogFormat::Json, LogFormat::Terminal);
}
#[test]
fn test_log_format_debug() {
let terminal_debug = format!("{:?}", LogFormat::Terminal);
let json_debug = format!("{:?}", LogFormat::Json);
assert!(terminal_debug.contains("Terminal"));
assert!(json_debug.contains("Json"));
}
#[test]
fn test_log_format_clone() {
let terminal = LogFormat::Terminal;
let json = LogFormat::Json;
let terminal_clone = terminal.clone();
let json_clone = json.clone();
assert_eq!(terminal, terminal_clone);
assert_eq!(json, json_clone);
}
#[test]
fn test_request_info_creation() {
let start_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let request_info = RequestInfo {
trace_id: "test-trace-id".to_string(),
method: "GET".to_string(),
path: "/api/test".to_string(),
remote_addr: "127.0.0.1:8080".to_string(),
user_agent: "test-agent".to_string(),
start_time_ms: start_time,
};
assert_eq!(request_info.trace_id, "test-trace-id");
assert_eq!(request_info.method, "GET");
assert_eq!(request_info.path, "/api/test");
assert_eq!(request_info.remote_addr, "127.0.0.1:8080");
assert_eq!(request_info.user_agent, "test-agent");
assert_eq!(request_info.start_time_ms, start_time);
}
#[test]
fn test_request_info_elapsed_ms() {
let start_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis()
.saturating_sub(100);
let request_info = RequestInfo {
trace_id: "test".to_string(),
method: "GET".to_string(),
path: "/test".to_string(),
remote_addr: "127.0.0.1".to_string(),
user_agent: "test".to_string(),
start_time_ms: start_time,
};
let elapsed = request_info.elapsed_ms();
assert!((90..=200).contains(&elapsed));
}
#[test]
fn test_request_info_elapsed_ms_future_time() {
let future_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis()
.saturating_add(1000);
let request_info = RequestInfo {
trace_id: "test".to_string(),
method: "GET".to_string(),
path: "/test".to_string(),
remote_addr: "127.0.0.1".to_string(),
user_agent: "test".to_string(),
start_time_ms: future_time,
};
let elapsed = request_info.elapsed_ms();
assert_eq!(elapsed, 0);
}
#[test]
fn test_request_info_debug() {
let request_info = RequestInfo {
trace_id: "debug-test".to_string(),
method: "POST".to_string(),
path: "/debug".to_string(),
remote_addr: "192.168.1.1".to_string(),
user_agent: "debug-agent".to_string(),
start_time_ms: 1_234_567_890,
};
let debug_str = format!("{request_info:?}");
assert!(debug_str.contains("RequestInfo"));
assert!(debug_str.contains("debug-test"));
assert!(debug_str.contains("POST"));
assert!(debug_str.contains("/debug"));
assert!(debug_str.contains("192.168.1.1"));
assert!(debug_str.contains("debug-agent"));
assert!(debug_str.contains("1234567890"));
}
#[test]
fn test_request_info_clone() {
let original = RequestInfo {
trace_id: "clone-test".to_string(),
method: "PUT".to_string(),
path: "/clone".to_string(),
remote_addr: "10.0.0.1".to_string(),
user_agent: "clone-agent".to_string(),
start_time_ms: 9_876_543_210,
};
let cloned = original.clone();
assert_eq!(original.trace_id, cloned.trace_id);
assert_eq!(original.method, cloned.method);
assert_eq!(original.path, cloned.path);
assert_eq!(original.remote_addr, cloned.remote_addr);
assert_eq!(original.user_agent, cloned.user_agent);
assert_eq!(original.start_time_ms, cloned.start_time_ms);
}
#[test]
fn test_generate_trace_id() {
let trace_id1 = generate_trace_id();
let trace_id2 = generate_trace_id();
assert!(uuid::Uuid::parse_str(&trace_id1).is_ok());
assert!(uuid::Uuid::parse_str(&trace_id2).is_ok());
assert_ne!(trace_id1, trace_id2);
assert_eq!(trace_id1.len(), 36);
assert_eq!(trace_id2.len(), 36);
}
#[test]
fn test_generate_trace_id_format() {
let trace_id = generate_trace_id();
let parts: Vec<&str> = trace_id.split('-').collect();
assert_eq!(parts.len(), 5);
assert_eq!(parts[0].len(), 8);
assert_eq!(parts[1].len(), 4);
assert_eq!(parts[2].len(), 4);
assert_eq!(parts[3].len(), 4);
assert_eq!(parts[4].len(), 12);
for part in parts {
assert!(part.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
}
}
#[test]
fn test_init_global_logger_terminal() {
let config = LoggerConfig {
format: LogFormat::Terminal,
level: slog::Level::Info,
include_location: true,
include_thread_id: true,
static_fields: HashMap::new(),
};
let _guard = init_global_logger(&config);
}
#[test]
fn test_init_global_logger_json() {
let config = LoggerConfig {
format: LogFormat::Json,
level: slog::Level::Debug,
include_location: false,
include_thread_id: false,
static_fields: HashMap::new(),
};
let _guard = init_global_logger(&config);
}
#[test]
fn test_init_global_logger_with_static_fields() {
let mut static_fields = HashMap::new();
static_fields.insert("service".to_string(), "test-service".to_string());
static_fields.insert("environment".to_string(), "test".to_string());
let config = LoggerConfig {
format: LogFormat::Terminal,
level: slog::Level::Warning,
include_location: true,
include_thread_id: true,
static_fields,
};
let _guard = init_global_logger(&config);
}
fn create_test_config() -> LoggingConfig {
LoggingConfig {
structured: false,
format: "terminal".to_string(),
level: "info".to_string(),
include_location: true,
include_thread_id: true,
include_trace_id: true,
propagate_trace_id: false,
trace_id_header: "x-trace-id".to_string(),
static_fields: std::collections::HashMap::new(),
}
}
fn create_test_config_structured() -> LoggingConfig {
LoggingConfig {
structured: true,
format: "json".to_string(),
level: "info".to_string(),
include_location: true,
include_thread_id: true,
include_trace_id: true,
propagate_trace_id: false,
trace_id_header: "x-trace-id".to_string(),
static_fields: std::collections::HashMap::new(),
}
}
fn create_test_config_with_propagation() -> LoggingConfig {
LoggingConfig {
structured: false,
format: "terminal".to_string(),
level: "info".to_string(),
include_location: true,
include_thread_id: true,
include_trace_id: true,
propagate_trace_id: true,
trace_id_header: "x-trace-id".to_string(),
static_fields: std::collections::HashMap::new(),
}
}
fn create_test_request() -> Request<Empty<Bytes>> {
Request::builder()
.method(Method::GET)
.uri("/test/path")
.header("user-agent", "test-agent/1.0")
.body(Empty::<Bytes>::new())
.unwrap()
}
fn create_test_request_with_trace_id(trace_id: &str) -> Request<Empty<Bytes>> {
Request::builder()
.method(Method::GET)
.uri("/test/path")
.header("user-agent", "test-agent/1.0")
.header("x-trace-id", trace_id)
.body(Empty::<Bytes>::new())
.unwrap()
}
fn create_test_socket_addr() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 8080)
}
#[tokio::test]
async fn test_logging_middleware_new() {
let config = create_test_config();
let middleware = LoggingMiddleware::new(config);
assert!(!middleware.config().structured); assert!(!middleware.config().propagate_trace_id); }
#[tokio::test]
async fn test_process_request_basic() {
let config = create_test_config();
let middleware = LoggingMiddleware::new(config);
let request = create_test_request();
let remote_addr = Some(create_test_socket_addr());
let (processed_req, request_info) = middleware.process(request, remote_addr);
assert_eq!(processed_req.method(), Method::GET);
assert_eq!(processed_req.uri().path(), "/test/path");
assert_eq!(request_info.method, "GET");
assert_eq!(request_info.path, "/test/path");
assert_eq!(request_info.remote_addr, "192.168.1.100:8080");
assert_eq!(request_info.user_agent, "test-agent/1.0");
assert!(!request_info.trace_id.is_empty());
}
#[tokio::test]
async fn test_process_request_no_remote_addr() {
let config = create_test_config();
let middleware = LoggingMiddleware::new(config);
let request = create_test_request();
let (_, request_info) = middleware.process(request, None);
assert_eq!(request_info.remote_addr, "unknown");
}
#[tokio::test]
async fn test_process_request_no_user_agent() {
let config = create_test_config();
let middleware = LoggingMiddleware::new(config);
let request = Request::builder()
.method(Method::POST)
.uri("/api/test")
.body(Empty::<Bytes>::new())
.unwrap();
let (_, request_info) = middleware.process(request, None);
assert_eq!(request_info.method, "POST");
assert_eq!(request_info.path, "/api/test");
assert_eq!(request_info.user_agent, "unknown");
}
#[tokio::test]
async fn test_process_request_with_trace_propagation() {
let config = create_test_config_with_propagation();
let middleware = LoggingMiddleware::new(config);
let existing_trace_id = "existing-trace-123";
let request = create_test_request_with_trace_id(existing_trace_id);
let (_, request_info) = middleware.process(request, None);
assert_eq!(request_info.trace_id, existing_trace_id);
}
#[tokio::test]
async fn test_process_request_without_trace_propagation() {
let config = create_test_config();
let middleware = LoggingMiddleware::new(config);
let request = create_test_request_with_trace_id("existing-trace-123");
let (_, request_info) = middleware.process(request, None);
assert_ne!(request_info.trace_id, "existing-trace-123");
assert!(!request_info.trace_id.is_empty());
}
#[tokio::test]
async fn test_process_request_invalid_trace_header() {
let config = create_test_config_with_propagation();
let middleware = LoggingMiddleware::new(config);
let request = Request::builder()
.method(Method::GET)
.uri("/test")
.header("x-trace-id", "") .body(Empty::<Bytes>::new())
.unwrap();
let (_, request_info) = middleware.process(request, None);
assert!(!request_info.trace_id.is_empty());
assert_ne!(request_info.trace_id, "");
}
#[test]
fn test_log_response_basic() {
let config = create_test_config();
let middleware = LoggingMiddleware::new(config);
let response = Response::builder()
.status(200)
.body(Empty::<Bytes>::new())
.unwrap();
let request_info = RequestInfo {
trace_id: "test-trace-123".to_string(),
method: "GET".to_string(),
path: "/test".to_string(),
remote_addr: "192.168.1.1".to_string(),
user_agent: "test-agent".to_string(),
start_time_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
};
middleware.log_response(&response, &request_info, Some(Duration::from_millis(50)));
}
#[test]
#[serial]
fn test_log_response_structured() {
test_logger::init_test_logger();
let config = create_test_config_structured();
let middleware = LoggingMiddleware::new(config);
let response = Response::builder()
.status(404)
.body(Empty::<Bytes>::new())
.unwrap();
let request_info = RequestInfo {
trace_id: "test-trace-456".to_string(),
method: "POST".to_string(),
path: "/api/users".to_string(),
remote_addr: "10.0.0.1".to_string(),
user_agent: "curl/7.68.0".to_string(),
start_time_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
};
middleware.log_response(&response, &request_info, None);
}
#[test]
fn test_log_response_no_upstream_duration() {
let config = create_test_config();
let middleware = LoggingMiddleware::new(config);
let response = Response::builder()
.status(500)
.body(Empty::<Bytes>::new())
.unwrap();
let request_info = RequestInfo {
trace_id: "test-trace-789".to_string(),
method: "DELETE".to_string(),
path: "/api/resource/123".to_string(),
remote_addr: "172.16.0.1".to_string(),
user_agent: "Mozilla/5.0".to_string(),
start_time_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
};
middleware.log_response(&response, &request_info, None);
}
struct MockResponseFuture {
response: Option<Result<Response<Empty<Bytes>>, &'static str>>,
}
impl MockResponseFuture {
fn new_ok(response: Response<Empty<Bytes>>) -> Self {
Self {
response: Some(Ok(response)),
}
}
fn new_err(error: &'static str) -> Self {
Self {
response: Some(Err(error)),
}
}
}
impl Future for MockResponseFuture {
type Output = Result<Response<Empty<Bytes>>, &'static str>;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(self.response.take().unwrap())
}
}
impl Unpin for MockResponseFuture {}
#[tokio::test]
async fn test_traced_response_future_success_with_trace_id() {
let response = Response::builder()
.status(200)
.body(Empty::<Bytes>::new())
.unwrap();
let future = MockResponseFuture::new_ok(response);
let traced_future =
future.with_trace_id("test-trace-123".to_string(), "x-trace-id".to_string(), true);
let result = traced_future.await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), 200);
assert!(response.headers().contains_key("x-trace-id"));
assert_eq!(
response.headers().get("x-trace-id").unwrap(),
"test-trace-123"
);
}
#[tokio::test]
async fn test_traced_response_future_success_without_trace_id() {
let response = Response::builder()
.status(201)
.body(Empty::<Bytes>::new())
.unwrap();
let future = MockResponseFuture::new_ok(response);
let traced_future = future.with_trace_id(
"test-trace-456".to_string(),
"x-trace-id".to_string(),
false, );
let result = traced_future.await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), 201);
assert!(!response.headers().contains_key("x-trace-id"));
}
#[tokio::test]
async fn test_traced_response_future_error() {
let future = MockResponseFuture::new_err("test error");
let traced_future =
future.with_trace_id("test-trace-789".to_string(), "x-trace-id".to_string(), true);
let result = traced_future.await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "test error");
}
#[tokio::test]
async fn test_traced_response_future_invalid_header_name() {
let response = Response::builder()
.status(200)
.body(Empty::<Bytes>::new())
.unwrap();
let future = MockResponseFuture::new_ok(response);
let traced_future = future.with_trace_id(
"test-trace-123".to_string(),
"invalid header name with spaces".to_string(), true,
);
let result = traced_future.await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.headers().contains_key("x-trace-id"));
}
#[tokio::test]
async fn test_traced_response_future_invalid_header_value() {
let response = Response::builder()
.status(200)
.body(Empty::<Bytes>::new())
.unwrap();
let future = MockResponseFuture::new_ok(response);
let traced_future = future.with_trace_id(
"\x00\x01\x02".to_string(), "x-trace-id".to_string(),
true,
);
let result = traced_future.await;
assert!(result.is_ok());
let response = result.unwrap();
assert!(response.headers().contains_key("x-trace-id"));
assert_eq!(
response.headers().get("x-trace-id").unwrap(),
"invalid-trace-id"
);
}
#[test]
fn test_init_global_logger_all_log_levels() {
let levels = vec![
slog::Level::Trace,
slog::Level::Debug,
slog::Level::Info,
slog::Level::Warning,
slog::Level::Error,
slog::Level::Critical,
];
for level in levels {
let config = LoggerConfig {
format: LogFormat::Terminal,
level,
include_location: true,
include_thread_id: true,
static_fields: HashMap::new(),
};
let _guard = init_global_logger(&config);
}
}
#[test]
fn test_init_global_logger_json_with_all_levels() {
let levels = vec![
slog::Level::Trace,
slog::Level::Debug,
slog::Level::Info,
slog::Level::Warning,
slog::Level::Error,
slog::Level::Critical,
];
for level in levels {
let config = LoggerConfig {
format: LogFormat::Json,
level,
include_location: false,
include_thread_id: false,
static_fields: HashMap::new(),
};
let _guard = init_global_logger(&config);
}
}
#[test]
fn test_init_global_logger_with_large_static_fields() {
let mut static_fields = HashMap::new();
for i in 0..100 {
static_fields.insert(format!("field_{i}"), format!("value_{i}"));
}
let config = LoggerConfig {
format: LogFormat::Json,
level: slog::Level::Info,
include_location: true,
include_thread_id: true,
static_fields,
};
let _guard = init_global_logger(&config);
}
#[test]
fn test_init_global_logger_with_special_character_static_fields() {
let mut static_fields = HashMap::new();
static_fields.insert("unicode_field".to_string(), "🦀 Rust 🚀".to_string());
static_fields.insert(
"json_field".to_string(),
r#"{"nested": "value"}"#.to_string(),
);
static_fields.insert(
"newline_field".to_string(),
"line1\nline2\nline3".to_string(),
);
static_fields.insert("tab_field".to_string(), "col1\tcol2\tcol3".to_string());
static_fields.insert("quote_field".to_string(), r#"He said "Hello""#.to_string());
static_fields.insert("empty_field".to_string(), "".to_string());
let config = LoggerConfig {
format: LogFormat::Json,
level: slog::Level::Debug,
include_location: true,
include_thread_id: true,
static_fields,
};
let _guard = init_global_logger(&config);
}
#[test]
fn test_logger_guard_drop_behavior() {
let config = LoggerConfig {
format: LogFormat::Terminal,
level: slog::Level::Info,
include_location: true,
include_thread_id: true,
static_fields: HashMap::new(),
};
{
let _guard1 = init_global_logger(&config);
{
let _guard2 = init_global_logger(&config);
}
}
let _guard3 = init_global_logger(&config);
}
#[test]
fn test_concurrent_logger_initialization() {
use std::sync::Arc;
use std::thread;
let config = Arc::new(LoggerConfig {
format: LogFormat::Json,
level: slog::Level::Info,
include_location: true,
include_thread_id: true,
static_fields: HashMap::new(),
});
let handles: Vec<_> = (0..10)
.map(|_| {
let config_clone = Arc::clone(&config);
thread::spawn(move || {
let _guard = init_global_logger(&config_clone);
thread::sleep(std::time::Duration::from_millis(1));
})
})
.collect();
for handle in handles {
handle.join().expect("Thread should complete successfully");
}
}
#[test]
fn test_request_info_elapsed_ms_edge_cases() {
let request_info = RequestInfo {
trace_id: "test".to_string(),
method: "GET".to_string(),
path: "/test".to_string(),
remote_addr: "127.0.0.1".to_string(),
user_agent: "test".to_string(),
start_time_ms: 0,
};
let elapsed = request_info.elapsed_ms();
assert!(elapsed > 0);
let request_info = RequestInfo {
trace_id: "test".to_string(),
method: "GET".to_string(),
path: "/test".to_string(),
remote_addr: "127.0.0.1".to_string(),
user_agent: "test".to_string(),
start_time_ms: u128::MAX,
};
let elapsed = request_info.elapsed_ms();
assert_eq!(elapsed, 0);
}
#[test]
fn test_request_info_with_extreme_values() {
let request_info = RequestInfo {
trace_id: "a".repeat(1000), method: "CUSTOM_METHOD_WITH_VERY_LONG_NAME".to_string(),
path: "/".repeat(500), remote_addr: "255.255.255.255:65535".to_string(), user_agent: "Mozilla/5.0 ".repeat(100), start_time_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
};
let elapsed = request_info.elapsed_ms();
let _ = elapsed;
let debug_str = format!("{request_info:?}");
assert!(debug_str.contains("RequestInfo"));
}
#[test]
fn test_generate_trace_id_uniqueness() {
let mut trace_ids = std::collections::HashSet::new();
for _ in 0..1000 {
let trace_id = generate_trace_id();
assert!(trace_ids.insert(trace_id), "Trace ID should be unique");
}
}
#[test]
fn test_generate_trace_id_concurrent() {
use std::sync::{Arc, Mutex};
use std::thread;
let trace_ids = Arc::new(Mutex::new(std::collections::HashSet::new()));
let handles: Vec<_> = (0..10)
.map(|_| {
let trace_ids_clone = Arc::clone(&trace_ids);
thread::spawn(move || {
let mut local_ids = Vec::new();
for _ in 0..100 {
local_ids.push(generate_trace_id());
}
let mut global_ids = trace_ids_clone.lock().unwrap();
for id in local_ids {
assert!(
global_ids.insert(id),
"Concurrent trace IDs should be unique"
);
}
})
})
.collect();
for handle in handles {
handle.join().expect("Thread should complete successfully");
}
let final_ids = trace_ids.lock().unwrap();
assert_eq!(final_ids.len(), 1000); }
#[test]
fn test_logger_config_with_all_combinations() {
let formats = vec![LogFormat::Terminal, LogFormat::Json];
let levels = vec![
slog::Level::Trace,
slog::Level::Debug,
slog::Level::Info,
slog::Level::Warning,
slog::Level::Error,
slog::Level::Critical,
];
let location_flags = vec![true, false];
let thread_flags = vec![true, false];
for format in &formats {
for &level in &levels {
for &include_location in &location_flags {
for &include_thread_id in &thread_flags {
let config = LoggerConfig {
format: format.clone(),
level,
include_location,
include_thread_id,
static_fields: HashMap::new(),
};
let _guard = init_global_logger(&config);
}
}
}
}
}
#[test]
fn test_slog_level_to_log_level_conversion() {
let test_cases = vec![
(slog::Level::Trace, "TRACE"),
(slog::Level::Debug, "DEBUG"),
(slog::Level::Info, "INFO"),
(slog::Level::Warning, "WARN"),
(slog::Level::Error, "ERROR"),
(slog::Level::Critical, "CRITICAL"),
];
for (slog_level, expected_name) in test_cases {
let config = LoggerConfig {
format: LogFormat::Terminal,
level: slog_level,
include_location: true,
include_thread_id: true,
static_fields: HashMap::new(),
};
let _guard = init_global_logger(&config);
assert_eq!(slog_level.as_str(), expected_name);
}
}
#[test]
fn test_static_fields_memory_management() {
let mut static_fields = HashMap::new();
static_fields.insert("key_with_underscores".to_string(), "value1".to_string());
static_fields.insert("key-with-dashes".to_string(), "value2".to_string());
static_fields.insert("key.with.dots".to_string(), "value3".to_string());
static_fields.insert("key/with/slashes".to_string(), "value4".to_string());
static_fields.insert("key with spaces".to_string(), "value5".to_string());
static_fields.insert("🔑".to_string(), "🔒".to_string());
let config = LoggerConfig {
format: LogFormat::Json,
level: slog::Level::Info,
include_location: true,
include_thread_id: true,
static_fields,
};
let _guard = init_global_logger(&config);
}
#[test]
fn test_empty_static_fields_handling() {
let config = LoggerConfig {
format: LogFormat::Json,
level: slog::Level::Info,
include_location: true,
include_thread_id: true,
static_fields: HashMap::new(),
};
let _guard = init_global_logger(&config);
let mut static_fields = HashMap::new();
static_fields.insert("empty_value".to_string(), "".to_string());
static_fields.insert("".to_string(), "empty_key".to_string());
let config2 = LoggerConfig {
format: LogFormat::Terminal,
level: slog::Level::Debug,
include_location: false,
include_thread_id: false,
static_fields,
};
let _guard2 = init_global_logger(&config2);
}
#[test]
fn test_request_info_with_unicode_and_special_chars() {
let request_info = RequestInfo {
trace_id: "🔍-trace-🆔".to_string(),
method: "ПОСТ".to_string(), path: "/测试/路径".to_string(), remote_addr: "::1".to_string(), user_agent: "Mozilla/5.0 (🦀; Rust/1.0)".to_string(),
start_time_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
};
let elapsed = request_info.elapsed_ms();
let _ = elapsed;
let cloned = request_info.clone();
assert_eq!(request_info.trace_id, cloned.trace_id);
assert_eq!(request_info.method, cloned.method);
assert_eq!(request_info.path, cloned.path);
}
#[test]
fn test_to_logger_config_format_mapping() {
let config = LoggingConfig {
format: "json".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Json);
let config = LoggingConfig {
format: "JSON".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Json);
let config = LoggingConfig {
format: "terminal".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Terminal);
let config = LoggingConfig {
format: "TERMINAL".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Terminal);
let config = LoggingConfig {
format: "unknown".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Terminal);
let config = LoggingConfig {
format: "".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Terminal);
}
#[test]
fn test_to_logger_config_level_mapping() {
let config = LoggingConfig {
level: "trace".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Trace);
let config = LoggingConfig {
level: "debug".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Debug);
let config = LoggingConfig {
level: "info".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Info);
let config = LoggingConfig {
level: "warn".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Warning);
let config = LoggingConfig {
level: "error".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Error);
let config = LoggingConfig {
level: "critical".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Critical);
}
#[test]
fn test_to_logger_config_level_case_insensitive() {
let config = LoggingConfig {
level: "TRACE".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Trace);
let config = LoggingConfig {
level: "DEBUG".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Debug);
let config = LoggingConfig {
level: "WARN".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Warning);
let config = LoggingConfig {
level: "ERROR".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Error);
let config = LoggingConfig {
level: "CRITICAL".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Critical);
let config = LoggingConfig {
level: "TrAcE".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Trace);
}
#[test]
fn test_to_logger_config_level_defaults() {
let config = LoggingConfig {
level: "unknown".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Info);
let config = LoggingConfig {
level: "".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Info);
let config = LoggingConfig {
level: "info".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.level, slog::Level::Info);
}
#[test]
fn test_to_logger_config_boolean_fields() {
let config = LoggingConfig {
include_location: false,
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert!(!logger_config.include_location);
let config = LoggingConfig {
include_location: true,
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert!(logger_config.include_location);
let config = LoggingConfig {
include_thread_id: false,
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert!(!logger_config.include_thread_id);
let config = LoggingConfig {
include_thread_id: true,
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert!(logger_config.include_thread_id);
}
#[test]
fn test_to_logger_config_static_fields() {
let config = LoggingConfig {
static_fields: HashMap::new(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert!(logger_config.static_fields.is_empty());
let mut static_fields = HashMap::new();
static_fields.insert("service".to_string(), "foxy".to_string());
static_fields.insert("version".to_string(), "1.0.0".to_string());
static_fields.insert("environment".to_string(), "test".to_string());
let config = LoggingConfig {
static_fields: static_fields.clone(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.static_fields.len(), 3);
assert_eq!(
logger_config.static_fields.get("service"),
Some(&"foxy".to_string())
);
assert_eq!(
logger_config.static_fields.get("version"),
Some(&"1.0.0".to_string())
);
assert_eq!(
logger_config.static_fields.get("environment"),
Some(&"test".to_string())
);
assert_eq!(logger_config.static_fields, static_fields);
}
#[test]
fn test_to_logger_config_comprehensive() {
let mut static_fields = HashMap::new();
static_fields.insert("app".to_string(), "foxy-proxy".to_string());
static_fields.insert("build".to_string(), "release".to_string());
let config = LoggingConfig {
structured: true, format: "JSON".to_string(),
level: "DEBUG".to_string(),
include_location: false,
include_thread_id: false,
include_trace_id: true, propagate_trace_id: false, trace_id_header: "X-Custom-Trace".to_string(), static_fields,
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Json);
assert_eq!(logger_config.level, slog::Level::Debug);
assert!(!logger_config.include_location);
assert!(!logger_config.include_thread_id);
assert_eq!(logger_config.static_fields.len(), 2);
assert_eq!(
logger_config.static_fields.get("app"),
Some(&"foxy-proxy".to_string())
);
assert_eq!(
logger_config.static_fields.get("build"),
Some(&"release".to_string())
);
}
#[test]
fn test_to_logger_config_edge_cases() {
let config = LoggingConfig {
format: "jSoN".to_string(),
level: "wArN".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Json);
assert_eq!(logger_config.level, slog::Level::Warning);
let config = LoggingConfig {
format: " json ".to_string(),
level: " error ".to_string(),
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.format, LogFormat::Terminal);
assert_eq!(logger_config.level, slog::Level::Info);
let mut static_fields = HashMap::new();
static_fields.insert("key-with-dashes".to_string(), "value".to_string());
static_fields.insert("key_with_underscores".to_string(), "value".to_string());
static_fields.insert("key.with.dots".to_string(), "value".to_string());
static_fields.insert("123numeric".to_string(), "value".to_string());
let config = LoggingConfig {
static_fields,
..LoggingConfig::default()
};
let logger_config = config.to_logger_config();
assert_eq!(logger_config.static_fields.len(), 4);
}
#[test]
fn test_to_logger_config_must_use_annotation() {
let config = LoggingConfig::default();
let _logger_config = config.to_logger_config();
}
#[test]
fn test_logging_config_default() {
let config = LoggingConfig::default();
assert!(!config.structured);
assert_eq!(config.format, "terminal");
assert_eq!(config.level, "info");
assert!(config.include_location);
assert!(config.include_thread_id);
assert!(config.include_trace_id);
assert!(config.propagate_trace_id);
assert_eq!(config.trace_id_header, "X-Trace-ID");
assert!(config.static_fields.is_empty());
}
#[test]
fn test_logging_config_default_functions_via_serde() {
let json_configs = vec![
r#"{"structured": null}"#, r#"{"format": null}"#, r#"{"level": null}"#, r#"{"include_location": null}"#, r#"{"include_thread_id": null}"#, r#"{"include_trace_id": null}"#, r#"{"propagate_trace_id": null}"#, r#"{"trace_id_header": null}"#, ];
for json in json_configs {
let result = serde_json::from_str::<LoggingConfig>(json);
let _ = result;
}
let json = r#"{"format": "json"}"#;
let config: LoggingConfig = serde_json::from_str(json).unwrap();
assert!(!config.structured); assert_eq!(config.format, "json"); assert_eq!(config.level, "info"); assert!(config.include_location); assert!(config.include_thread_id); assert!(config.include_trace_id); assert!(config.propagate_trace_id); assert_eq!(config.trace_id_header, "X-Trace-ID"); }
#[test]
fn test_logging_config_serialization() {
let mut static_fields = HashMap::new();
static_fields.insert("service".to_string(), "foxy".to_string());
static_fields.insert("version".to_string(), "1.0.0".to_string());
let config = LoggingConfig {
structured: true,
format: "json".to_string(),
level: "debug".to_string(),
include_location: false,
include_thread_id: false,
include_trace_id: false,
propagate_trace_id: false,
trace_id_header: "X-Custom-Trace".to_string(),
static_fields,
};
let serialized = serde_json::to_string(&config).unwrap();
assert!(serialized.contains("\"structured\":true"));
assert!(serialized.contains("\"format\":\"json\""));
assert!(serialized.contains("\"level\":\"debug\""));
assert!(serialized.contains("\"include_location\":false"));
assert!(serialized.contains("\"include_thread_id\":false"));
assert!(serialized.contains("\"include_trace_id\":false"));
assert!(serialized.contains("\"propagate_trace_id\":false"));
assert!(serialized.contains("\"trace_id_header\":\"X-Custom-Trace\""));
assert!(serialized.contains("\"service\":\"foxy\""));
assert!(serialized.contains("\"version\":\"1.0.0\""));
let deserialized: LoggingConfig = serde_json::from_str(&serialized).unwrap();
assert_eq!(config.structured, deserialized.structured);
assert_eq!(config.format, deserialized.format);
assert_eq!(config.level, deserialized.level);
assert_eq!(config.include_location, deserialized.include_location);
assert_eq!(config.include_thread_id, deserialized.include_thread_id);
assert_eq!(config.include_trace_id, deserialized.include_trace_id);
assert_eq!(config.propagate_trace_id, deserialized.propagate_trace_id);
assert_eq!(config.trace_id_header, deserialized.trace_id_header);
assert_eq!(config.static_fields, deserialized.static_fields);
}
#[test]
fn test_logging_config_partial_deserialization() {
let json = r#"{"format": "json", "level": "error"}"#;
let config: LoggingConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.format, "json");
assert_eq!(config.level, "error");
assert!(!config.structured); assert!(config.include_location); assert!(config.include_thread_id); assert!(config.include_trace_id); assert!(config.propagate_trace_id); assert_eq!(config.trace_id_header, "X-Trace-ID"); assert!(config.static_fields.is_empty()); }
#[test]
fn test_logging_config_empty_deserialization() {
let json = r#"{}"#;
let config: LoggingConfig = serde_json::from_str(json).unwrap();
assert!(!config.structured);
assert_eq!(config.format, "terminal");
assert_eq!(config.level, "info");
assert!(config.include_location);
assert!(config.include_thread_id);
assert!(config.include_trace_id);
assert!(config.propagate_trace_id);
assert_eq!(config.trace_id_header, "X-Trace-ID");
assert!(config.static_fields.is_empty());
}
#[test]
fn test_logging_config_debug_clone() {
let mut static_fields = HashMap::new();
static_fields.insert("test".to_string(), "value".to_string());
let config = LoggingConfig {
structured: true,
format: "json".to_string(),
level: "trace".to_string(),
include_location: false,
include_thread_id: false,
include_trace_id: false,
propagate_trace_id: false,
trace_id_header: "Custom-Header".to_string(),
static_fields,
};
let debug_str = format!("{config:?}");
assert!(debug_str.contains("LoggingConfig"));
assert!(debug_str.contains("structured"));
assert!(debug_str.contains("format"));
assert!(debug_str.contains("level"));
let cloned = config.clone();
assert_eq!(config.structured, cloned.structured);
assert_eq!(config.format, cloned.format);
assert_eq!(config.level, cloned.level);
assert_eq!(config.include_location, cloned.include_location);
assert_eq!(config.include_thread_id, cloned.include_thread_id);
assert_eq!(config.include_trace_id, cloned.include_trace_id);
assert_eq!(config.propagate_trace_id, cloned.propagate_trace_id);
assert_eq!(config.trace_id_header, cloned.trace_id_header);
assert_eq!(config.static_fields, cloned.static_fields);
}
}