use std::path::PathBuf;
pub(crate) const DEFAULT_LEVEL: &str = "info";
pub(crate) const DEFAULT_SERVICE_NAME: &str = "app";
pub(crate) const DEFAULT_FILE_PREFIX: &str = "app";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorLogDetail {
#[default]
TypeOnly,
MessageOnly,
FullChain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogFormat {
#[default]
Auto,
Pretty,
Compact,
Json,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Rotation {
Never,
Hourly,
#[default]
Daily,
}
#[derive(Debug, Clone)]
pub struct FileLogConfig {
pub(crate) directory: PathBuf,
pub(crate) prefix: String,
pub(crate) rotation: Rotation,
pub(crate) non_blocking: bool,
}
impl FileLogConfig {
pub fn new(directory: impl Into<PathBuf>) -> Self {
Self {
directory: directory.into(),
prefix: DEFAULT_FILE_PREFIX.to_owned(),
rotation: Rotation::default(),
non_blocking: true,
}
}
pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
pub fn rotation(mut self, rotation: Rotation) -> Self {
self.rotation = rotation;
self
}
pub fn non_blocking(mut self, non_blocking: bool) -> Self {
self.non_blocking = non_blocking;
self
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TelemetryConfig {
pub(crate) enabled: bool,
pub(crate) otlp_endpoint: String,
pub(crate) service_name: String,
}
impl TelemetryConfig {
pub fn new(otlp_endpoint: impl Into<String>) -> Self {
Self {
enabled: true,
otlp_endpoint: otlp_endpoint.into(),
service_name: DEFAULT_SERVICE_NAME.to_owned(),
}
}
pub fn service_name(mut self, name: impl Into<String>) -> Self {
self.service_name = name.into();
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
#[derive(Debug, Clone)]
pub struct LoggerConfig {
pub(crate) level: String,
pub(crate) format: LogFormat,
pub(crate) color: bool,
pub(crate) service_name: String,
pub(crate) error_detail: ErrorLogDetail,
pub(crate) request_logs: bool,
#[allow(dead_code)]
pub(crate) include_source: bool,
#[allow(dead_code)]
pub(crate) include_thread_ids: bool,
pub(crate) non_blocking: bool,
pub(crate) file: Option<FileLogConfig>,
#[allow(dead_code)]
pub(crate) telemetry: Option<TelemetryConfig>,
}
impl Default for LoggerConfig {
fn default() -> Self {
Self {
level: DEFAULT_LEVEL.to_owned(),
format: LogFormat::Auto,
color: true,
service_name: DEFAULT_SERVICE_NAME.to_owned(),
error_detail: ErrorLogDetail::default(),
request_logs: true,
include_source: false,
include_thread_ids: false,
non_blocking: false,
file: None,
telemetry: None,
}
}
}
impl LoggerConfig {
pub fn new() -> Self {
Self::default()
}
pub fn level(mut self, level: impl Into<String>) -> Self {
self.level = level.into();
self
}
pub fn format(mut self, format: LogFormat) -> Self {
self.format = format;
self
}
pub fn color(mut self, color: bool) -> Self {
self.color = color;
self
}
pub fn service_name(mut self, name: impl Into<String>) -> Self {
self.service_name = name.into();
self
}
pub fn error_detail(mut self, detail: ErrorLogDetail) -> Self {
self.error_detail = detail;
self
}
pub fn request_logs(mut self, enabled: bool) -> Self {
self.request_logs = enabled;
self
}
pub fn include_source(mut self, include: bool) -> Self {
self.include_source = include;
self
}
pub fn include_thread_ids(mut self, include: bool) -> Self {
self.include_thread_ids = include;
self
}
pub fn non_blocking(mut self, non_blocking: bool) -> Self {
self.non_blocking = non_blocking;
self
}
pub fn file(mut self, file: FileLogConfig) -> Self {
self.file = Some(file);
self
}
pub fn telemetry(mut self, telemetry: TelemetryConfig) -> Self {
self.telemetry = Some(telemetry);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_sensible() {
let config = LoggerConfig::new();
assert_eq!(config.level, "info");
assert_eq!(config.format, LogFormat::Auto);
assert!(config.color);
assert_eq!(config.error_detail, ErrorLogDetail::TypeOnly);
assert!(config.request_logs);
assert!(config.file.is_none());
assert!(config.telemetry.is_none());
}
#[test]
fn builders_set_fields() {
let config = LoggerConfig::new()
.level("debug")
.format(LogFormat::Json)
.service_name("tork-api")
.error_detail(ErrorLogDetail::FullChain)
.request_logs(false)
.include_source(true)
.include_thread_ids(true)
.non_blocking(true)
.file(
FileLogConfig::new("./logs")
.prefix("api")
.rotation(Rotation::Hourly),
);
assert_eq!(config.level, "debug");
assert_eq!(config.format, LogFormat::Json);
assert_eq!(config.service_name, "tork-api");
assert_eq!(config.error_detail, ErrorLogDetail::FullChain);
assert!(!config.request_logs);
assert!(config.include_source);
assert!(config.include_thread_ids);
assert!(config.non_blocking);
let file = config.file.expect("file sink");
assert_eq!(file.prefix, "api");
assert_eq!(file.rotation, Rotation::Hourly);
}
#[test]
fn file_and_telemetry_builders_cover_all_fields() {
let file = FileLogConfig::new("./logs")
.prefix("svc")
.rotation(Rotation::Never)
.non_blocking(false);
assert_eq!(file.directory, PathBuf::from("./logs"));
assert_eq!(file.prefix, "svc");
assert_eq!(file.rotation, Rotation::Never);
assert!(!file.non_blocking);
let telemetry = TelemetryConfig::new("http://localhost:4317")
.service_name("tork-api")
.enabled(false);
assert!(!telemetry.enabled);
assert_eq!(telemetry.otlp_endpoint, "http://localhost:4317");
assert_eq!(telemetry.service_name, "tork-api");
}
#[test]
fn log_format_and_rotation_deserialize_from_lowercase() {
let format: LogFormat = serde_json::from_str("\"json\"").unwrap();
assert_eq!(format, LogFormat::Json);
let format: LogFormat = serde_json::from_str("\"auto\"").unwrap();
assert_eq!(format, LogFormat::Auto);
let format: LogFormat = serde_json::from_str("\"pretty\"").unwrap();
assert_eq!(format, LogFormat::Pretty);
let format: LogFormat = serde_json::from_str("\"compact\"").unwrap();
assert_eq!(format, LogFormat::Compact);
let rotation: Rotation = serde_json::from_str("\"never\"").unwrap();
assert_eq!(rotation, Rotation::Never);
let rotation: Rotation = serde_json::from_str("\"hourly\"").unwrap();
assert_eq!(rotation, Rotation::Hourly);
let rotation: Rotation = serde_json::from_str("\"daily\"").unwrap();
assert_eq!(rotation, Rotation::Daily);
}
}