use crate::color::HexColor;
use crate::error::{Logger2Error, Result};
use crate::level::Level;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
pub default_priority: String,
pub default_tag: String,
pub timestamp_format: String,
pub show_pid: bool,
pub max_message_size: usize,
}
impl Default for GeneralConfig {
fn default() -> Self {
GeneralConfig {
default_priority: "user.notice".to_string(),
default_tag: String::new(),
timestamp_format: "%Y-%m-%d %H:%M:%S%.3f".to_string(),
show_pid: false,
max_message_size: 8192,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OutputConfig {
pub stderr: bool,
pub syslog: bool,
pub json: bool,
pub template: String,
}
impl Default for OutputConfig {
fn default() -> Self {
OutputConfig {
stderr: true,
syslog: true,
json: false,
template: "{timestamp} {emoji} {level} [{tag}] {message}".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Protocol {
Udp,
Tcp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NetworkConfig {
pub server: String,
pub port: u16,
pub protocol: Protocol,
pub socket: String,
pub rfc5424: bool,
pub octet_count: bool,
pub connect_timeout_ms: u64,
}
impl Default for NetworkConfig {
fn default() -> Self {
NetworkConfig {
server: String::new(),
port: 514,
protocol: Protocol::Udp,
socket: default_local_socket(),
rfc5424: false,
octet_count: false,
connect_timeout_ms: 2000,
}
}
}
#[cfg(target_os = "macos")]
fn default_local_socket() -> String {
"/var/run/syslog".to_string()
}
#[cfg(all(unix, not(target_os = "macos")))]
fn default_local_socket() -> String {
"/dev/log".to_string()
}
#[cfg(not(unix))]
fn default_local_socket() -> String {
String::new()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ColorsConfig {
pub enabled: bool,
pub levels: HashMap<String, HexColor>,
pub tag: HexColor,
pub timestamp: HexColor,
pub message: HexColor,
}
impl Default for ColorsConfig {
fn default() -> Self {
let mut levels = HashMap::new();
levels.insert("trace".into(), HexColor::new(0x80, 0x80, 0x80));
levels.insert("debug".into(), HexColor::new(0x00, 0xAF, 0xFF));
levels.insert("info".into(), HexColor::new(0x00, 0xFF, 0x00));
levels.insert("notice".into(), HexColor::new(0x00, 0xFF, 0xFF));
levels.insert("warning".into(), HexColor::new(0xFF, 0xFF, 0x00));
levels.insert("error".into(), HexColor::new(0xFF, 0x55, 0x55));
levels.insert("critical".into(), HexColor::new(0xFF, 0x00, 0xFF));
levels.insert("alert".into(), HexColor::new(0xFF, 0x00, 0x00));
levels.insert("emergency".into(), HexColor::new(0xFF, 0xFF, 0xFF));
ColorsConfig {
enabled: true,
levels,
tag: HexColor::new(0x87, 0xAF, 0xFF),
timestamp: HexColor::new(0x80, 0x80, 0x80),
message: HexColor::new(0xFF, 0xFF, 0xFF),
}
}
}
impl ColorsConfig {
pub fn for_level(&self, level: Level) -> HexColor {
self.levels
.get(level.as_str())
.copied()
.unwrap_or(HexColor::new(0xFF, 0xFF, 0xFF))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EmojiConfig {
pub enabled: bool,
pub levels: HashMap<String, String>,
}
impl Default for EmojiConfig {
fn default() -> Self {
let mut levels = HashMap::new();
levels.insert("trace".into(), "🔍".into());
levels.insert("debug".into(), "🐛".into());
levels.insert("info".into(), "ℹ️".into());
levels.insert("notice".into(), "📝".into());
levels.insert("warning".into(), "⚠️".into());
levels.insert("error".into(), "❌".into());
levels.insert("critical".into(), "🔥".into());
levels.insert("alert".into(), "🚨".into());
levels.insert("emergency".into(), "💀".into());
EmojiConfig {
enabled: true,
levels,
}
}
}
impl EmojiConfig {
pub fn for_level(&self, level: Level) -> &str {
self.levels
.get(level.as_str())
.map(String::as_str)
.unwrap_or("")
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub general: GeneralConfig,
pub output: OutputConfig,
pub network: NetworkConfig,
pub colors: ColorsConfig,
pub emoji: EmojiConfig,
}
impl Config {
pub fn candidate_paths(explicit: Option<&Path>) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(p) = explicit {
paths.push(p.to_path_buf());
return paths;
}
if let Ok(env_path) = std::env::var("LOGGER2_CONFIG") {
paths.push(PathBuf::from(env_path));
}
if let Some(config_dir) = dirs::config_dir() {
paths.push(config_dir.join("logger2").join("config.toml"));
}
if let Some(home) = dirs::home_dir() {
paths.push(home.join(".logger2.toml"));
}
#[cfg(unix)]
paths.push(PathBuf::from("/etc/logger2/config.toml"));
paths
}
pub fn load(explicit: Option<&Path>) -> Result<(Config, Option<PathBuf>)> {
for path in Self::candidate_paths(explicit) {
if path.is_file() {
let text = std::fs::read_to_string(&path)?;
let cfg: Config = toml::from_str(&text)
.map_err(|e| Logger2Error::Config(format!("{}: {}", path.display(), e)))?;
return Ok((cfg, Some(path)));
}
}
if explicit.is_some() {
return Err(Logger2Error::Config(format!(
"config file not found: {}",
explicit.unwrap().display()
)));
}
Ok((Config::default(), None))
}
pub fn to_toml_string(&self) -> Result<String> {
Ok(toml::to_string_pretty(self)?)
}
pub fn init_default(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let cfg = Config::default();
let toml_body = cfg.to_toml_string()?;
let banner = "# logger2 configuration file\n\
# See https://github.com/licface/logger2 for full documentation.\n\
# Command-line flags always override values from this file.\n\n";
std::fs::write(path, format!("{banner}{toml_body}"))?;
Ok(())
}
pub fn default_write_path() -> Result<PathBuf> {
let dir = dirs::config_dir()
.ok_or_else(|| Logger2Error::Config("could not determine config directory".into()))?
.join("logger2");
Ok(dir.join("config.toml"))
}
}