use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use hdds_logger::{
FileRotation, LogCollector, LogConfig, LogFilter, LogLevel, OutputConfig, OutputFormat,
StopHandle, SyslogFacility,
};
use std::path::PathBuf;
use tracing_subscriber::{fmt, EnvFilter};
#[derive(Parser)]
#[command(name = "hdds-logger")]
#[command(author = "naskel.com")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Distributed logging service for HDDS - aggregate logs from DDS participants")]
#[command(long_about = None)]
struct Cli {
#[arg(short, long, default_value = "0")]
domain: u32,
#[arg(short, long, default_value = "rt/rosout")]
topic: String,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long, value_enum, default_value = "text")]
format: FormatArg,
#[arg(short, long, value_enum, default_value = "info")]
level: LevelArg,
#[arg(long)]
rotate: Option<String>,
#[arg(long, default_value = "5")]
rotate_keep: u32,
#[arg(long)]
syslog: bool,
#[arg(long, value_enum, default_value = "local0")]
facility: FacilityArg,
#[arg(long)]
participant: Option<String>,
#[arg(long)]
node: Option<String>,
#[arg(long, default_value = "true")]
colors: bool,
#[arg(short, long)]
verbose: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum FormatArg {
Text,
Json,
JsonLines,
Syslog,
}
impl From<FormatArg> for OutputFormat {
fn from(arg: FormatArg) -> Self {
match arg {
FormatArg::Text => OutputFormat::Text,
FormatArg::Json => OutputFormat::Json,
FormatArg::JsonLines => OutputFormat::JsonLines,
FormatArg::Syslog => OutputFormat::Syslog,
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum LevelArg {
Debug,
Info,
Warn,
Error,
Fatal,
}
impl From<LevelArg> for LogLevel {
fn from(arg: LevelArg) -> Self {
match arg {
LevelArg::Debug => LogLevel::Debug,
LevelArg::Info => LogLevel::Info,
LevelArg::Warn => LogLevel::Warn,
LevelArg::Error => LogLevel::Error,
LevelArg::Fatal => LogLevel::Fatal,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
enum FacilityArg {
Kern,
User,
Daemon,
Auth,
Syslog,
Local0,
Local1,
Local2,
Local3,
Local4,
Local5,
Local6,
Local7,
}
impl From<FacilityArg> for SyslogFacility {
fn from(arg: FacilityArg) -> Self {
match arg {
FacilityArg::Kern => SyslogFacility::Kern,
FacilityArg::User => SyslogFacility::User,
FacilityArg::Daemon => SyslogFacility::Daemon,
FacilityArg::Auth => SyslogFacility::Auth,
FacilityArg::Syslog => SyslogFacility::Syslog,
FacilityArg::Local0 => SyslogFacility::Local0,
FacilityArg::Local1 => SyslogFacility::Local1,
FacilityArg::Local2 => SyslogFacility::Local2,
FacilityArg::Local3 => SyslogFacility::Local3,
FacilityArg::Local4 => SyslogFacility::Local4,
FacilityArg::Local5 => SyslogFacility::Local5,
FacilityArg::Local6 => SyslogFacility::Local6,
FacilityArg::Local7 => SyslogFacility::Local7,
}
}
}
fn main() -> Result<()> {
let cli = Cli::parse();
let filter = if cli.verbose {
EnvFilter::new("hdds_logger=debug,hdds=debug")
} else {
EnvFilter::new("hdds_logger=info,hdds=warn")
};
fmt().with_env_filter(filter).with_target(false).init();
let config = build_config(&cli)?;
tracing::info!(
domain = cli.domain,
topic = %cli.topic,
format = ?cli.format,
level = ?cli.level,
"Starting HDDS Logger"
);
let mut collector = LogCollector::new(config).context("Failed to create log collector")?;
let stop_handle = collector.stop_handle();
ctrlc_handler(stop_handle);
collector.run().context("Log collector error")?;
let stats = collector.stats();
tracing::info!(
logs_received = stats.logs_received,
logs_written = stats.logs_written,
logs_filtered = stats.logs_filtered,
"Logger shutdown complete"
);
Ok(())
}
fn build_config(cli: &Cli) -> Result<LogConfig> {
let output = if cli.syslog {
OutputConfig::Syslog {
facility: cli.facility.into(),
}
} else if let Some(ref path) = cli.output {
let rotation = cli.rotate.as_ref().map(|size_str| {
let max_size = parse_size(size_str).unwrap_or(10 * 1024 * 1024);
FileRotation {
max_size,
max_files: cli.rotate_keep,
compress: false,
}
});
OutputConfig::File {
path: path.clone(),
rotation,
}
} else {
OutputConfig::Stdout
};
let mut filter = LogFilter::min_level(cli.level.into());
filter.participant_pattern = cli.participant.clone();
filter.node_pattern = cli.node.clone();
Ok(LogConfig {
format: cli.format.into(),
output,
filter,
domain_id: cli.domain,
topic_pattern: cli.topic.clone(),
})
}
fn parse_size(s: &str) -> Option<u64> {
let s = s.trim().to_uppercase();
if let Some(num) = s.strip_suffix('K') {
num.parse::<u64>().ok().map(|n| n * 1024)
} else if let Some(num) = s.strip_suffix('M') {
num.parse::<u64>().ok().map(|n| n * 1024 * 1024)
} else if let Some(num) = s.strip_suffix('G') {
num.parse::<u64>().ok().map(|n| n * 1024 * 1024 * 1024)
} else {
s.parse::<u64>().ok()
}
}
fn ctrlc_handler(stop_handle: StopHandle) {
let _ = ctrlc::set_handler(move || {
tracing::info!("Received Ctrl+C, shutting down...");
stop_handle.stop();
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_size() {
assert_eq!(parse_size("100"), Some(100));
assert_eq!(parse_size("10K"), Some(10 * 1024));
assert_eq!(parse_size("10M"), Some(10 * 1024 * 1024));
assert_eq!(parse_size("1G"), Some(1024 * 1024 * 1024));
assert_eq!(parse_size("10m"), Some(10 * 1024 * 1024)); assert_eq!(parse_size("invalid"), None);
}
}