use super::LoggerManager;
use crate::InklogError;
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
use crate::integrations::Database;
use crate::integrations::{Cache, Config};
use crate::{ConsoleSinkConfig, FileSinkConfig, InklogConfig};
use std::sync::Arc;
#[derive(Default)]
pub struct LoggerDependencies {
pub cache: Option<Arc<dyn Cache>>,
pub config: Option<Arc<dyn Config>>,
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
pub database: Option<Arc<dyn Database>>,
}
impl std::fmt::Debug for LoggerDependencies {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut builder = f.debug_struct("LoggerDependencies");
builder
.field("cache", &self.cache.as_ref().map(|_| "Arc<dyn Cache>"))
.field("config", &self.config.as_ref().map(|_| "Arc<dyn Config>"));
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
builder.field(
"database",
&self.database.as_ref().map(|_| "Arc<dyn Database>"),
);
builder.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_valid_level() {
let builder = LoggerBuilder::new().level("debug");
assert!(builder.validation_errors.is_empty());
assert_eq!(builder.config.global.level, "debug");
}
#[test]
fn test_builder_invalid_level() {
let builder = LoggerBuilder::new().level("invalid_level");
assert_eq!(builder.validation_errors.len(), 1);
assert!(builder.validation_errors[0].contains("Invalid log level"));
}
#[test]
fn test_builder_case_insensitive_level() {
let builder = LoggerBuilder::new().level("DEBUG");
assert!(builder.validation_errors.is_empty());
}
#[test]
fn test_builder_multiple_errors() {
let builder = LoggerBuilder::new().level("bad1").level("bad2");
assert_eq!(builder.validation_errors.len(), 2);
}
#[cfg(feature = "http")]
#[test]
fn test_builder_http_port_zero() {
let builder = LoggerBuilder::new().http_port(0);
assert_eq!(builder.validation_errors.len(), 1);
assert!(builder.validation_errors[0].contains("HTTP port"));
}
#[cfg(feature = "http")]
#[test]
fn test_builder_http_port_valid() {
let builder = LoggerBuilder::new().http_port(8080);
assert!(builder.validation_errors.is_empty());
}
#[tokio::test]
async fn test_builder_build_fails_with_validation_errors() {
let result = LoggerBuilder::new().level("invalid").build().await;
assert!(result.is_err());
match result {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("Builder validation failed") || msg.contains("构建器验证失败")
);
}
Ok(_) => panic!("Expected build to fail with validation errors"),
}
}
#[test]
fn test_builder_file_when_no_file_sink() {
let mut builder = LoggerBuilder::new();
builder.config.file_sink = None;
builder = builder.file("/tmp/test.log");
assert!(builder.config.file_sink.is_some());
let file_cfg = builder.config.file_sink.as_ref().unwrap();
assert!(file_cfg.enabled);
assert_eq!(file_cfg.path, std::path::PathBuf::from("/tmp/test.log"));
}
#[test]
fn test_builder_file_when_file_sink_exists() {
let mut builder = LoggerBuilder::new();
builder.config.file_sink = Some(FileSinkConfig::default());
builder = builder.file("/tmp/updated.log");
let file_cfg = builder.config.file_sink.as_ref().unwrap();
assert!(file_cfg.enabled);
assert_eq!(file_cfg.path, std::path::PathBuf::from("/tmp/updated.log"));
}
#[cfg(feature = "http")]
#[test]
fn test_builder_http_error_mode_unknown() {
let builder = LoggerBuilder::new().http_error_mode("invalid_mode");
assert_eq!(builder.validation_errors.len(), 1);
assert!(builder.validation_errors[0].contains("Unknown HTTP error mode"));
}
#[cfg(feature = "http")]
#[test]
fn test_builder_http_error_mode_warn() {
let builder = LoggerBuilder::new().http_error_mode("warn");
assert!(builder.validation_errors.is_empty());
assert!(matches!(
builder.config.http_server.as_ref().unwrap().error_mode,
crate::HttpErrorMode::Warn
));
}
}
#[derive(Default)]
pub struct LoggerBuilder {
pub(crate) config: InklogConfig,
pub(crate) deps: LoggerDependencies,
pub(crate) validation_errors: Vec<String>,
}
impl LoggerBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn level(mut self, level: impl Into<String>) -> Self {
let level_str = level.into();
if !crate::LogLevel::is_valid_level(&level_str) {
self.validation_errors.push(format!(
"Invalid log level '{}'. Valid levels: {}",
level_str,
crate::LogLevel::VALID_LEVEL_STRINGS.join(", ")
));
}
self.config.global.level = level_str;
self
}
pub fn format(mut self, format: impl Into<String>) -> Self {
self.config.global.format = format.into();
self
}
pub fn console(mut self, enabled: bool) -> Self {
if let Some(ref mut console) = self.config.console_sink {
console.enabled = enabled;
} else if enabled {
self.config.console_sink = Some(ConsoleSinkConfig::default());
}
self
}
pub fn file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
if let Some(ref mut file) = self.config.file_sink {
file.enabled = true;
file.path = path.into();
} else {
let path_buf = path.into();
self.config.file_sink = Some(FileSinkConfig {
enabled: true,
path: path_buf,
..Default::default()
});
}
self
}
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
pub fn database(mut self, url: impl Into<String>) -> Self {
let url_str = url.into();
let config = crate::DatabaseSinkConfig {
name: "default".to_string(),
enabled: true,
driver: crate::DatabaseDriver::default(),
url: url_str,
pool_size: 10,
batch_size: 100,
flush_interval_ms: 500,
partition: crate::PartitionStrategy::default(),
table_name: "logs".to_string(),
archive_format: crate::ArchiveFormat::default(),
parquet_config: crate::ParquetConfig::default(),
permissions_path: None,
admin_role: "admin".to_string(),
};
self.config.database_sink = Some(config);
self
}
pub fn channel_capacity(mut self, capacity: usize) -> Self {
self.config.performance.channel_capacity = capacity;
self
}
pub fn worker_threads(mut self, threads: usize) -> Self {
self.config.performance.worker_threads = threads;
self
}
pub fn console_colored(mut self, colored: bool) -> Self {
if let Some(ref mut console) = self.config.console_sink {
console.colored = colored;
} else if colored {
self.config.console_sink = Some(ConsoleSinkConfig {
colored,
..Default::default()
});
}
self
}
pub fn console_stderr_levels(mut self, levels: &[&str]) -> Self {
if let Some(ref mut console) = self.config.console_sink {
console.stderr_levels = levels.iter().map(|s| (*s).to_string()).collect();
} else {
self.config.console_sink = Some(ConsoleSinkConfig {
stderr_levels: levels.iter().map(|s| (*s).to_string()).collect(),
..Default::default()
});
}
self
}
pub fn file_max_size(mut self, max_size: impl Into<String>) -> Self {
if let Some(ref mut file) = self.config.file_sink {
file.max_size = max_size.into();
} else {
self.config.file_sink = Some(FileSinkConfig {
max_size: max_size.into(),
..Default::default()
});
}
self
}
pub fn file_compress(mut self, compress: bool) -> Self {
if let Some(ref mut file) = self.config.file_sink {
file.compress = compress;
} else {
self.config.file_sink = Some(FileSinkConfig {
compress,
..Default::default()
});
}
self
}
pub fn file_rotation_time(mut self, rotation: impl Into<String>) -> Self {
if let Some(ref mut file) = self.config.file_sink {
file.rotation_time = rotation.into();
} else {
self.config.file_sink = Some(FileSinkConfig {
rotation_time: rotation.into(),
..Default::default()
});
}
self
}
pub fn file_keep_files(mut self, keep: u32) -> Self {
if let Some(ref mut file) = self.config.file_sink {
file.keep_files = keep;
} else {
self.config.file_sink = Some(FileSinkConfig {
keep_files: keep,
..Default::default()
});
}
self
}
#[cfg(feature = "http")]
pub fn enable_http_server(mut self, enabled: bool) -> Self {
if let Some(ref mut http) = self.config.http_server {
http.enabled = enabled;
} else if enabled {
self.config.http_server = Some(crate::HttpServerConfig {
enabled: true,
..Default::default()
});
}
self
}
#[cfg(feature = "http")]
pub fn http_host(mut self, host: impl Into<String>) -> Self {
if let Some(ref mut http) = self.config.http_server {
http.host = host.into();
} else {
self.config.http_server = Some(crate::HttpServerConfig {
host: host.into(),
..Default::default()
});
}
self
}
#[cfg(feature = "http")]
pub fn http_port(mut self, port: u16) -> Self {
if port == 0 {
self.validation_errors
.push("HTTP port must be between 1 and 65535".to_string());
}
if let Some(ref mut http) = self.config.http_server {
http.port = port;
} else {
self.config.http_server = Some(crate::HttpServerConfig {
port,
..Default::default()
});
}
self
}
#[cfg(feature = "http")]
pub fn http_metrics_path(mut self, path: impl Into<String>) -> Self {
if let Some(ref mut http) = self.config.http_server {
http.metrics_path = path.into();
} else {
self.config.http_server = Some(crate::HttpServerConfig {
metrics_path: path.into(),
..Default::default()
});
}
self
}
#[cfg(feature = "http")]
pub fn http_health_path(mut self, path: impl Into<String>) -> Self {
if let Some(ref mut http) = self.config.http_server {
http.health_path = path.into();
} else {
self.config.http_server = Some(crate::HttpServerConfig {
health_path: path.into(),
..Default::default()
});
}
self
}
#[cfg(feature = "http")]
pub fn http_error_mode(mut self, mode: impl Into<String>) -> Self {
let mode_str = mode.into();
let error_mode = match mode_str.to_lowercase().as_str() {
"warn" => crate::HttpErrorMode::Warn,
"strict" => crate::HttpErrorMode::Strict,
_ => {
self.validation_errors.push(format!(
"Unknown HTTP error mode '{}'. Valid modes: warn, strict. Using default 'strict'.",
mode_str
));
crate::HttpErrorMode::default()
}
};
if let Some(ref mut http) = self.config.http_server {
http.error_mode = error_mode;
} else {
self.config.http_server = Some(crate::HttpServerConfig {
error_mode,
..Default::default()
});
}
self
}
pub fn cache(mut self, cache: Arc<dyn Cache>) -> Self {
self.deps.cache = Some(cache);
self
}
pub fn config(mut self, config: Arc<dyn Config>) -> Self {
self.deps.config = Some(config);
self
}
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
pub fn with_database(mut self, database: Arc<dyn Database>) -> Self {
self.deps.database = Some(database);
self
}
pub async fn build(self) -> Result<LoggerManager, InklogError> {
if !self.validation_errors.is_empty() {
let mut args = fluent_bundle::FluentArgs::new();
args.set("count", self.validation_errors.len());
return Err(InklogError::ConfigError(crate::i18n::tr_args(
"config-builder_validation_failed",
args,
)));
}
let has_deps = self.deps.cache.is_some() || self.deps.config.is_some() || {
#[cfg(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
))]
{
self.deps.database.is_some()
}
#[cfg(not(any(
feature = "sqlite",
feature = "postgres",
feature = "mysql",
feature = "duckdb"
)))]
{
false
}
};
if has_deps {
let mut deps = self.deps;
if deps.config.is_none() {
deps.config = Some(Arc::new(
crate::integrations::infra::InklogConfigAdapter::from_config(
self.config.clone(),
),
));
}
LoggerManager::with_dependencies(deps).await
} else {
LoggerManager::with_config(self.config).await
}
}
}