mod format;
mod prune;
mod query;
mod stream;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand, ValueEnum};
use crate::request_log;
use query::Filter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "lowercase")]
pub enum Format {
Oneline,
Json,
Full,
}
#[derive(Parser)]
pub struct LogCommand {
#[command(subcommand)]
action: Option<LogAction>,
#[arg(long, value_name = "DUR_OR_TS")]
since: Option<String>,
#[arg(long, value_name = "DUR_OR_TS")]
until: Option<String>,
#[arg(long, value_name = "METHOD")]
method: Option<String>,
#[arg(long, value_name = "STATUS")]
status: Option<String>,
#[arg(long, value_name = "NAME")]
service: Option<String>,
#[arg(long, value_name = "PATH")]
command: Option<String>,
#[arg(long, value_name = "SUBSTR")]
url: Option<String>,
#[arg(long, value_name = "REGEX")]
grep: Option<String>,
#[arg(long, value_name = "TOKEN")]
fuzzy: Vec<String>,
#[arg(long, value_name = "EXPR")]
query: Vec<String>,
#[arg(long, value_name = "ID")]
id: Option<String>,
#[arg(short = 'o', long, value_enum, default_value_t = Format::Oneline)]
output: Format,
#[arg(long = "format", hide = true)]
format: Option<Format>,
#[arg(short = 'n', long, value_name = "N")]
limit: Option<usize>,
#[arg(short = 'f', long)]
follow: bool,
}
#[derive(Subcommand)]
enum LogAction {
Prune(prune::PruneCommand),
}
impl LogCommand {
pub fn execute(mut self) -> Result<()> {
if let Some(action) = self.action {
return match action {
LogAction::Prune(cmd) => cmd.execute(),
};
}
if let Some(format) = self.format.take() {
eprintln!("warning: --format is deprecated; use -o/--output instead");
self.output = format;
}
let path = request_log::log_file_path().context("could not resolve the log file path")?;
let filter = Filter::build(query::FilterInput {
since: self.since.as_deref(),
until: self.until.as_deref(),
method: self.method.as_deref(),
status: self.status.as_deref(),
service: self.service.as_deref(),
command: self.command.as_deref(),
url: self.url.as_deref(),
grep: self.grep.as_deref(),
fuzzy: &self.fuzzy,
query: &self.query,
id: self.id.as_deref(),
})?;
stream::run(&path, &filter, self.output, self.limit, self.follow)
}
}
#[cfg(feature = "mcp")]
pub(crate) struct SearchRequest<'a> {
pub since: Option<&'a str>,
pub until: Option<&'a str>,
pub method: Option<&'a str>,
pub status: Option<&'a str>,
pub service: Option<&'a str>,
pub command: Option<&'a str>,
pub url: Option<&'a str>,
pub grep: Option<&'a str>,
pub fuzzy: &'a [String],
pub query: &'a [String],
pub id: Option<&'a str>,
pub format: Format,
pub limit: Option<usize>,
}
#[cfg(feature = "mcp")]
pub(crate) fn run_search_capture(req: SearchRequest<'_>) -> Result<String> {
let path = request_log::log_file_path().context("could not resolve the log file path")?;
let filter = Filter::build(query::FilterInput {
since: req.since,
until: req.until,
method: req.method,
status: req.status,
service: req.service,
command: req.command,
url: req.url,
grep: req.grep,
fuzzy: req.fuzzy,
query: req.query,
id: req.id,
})?;
stream::run_capture(&path, &filter, req.format, req.limit)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[derive(Parser)]
struct Wrapper {
#[command(subcommand)]
cmd: Wrapped,
}
#[derive(clap::Subcommand)]
enum Wrapped {
Log(LogCommand),
}
fn parse(args: &[&str]) -> LogCommand {
let mut full = vec!["omni-dev", "log"];
full.extend_from_slice(args);
match Wrapper::try_parse_from(full).unwrap().cmd {
Wrapped::Log(cmd) => cmd,
}
}
#[test]
fn defaults_are_sane() {
let cmd = parse(&[]);
assert_eq!(cmd.output, Format::Oneline);
assert!(cmd.format.is_none());
assert!(cmd.limit.is_none());
assert!(!cmd.follow);
}
#[test]
fn deprecated_format_alias_still_parses() {
let cmd = parse(&["--format", "json"]);
assert_eq!(cmd.format, Some(Format::Json));
assert_eq!(cmd.output, Format::Oneline);
}
#[test]
fn parses_full_flag_matrix() {
let cmd = parse(&[
"--since",
"2h",
"--method",
"GET",
"--status",
"5xx",
"--service",
"jira",
"--command",
"jira read",
"--url",
"issue",
"--grep",
"X-\\d+",
"--fuzzy",
"a",
"--fuzzy",
"b",
"--query",
"status:5xx OR method:POST",
"--id",
"abc",
"-o",
"json",
"-n",
"10",
"-f",
]);
assert_eq!(cmd.since.as_deref(), Some("2h"));
assert_eq!(cmd.status.as_deref(), Some("5xx"));
assert_eq!(cmd.fuzzy, vec!["a", "b"]);
assert_eq!(cmd.query.len(), 1);
assert_eq!(cmd.output, Format::Json);
assert_eq!(cmd.limit, Some(10));
assert!(cmd.follow);
}
}