use clap::Parser;
use logger2::cli::Cli;
use logger2::color::{parse_hex_color, should_use_color};
use logger2::config::Config;
use logger2::error::{Logger2Error, Result};
use logger2::level::{Level, Priority};
use logger2::output::RenderContext;
use logger2::{output, parse_kv, syslog};
use std::io::{IsTerminal, Read, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;
use clap_version_flag::colorful_version;
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
if args.len() == 2 && (args[1] == "-V" || args[1] == "--version") {
let version = colorful_version!();
version.print_and_exit();
}
let cli = Cli::parse();
match run(cli) {
Ok(code) => code,
Err(e) => {
eprintln!("logger2: error: {e}");
ExitCode::FAILURE
}
}
}
fn run(cli: Cli) -> Result<ExitCode> {
if cli.init_config {
let path = match &cli.config {
Some(p) => p.clone(),
None => Config::default_write_path()?,
};
if path.exists() {
eprintln!(
"logger2: refusing to overwrite existing config at {} (remove it first)",
path.display()
);
return Ok(ExitCode::FAILURE);
}
Config::init_default(&path)?;
println!("logger2: wrote default config to {}", path.display());
return Ok(ExitCode::SUCCESS);
}
let (mut config, loaded_from) = Config::load(cli.config.as_deref())?;
if cli.verbose > 0 {
match &loaded_from {
Some(p) => eprintln!("logger2: loaded config from {}", p.display()),
None => eprintln!("logger2: no config file found, using built-in defaults"),
}
}
if cli.print_config {
print!("{}", config.to_toml_string()?);
return Ok(ExitCode::SUCCESS);
}
if let Some(fmt) = &cli.timestamp_format {
config.general.timestamp_format = fmt.clone();
}
if let Some(server) = &cli.server {
config.network.server = server.clone();
}
if let Some(port) = cli.port {
config.network.port = port;
}
if let Some(proto) = cli.protocol_override() {
config.network.protocol = proto;
}
if let Some(socket) = &cli.socket {
config.network.socket = socket.display().to_string();
}
if cli.rfc5424 {
config.network.rfc5424 = true;
}
if cli.octet_count {
config.network.octet_count = true;
}
if let Some(size) = cli.size {
config.general.max_message_size = size;
}
if cli.stderr {
config.output.stderr = true;
}
if cli.json {
config.output.json = true;
}
let mut priority: Priority = match &cli.priority {
Some(p) => p.parse()?,
None => config.general.default_priority.parse()?,
};
if let Some(level_str) = &cli.level {
priority.level = level_str.parse::<Level>()?;
}
let level = priority.level;
let message = resolve_message(&cli)?;
if message.len() > config.general.max_message_size {
return Err(Logger2Error::MessageTooLarge(
message.len(),
config.general.max_message_size,
));
}
let tag = cli.tag.clone().unwrap_or_else(|| {
if !config.general.default_tag.is_empty() {
config.general.default_tag.clone()
} else {
whoami::username()
}
});
let pid: Option<u32> = if cli.id.is_some() || config.general.show_pid {
match cli.id.as_deref() {
Some("self") | None => Some(std::process::id()),
Some(other) => other.parse::<u32>().ok().or(Some(std::process::id())),
}
} else {
None
};
let mut fields = Vec::with_capacity(cli.kv.len());
for kv in &cli.kv {
fields.push(parse_kv(kv)?);
}
let color_override = match &cli.color {
Some(c) => Some(parse_hex_color(c)?),
None => None,
};
let emoji_override = cli.emoji.as_deref();
let force = if cli.no_color {
Some(false)
} else if cli.force_color {
Some(true)
} else {
None
};
let stdout_is_tty = std::io::stdout().is_terminal();
let stderr_is_tty = std::io::stderr().is_terminal();
let use_color_stdout = config.colors.enabled && should_use_color(force, stdout_is_tty);
let use_color_stderr = config.colors.enabled && should_use_color(force, stderr_is_tty);
let use_emoji = config.emoji.enabled && !cli.no_emoji;
let json_mode = config.output.json;
if json_mode {
let ctx = RenderContext {
level,
tag: &tag,
pid,
message: &message,
fields: &fields,
use_color: false,
use_emoji,
color_override,
emoji_override,
};
let line = output::render_json(&ctx)?;
println!("{line}");
} else if config.output.stderr || cli.dry_run {
let ctx = RenderContext {
level,
tag: &tag,
pid,
message: &message,
fields: &fields,
use_color: use_color_stderr,
use_emoji,
color_override,
emoji_override,
};
let line = output::render_console(&ctx, &config);
eprintln!("{line}");
let _ = use_color_stdout;
}
if cli.dry_run {
if !cli.quiet {
eprintln!("logger2: dry-run, nothing was sent to syslog");
}
return Ok(ExitCode::SUCCESS);
}
if config.output.syslog {
let hostname = whoami::fallible::hostname().unwrap_or_else(|_| "localhost".to_string());
let local_socket = PathBuf::from(&config.network.socket);
let server = if config.network.server.is_empty() {
None
} else {
Some(config.network.server.as_str())
};
let dest = syslog::Destination {
server,
port: config.network.port,
protocol: config.network.protocol,
local_socket: &local_socket,
rfc5424: config.network.rfc5424,
octet_count: config.network.octet_count,
connect_timeout: Duration::from_millis(config.network.connect_timeout_ms),
};
let msg = syslog::SyslogMessage {
priority,
hostname: &hostname,
tag: &tag,
pid,
message: &message,
structured_data: &fields,
};
match syslog::deliver(&dest, &msg) {
Ok(wire) => {
if cli.verbose > 1 {
eprintln!("logger2: sent: {wire}");
}
}
Err(e) => {
eprintln!("logger2: warning: failed to deliver to syslog: {e}");
return Ok(ExitCode::from(1));
}
}
}
Ok(ExitCode::SUCCESS)
}
fn resolve_message(cli: &Cli) -> Result<String> {
if !cli.message.is_empty() {
return Ok(cli.message.join(" "));
}
if let Some(path) = &cli.file {
let content = std::fs::read_to_string(path)?;
return Ok(content.trim_end_matches(['\n', '\r']).to_string());
}
if !std::io::stdin().is_terminal() {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
let trimmed = buf.trim_end_matches(['\n', '\r']).to_string();
if !trimmed.is_empty() {
return Ok(trimmed);
}
}
let _ = std::io::stdout().flush();
Err(Logger2Error::NoMessage)
}