use std::collections::HashSet;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum LogOutputFormat {
#[default]
Json,
Datadog,
Splunk,
Logfmt,
Pretty,
}
#[derive(Clone, Debug)]
pub struct StructuredLoggingConfig {
pub format: LogOutputFormat,
pub include_request_headers: bool,
pub include_response_headers: bool,
pub include_request_body: bool,
pub include_response_body: bool,
pub max_body_size: usize,
pub correlation_id_header: String,
pub generate_correlation_id: bool,
pub redact_headers: HashSet<String>,
pub include_timing: bool,
pub service_name: String,
pub service_version: Option<String>,
pub environment: Option<String>,
pub exclude_paths: Vec<String>,
pub static_fields: Vec<(String, String)>,
pub log_request_start: bool,
pub log_request_end: bool,
pub include_caller_info: bool,
}
impl Default for StructuredLoggingConfig {
fn default() -> Self {
let mut redact_headers = HashSet::new();
redact_headers.insert("authorization".to_string());
redact_headers.insert("cookie".to_string());
redact_headers.insert("x-api-key".to_string());
redact_headers.insert("x-auth-token".to_string());
Self {
format: LogOutputFormat::default(),
include_request_headers: false,
include_response_headers: false,
include_request_body: false,
include_response_body: false,
max_body_size: 1024,
correlation_id_header: "x-correlation-id".to_string(),
generate_correlation_id: true,
redact_headers,
include_timing: true,
service_name: "rustapi".to_string(),
service_version: None,
environment: None,
exclude_paths: vec!["/health".to_string(), "/metrics".to_string()],
static_fields: Vec::new(),
log_request_start: true,
log_request_end: true,
include_caller_info: false,
}
}
}
impl StructuredLoggingConfig {
pub fn builder() -> StructuredLoggingConfigBuilder {
StructuredLoggingConfigBuilder::default()
}
pub fn development() -> Self {
Self {
format: LogOutputFormat::Pretty,
include_request_headers: true,
include_response_headers: true,
include_timing: true,
log_request_start: true,
log_request_end: true,
include_caller_info: true,
..Default::default()
}
}
pub fn production_json() -> Self {
Self {
format: LogOutputFormat::Json,
include_request_headers: false,
include_response_headers: false,
include_timing: true,
log_request_start: false,
log_request_end: true,
include_caller_info: false,
..Default::default()
}
}
pub fn datadog() -> Self {
Self {
format: LogOutputFormat::Datadog,
include_timing: true,
log_request_start: false,
log_request_end: true,
..Default::default()
}
}
pub fn splunk() -> Self {
Self {
format: LogOutputFormat::Splunk,
include_timing: true,
log_request_start: false,
log_request_end: true,
..Default::default()
}
}
}
#[derive(Default)]
pub struct StructuredLoggingConfigBuilder {
config: StructuredLoggingConfig,
}
impl StructuredLoggingConfigBuilder {
pub fn format(mut self, format: LogOutputFormat) -> Self {
self.config.format = format;
self
}
pub fn include_request_headers(mut self, include: bool) -> Self {
self.config.include_request_headers = include;
self
}
pub fn include_response_headers(mut self, include: bool) -> Self {
self.config.include_response_headers = include;
self
}
pub fn include_request_body(mut self, include: bool) -> Self {
self.config.include_request_body = include;
self
}
pub fn include_response_body(mut self, include: bool) -> Self {
self.config.include_response_body = include;
self
}
pub fn max_body_size(mut self, size: usize) -> Self {
self.config.max_body_size = size;
self
}
pub fn correlation_id_header(mut self, header: impl Into<String>) -> Self {
self.config.correlation_id_header = header.into();
self
}
pub fn generate_correlation_id(mut self, generate: bool) -> Self {
self.config.generate_correlation_id = generate;
self
}
pub fn redact_header(mut self, header: impl Into<String>) -> Self {
self.config
.redact_headers
.insert(header.into().to_lowercase());
self
}
pub fn redact_headers(mut self, headers: Vec<String>) -> Self {
self.config.redact_headers = headers.into_iter().map(|h| h.to_lowercase()).collect();
self
}
pub fn include_timing(mut self, include: bool) -> Self {
self.config.include_timing = include;
self
}
pub fn service_name(mut self, name: impl Into<String>) -> Self {
self.config.service_name = name.into();
self
}
pub fn service_version(mut self, version: impl Into<String>) -> Self {
self.config.service_version = Some(version.into());
self
}
pub fn environment(mut self, env: impl Into<String>) -> Self {
self.config.environment = Some(env.into());
self
}
pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
self.config.exclude_paths.push(path.into());
self
}
pub fn exclude_paths(mut self, paths: Vec<String>) -> Self {
self.config.exclude_paths = paths;
self
}
pub fn static_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config.static_fields.push((key.into(), value.into()));
self
}
pub fn log_request_start(mut self, log: bool) -> Self {
self.config.log_request_start = log;
self
}
pub fn log_request_end(mut self, log: bool) -> Self {
self.config.log_request_end = log;
self
}
pub fn include_caller_info(mut self, include: bool) -> Self {
self.config.include_caller_info = include;
self
}
pub fn build(self) -> StructuredLoggingConfig {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = StructuredLoggingConfig::default();
assert_eq!(config.format, LogOutputFormat::Json);
assert!(config.generate_correlation_id);
assert!(config.include_timing);
assert!(config.redact_headers.contains("authorization"));
}
#[test]
fn test_builder() {
let config = StructuredLoggingConfig::builder()
.format(LogOutputFormat::Datadog)
.service_name("my-service")
.service_version("1.0.0")
.environment("production")
.include_request_headers(true)
.redact_header("x-secret")
.static_field("region", "us-east-1")
.build();
assert_eq!(config.format, LogOutputFormat::Datadog);
assert_eq!(config.service_name, "my-service");
assert_eq!(config.service_version, Some("1.0.0".to_string()));
assert!(config.include_request_headers);
assert!(config.redact_headers.contains("x-secret"));
assert_eq!(config.static_fields.len(), 1);
}
#[test]
fn test_development_preset() {
let config = StructuredLoggingConfig::development();
assert_eq!(config.format, LogOutputFormat::Pretty);
assert!(config.include_request_headers);
assert!(config.include_caller_info);
}
#[test]
fn test_production_preset() {
let config = StructuredLoggingConfig::production_json();
assert_eq!(config.format, LogOutputFormat::Json);
assert!(!config.include_request_headers);
assert!(!config.log_request_start);
assert!(config.log_request_end);
}
#[test]
fn test_datadog_preset() {
let config = StructuredLoggingConfig::datadog();
assert_eq!(config.format, LogOutputFormat::Datadog);
}
#[test]
fn test_splunk_preset() {
let config = StructuredLoggingConfig::splunk();
assert_eq!(config.format, LogOutputFormat::Splunk);
}
}