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