1#![forbid(unsafe_code)]
14#![deny(missing_docs)]
15
16use clap::{CommandFactory, Parser, ValueEnum};
17use rlg::log_format::LogFormat;
18use rlg::log_level::LogLevel;
19use rlg_cli::{Filter, parse_record, render};
20use std::fs::File;
21use std::io::{self, BufRead, BufReader, Write};
22use std::path::PathBuf;
23
24#[derive(Parser, Debug)]
26#[command(name = "rlg", version, about, long_about = None)]
27struct Cli {
28 #[arg(long, exclusive = true)]
30 manpage: bool,
31
32 #[arg(long, value_name = "SHELL", exclusive = true)]
34 completions: Option<clap_complete::Shell>,
35
36 input: Option<PathBuf>,
38
39 #[arg(short, long, value_enum, default_value_t = OutputFormat::Logfmt)]
42 format: OutputFormat,
43
44 #[arg(short = 'l', long)]
46 min_level: Option<LevelArg>,
47
48 #[arg(short = 'c', long)]
50 component: Option<String>,
51
52 #[arg(short = 'a', long)]
56 attr: Option<String>,
57}
58
59#[derive(Copy, Clone, Debug, ValueEnum)]
61enum OutputFormat {
62 Clf,
63 Cef,
64 Elf,
65 W3c,
66 Apache,
67 Log4jXml,
68 Json,
69 Gelf,
70 Logstash,
71 Ndjson,
72 Mcp,
73 Otlp,
74 Logfmt,
75 Ecs,
76}
77
78impl From<OutputFormat> for LogFormat {
79 fn from(o: OutputFormat) -> Self {
80 match o {
81 OutputFormat::Clf => Self::CLF,
82 OutputFormat::Cef => Self::CEF,
83 OutputFormat::Elf => Self::ELF,
84 OutputFormat::W3c => Self::W3C,
85 OutputFormat::Apache => Self::ApacheAccessLog,
86 OutputFormat::Log4jXml => Self::Log4jXML,
87 OutputFormat::Json => Self::JSON,
88 OutputFormat::Gelf => Self::GELF,
89 OutputFormat::Logstash => Self::Logstash,
90 OutputFormat::Ndjson => Self::NDJSON,
91 OutputFormat::Mcp => Self::MCP,
92 OutputFormat::Otlp => Self::OTLP,
93 OutputFormat::Logfmt => Self::Logfmt,
94 OutputFormat::Ecs => Self::ECS,
95 }
96 }
97}
98
99#[derive(Copy, Clone, Debug, ValueEnum)]
101enum LevelArg {
102 Trace,
103 Debug,
104 Verbose,
105 Info,
106 Warn,
107 Error,
108 Fatal,
109 Critical,
110}
111
112impl From<LevelArg> for LogLevel {
113 fn from(l: LevelArg) -> Self {
114 match l {
115 LevelArg::Trace => Self::TRACE,
116 LevelArg::Debug => Self::DEBUG,
117 LevelArg::Verbose => Self::VERBOSE,
118 LevelArg::Info => Self::INFO,
119 LevelArg::Warn => Self::WARN,
120 LevelArg::Error => Self::ERROR,
121 LevelArg::Fatal => Self::FATAL,
122 LevelArg::Critical => Self::CRITICAL,
123 }
124 }
125}
126
127fn build_filter(cli: &Cli) -> anyhow::Result<Filter> {
128 let mut filter = Filter::new();
129 if let Some(level) = cli.min_level {
130 filter = filter.min_level(level.into());
131 }
132 if let Some(comp) = &cli.component {
133 filter = filter.component(comp);
134 }
135 if let Some(spec) = &cli.attr {
136 let (k, raw) = spec.split_once('=').ok_or_else(|| {
137 anyhow::anyhow!(
138 "--attr must be `key=value` (got: `{spec}`)"
139 )
140 })?;
141 let v: serde_json::Value = serde_json::from_str(raw)
142 .unwrap_or_else(|_| {
143 serde_json::Value::String(raw.to_string())
144 });
145 filter = filter.attribute(k, v);
146 }
147 Ok(filter)
148}
149
150fn run<R: BufRead, W: Write>(
151 reader: R,
152 mut writer: W,
153 filter: &Filter,
154 format: LogFormat,
155) -> anyhow::Result<()> {
156 for line in reader.lines() {
157 let line = line?;
158 if line.trim().is_empty() {
159 continue;
160 }
161 let Ok(record) = parse_record(&line) else {
162 writeln!(writer, "{line}")?;
166 continue;
167 };
168 if !filter.matches(&record) {
169 continue;
170 }
171 writeln!(writer, "{}", render(record, format))?;
172 }
173 Ok(())
174}
175
176fn print_generated(cli: &Cli) -> io::Result<bool> {
179 if cli.manpage {
180 clap_mangen::Man::new(Cli::command())
181 .render(&mut io::stdout())?;
182 return Ok(true);
183 }
184 if let Some(shell) = cli.completions {
185 clap_complete::generate(
186 shell,
187 &mut Cli::command(),
188 "rlg",
189 &mut io::stdout(),
190 );
191 return Ok(true);
192 }
193 Ok(false)
194}
195
196fn main() -> anyhow::Result<()> {
197 let cli = Cli::parse();
198 if print_generated(&cli)? {
199 return Ok(());
200 }
201 let filter = build_filter(&cli)?;
202 let format: LogFormat = cli.format.into();
203
204 let stdout = io::stdout();
205 let writer = stdout.lock();
206
207 match cli.input.as_ref() {
208 Some(path) => {
209 let file = File::open(path)?;
210 run(BufReader::new(file), writer, &filter, format)
211 }
212 None => {
213 let stdin = io::stdin();
214 run(stdin.lock(), writer, &filter, format)
215 }
216 }
217}