1mod count;
8mod format;
9mod prune;
10mod query;
11mod stream;
12
13use anyhow::{Context, Result};
14use clap::{Parser, Subcommand, ValueEnum};
15
16use crate::request_log;
17use query::Filter;
18
19pub(crate) use query::parse_time_bound;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
25#[value(rename_all = "lowercase")]
26pub enum Format {
27 Oneline,
29 Json,
31 Full,
33}
34
35#[derive(Parser)]
42pub struct LogCommand {
43 #[command(subcommand)]
45 action: Option<LogAction>,
46 #[arg(long, value_name = "DUR_OR_TS")]
49 since: Option<String>,
50 #[arg(long, value_name = "DUR_OR_TS")]
53 until: Option<String>,
54 #[arg(long, value_name = "METHOD")]
56 method: Option<String>,
57 #[arg(long, value_name = "STATUS")]
59 status: Option<String>,
60 #[arg(long, value_name = "NAME")]
62 service: Option<String>,
63 #[arg(long, value_name = "PATH")]
65 command: Option<String>,
66 #[arg(long, value_name = "SUBSTR")]
68 url: Option<String>,
69 #[arg(long, value_name = "REGEX")]
71 grep: Option<String>,
72 #[arg(long, value_name = "TOKEN")]
74 fuzzy: Vec<String>,
75 #[arg(long, value_name = "EXPR")]
78 query: Vec<String>,
79 #[arg(long, value_name = "ID")]
81 id: Option<String>,
82 #[arg(short = 'o', long, value_enum, default_value_t = Format::Oneline)]
84 output: Format,
85 #[arg(long = "format", hide = true)]
87 format: Option<Format>,
88 #[arg(short = 'n', long, value_name = "N")]
90 limit: Option<usize>,
91 #[arg(short = 'f', long)]
93 follow: bool,
94}
95
96#[derive(Subcommand)]
98enum LogAction {
99 Count(count::CountCommand),
101 Prune(prune::PruneCommand),
103}
104
105impl LogCommand {
106 pub fn execute(mut self) -> Result<()> {
108 if let Some(action) = self.action {
109 return match action {
110 LogAction::Count(cmd) => cmd.execute(),
111 LogAction::Prune(cmd) => cmd.execute(),
112 };
113 }
114 if let Some(format) = self.format.take() {
115 eprintln!("warning: --format is deprecated; use -o/--output instead");
116 self.output = format;
117 }
118 let path = request_log::log_file_path().context("could not resolve the log file path")?;
119 let filter = Filter::build(query::FilterInput {
120 since: self.since.as_deref(),
121 until: self.until.as_deref(),
122 method: self.method.as_deref(),
123 status: self.status.as_deref(),
124 service: self.service.as_deref(),
125 command: self.command.as_deref(),
126 url: self.url.as_deref(),
127 grep: self.grep.as_deref(),
128 fuzzy: &self.fuzzy,
129 query: &self.query,
130 id: self.id.as_deref(),
131 })?;
132 stream::run(&path, &filter, self.output, self.limit, self.follow)
133 }
134}
135
136#[cfg(feature = "mcp")]
143pub(crate) struct SearchRequest<'a> {
144 pub since: Option<&'a str>,
147 pub until: Option<&'a str>,
149 pub method: Option<&'a str>,
151 pub status: Option<&'a str>,
153 pub service: Option<&'a str>,
155 pub command: Option<&'a str>,
157 pub url: Option<&'a str>,
159 pub grep: Option<&'a str>,
161 pub fuzzy: &'a [String],
163 pub query: &'a [String],
165 pub id: Option<&'a str>,
167 pub format: Format,
169 pub limit: Option<usize>,
171}
172
173#[cfg(feature = "mcp")]
181pub(crate) fn run_search_capture(req: SearchRequest<'_>) -> Result<String> {
182 let path = request_log::log_file_path().context("could not resolve the log file path")?;
183 let filter = Filter::build(query::FilterInput {
184 since: req.since,
185 until: req.until,
186 method: req.method,
187 status: req.status,
188 service: req.service,
189 command: req.command,
190 url: req.url,
191 grep: req.grep,
192 fuzzy: req.fuzzy,
193 query: req.query,
194 id: req.id,
195 })?;
196 stream::run_capture(&path, &filter, req.format, req.limit)
197}
198
199#[cfg(test)]
200#[allow(clippy::unwrap_used, clippy::expect_used)]
201mod tests {
202 use super::*;
203
204 #[derive(Parser)]
205 struct Wrapper {
206 #[command(subcommand)]
207 cmd: Wrapped,
208 }
209
210 #[derive(clap::Subcommand)]
211 enum Wrapped {
212 Log(LogCommand),
213 }
214
215 fn parse(args: &[&str]) -> LogCommand {
216 let mut full = vec!["omni-dev", "log"];
217 full.extend_from_slice(args);
218 match Wrapper::try_parse_from(full).unwrap().cmd {
219 Wrapped::Log(cmd) => cmd,
220 }
221 }
222
223 #[test]
224 fn defaults_are_sane() {
225 let cmd = parse(&[]);
226 assert_eq!(cmd.output, Format::Oneline);
227 assert!(cmd.format.is_none());
228 assert!(cmd.limit.is_none());
229 assert!(!cmd.follow);
230 }
231
232 #[test]
233 fn deprecated_format_alias_still_parses() {
234 let cmd = parse(&["--format", "json"]);
235 assert_eq!(cmd.format, Some(Format::Json));
237 assert_eq!(cmd.output, Format::Oneline);
238 }
239
240 #[test]
241 fn parses_full_flag_matrix() {
242 let cmd = parse(&[
243 "--since",
244 "2h",
245 "--method",
246 "GET",
247 "--status",
248 "5xx",
249 "--service",
250 "jira",
251 "--command",
252 "jira read",
253 "--url",
254 "issue",
255 "--grep",
256 "X-\\d+",
257 "--fuzzy",
258 "a",
259 "--fuzzy",
260 "b",
261 "--query",
262 "status:5xx OR method:POST",
263 "--id",
264 "abc",
265 "-o",
266 "json",
267 "-n",
268 "10",
269 "-f",
270 ]);
271 assert_eq!(cmd.since.as_deref(), Some("2h"));
272 assert_eq!(cmd.status.as_deref(), Some("5xx"));
273 assert_eq!(cmd.fuzzy, vec!["a", "b"]);
274 assert_eq!(cmd.query.len(), 1);
275 assert_eq!(cmd.output, Format::Json);
276 assert_eq!(cmd.limit, Some(10));
277 assert!(cmd.follow);
278 }
279}