1#![forbid(unsafe_code)]
14#![deny(missing_docs)]
15
16use clap::{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 input: Option<PathBuf>,
30
31 #[arg(short, long, value_enum, default_value_t = OutputFormat::Logfmt)]
34 format: OutputFormat,
35
36 #[arg(short = 'l', long)]
38 min_level: Option<LevelArg>,
39
40 #[arg(short = 'c', long)]
42 component: Option<String>,
43
44 #[arg(short = 'a', long)]
48 attr: Option<String>,
49}
50
51#[derive(Copy, Clone, Debug, ValueEnum)]
53#[allow(missing_docs)]
54enum OutputFormat {
55 Clf,
56 Cef,
57 Elf,
58 W3c,
59 Apache,
60 Log4jXml,
61 Json,
62 Gelf,
63 Logstash,
64 Ndjson,
65 Mcp,
66 Otlp,
67 Logfmt,
68 Ecs,
69}
70
71impl From<OutputFormat> for LogFormat {
72 fn from(o: OutputFormat) -> Self {
73 match o {
74 OutputFormat::Clf => Self::CLF,
75 OutputFormat::Cef => Self::CEF,
76 OutputFormat::Elf => Self::ELF,
77 OutputFormat::W3c => Self::W3C,
78 OutputFormat::Apache => Self::ApacheAccessLog,
79 OutputFormat::Log4jXml => Self::Log4jXML,
80 OutputFormat::Json => Self::JSON,
81 OutputFormat::Gelf => Self::GELF,
82 OutputFormat::Logstash => Self::Logstash,
83 OutputFormat::Ndjson => Self::NDJSON,
84 OutputFormat::Mcp => Self::MCP,
85 OutputFormat::Otlp => Self::OTLP,
86 OutputFormat::Logfmt => Self::Logfmt,
87 OutputFormat::Ecs => Self::ECS,
88 }
89 }
90}
91
92#[derive(Copy, Clone, Debug, ValueEnum)]
94#[allow(missing_docs)]
95enum LevelArg {
96 Trace,
97 Debug,
98 Verbose,
99 Info,
100 Warn,
101 Error,
102 Fatal,
103 Critical,
104}
105
106impl From<LevelArg> for LogLevel {
107 fn from(l: LevelArg) -> Self {
108 match l {
109 LevelArg::Trace => Self::TRACE,
110 LevelArg::Debug => Self::DEBUG,
111 LevelArg::Verbose => Self::VERBOSE,
112 LevelArg::Info => Self::INFO,
113 LevelArg::Warn => Self::WARN,
114 LevelArg::Error => Self::ERROR,
115 LevelArg::Fatal => Self::FATAL,
116 LevelArg::Critical => Self::CRITICAL,
117 }
118 }
119}
120
121fn build_filter(cli: &Cli) -> anyhow::Result<Filter> {
122 let mut filter = Filter::new();
123 if let Some(level) = cli.min_level {
124 filter = filter.min_level(level.into());
125 }
126 if let Some(comp) = &cli.component {
127 filter = filter.component(comp);
128 }
129 if let Some(spec) = &cli.attr {
130 let (k, raw) = spec.split_once('=').ok_or_else(|| {
131 anyhow::anyhow!(
132 "--attr must be `key=value` (got: `{spec}`)"
133 )
134 })?;
135 let v: serde_json::Value = serde_json::from_str(raw)
136 .unwrap_or_else(|_| {
137 serde_json::Value::String(raw.to_string())
138 });
139 filter = filter.attribute(k, v);
140 }
141 Ok(filter)
142}
143
144fn run<R: BufRead, W: Write>(
145 reader: R,
146 mut writer: W,
147 filter: &Filter,
148 format: LogFormat,
149) -> anyhow::Result<()> {
150 for line in reader.lines() {
151 let line = line?;
152 if line.trim().is_empty() {
153 continue;
154 }
155 let Ok(record) = parse_record(&line) else {
156 writeln!(writer, "{line}")?;
160 continue;
161 };
162 if !filter.matches(&record) {
163 continue;
164 }
165 writeln!(writer, "{}", render(record, format))?;
166 }
167 Ok(())
168}
169
170fn main() -> anyhow::Result<()> {
171 let cli = Cli::parse();
172 let filter = build_filter(&cli)?;
173 let format: LogFormat = cli.format.into();
174
175 let stdout = io::stdout();
176 let writer = stdout.lock();
177
178 match cli.input.as_ref() {
179 Some(path) => {
180 let file = File::open(path)?;
181 run(BufReader::new(file), writer, &filter, format)
182 }
183 None => {
184 let stdin = io::stdin();
185 run(stdin.lock(), writer, &filter, format)
186 }
187 }
188}