feat: add --search to just log (#11995)

Summary
- extend the log client to accept an optional `--search` substring
filter when querying codex-state logs
- propagate the filter through `LogQuery` and apply it in
`push_log_filters` via `INSTR(message, ...)`
- add an integration test that exercises the new search filtering
behavior

Testing
- Not run (not requested)
This commit is contained in:
jif-oai
2026-02-17 14:19:52 +00:00
committed by GitHub
parent 56cd85cd4b
commit 31d4bfdde0
3 changed files with 66 additions and 0 deletions

View File

@@ -46,6 +46,10 @@ struct Args {
#[arg(long = "thread-id")]
thread_id: Vec<String>,
/// Substring match against the log message.
#[arg(long)]
search: Option<String>,
/// Include logs that do not have a thread id.
#[arg(long)]
threadless: bool,
@@ -67,6 +71,7 @@ struct LogFilter {
module_like: Vec<String>,
file_like: Vec<String>,
thread_ids: Vec<String>,
search: Option<String>,
include_threadless: bool,
}
@@ -154,6 +159,7 @@ fn build_filter(args: &Args) -> anyhow::Result<LogFilter> {
module_like,
file_like,
thread_ids,
search: args.search.clone(),
include_threadless: args.threadless,
})
}
@@ -233,6 +239,7 @@ fn to_log_query(
module_like: filter.module_like.clone(),
file_like: filter.file_like.clone(),
thread_ids: filter.thread_ids.clone(),
search: filter.search.clone(),
include_threadless: filter.include_threadless,
after_id,
limit,

View File

@@ -37,6 +37,7 @@ pub struct LogQuery {
pub module_like: Vec<String>,
pub file_like: Vec<String>,
pub thread_ids: Vec<String>,
pub search: Option<String>,
pub include_threadless: bool,
pub after_id: Option<i64>,
pub limit: Option<usize>,

View File

@@ -711,6 +711,11 @@ fn push_log_filters<'a>(builder: &mut QueryBuilder<'a, Sqlite>, query: &'a LogQu
if let Some(after_id) = query.after_id {
builder.push(" AND id > ").push_bind(after_id);
}
if let Some(search) = query.search.as_ref() {
builder.push(" AND INSTR(message, ");
builder.push_bind(search.as_str());
builder.push(") > 0");
}
}
fn push_like_filters<'a>(
@@ -906,6 +911,8 @@ mod tests {
use super::StateRuntime;
use super::ThreadMetadata;
use super::state_db_filename;
use crate::LogEntry;
use crate::LogQuery;
use crate::STATE_DB_FILENAME;
use crate::STATE_DB_VERSION;
use crate::model::Phase2JobClaimOutcome;
@@ -2495,6 +2502,57 @@ VALUES (?, ?, ?, ?, ?)
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
#[tokio::test]
async fn query_logs_with_search_matches_substring() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None)
.await
.expect("initialize runtime");
runtime
.insert_logs(&[
LogEntry {
ts: 1_700_000_001,
ts_nanos: 0,
level: "INFO".to_string(),
target: "cli".to_string(),
message: Some("alpha".to_string()),
thread_id: Some("thread-1".to_string()),
process_uuid: None,
file: Some("main.rs".to_string()),
line: Some(42),
module_path: None,
},
LogEntry {
ts: 1_700_000_002,
ts_nanos: 0,
level: "INFO".to_string(),
target: "cli".to_string(),
message: Some("alphabet".to_string()),
thread_id: Some("thread-1".to_string()),
process_uuid: None,
file: Some("main.rs".to_string()),
line: Some(43),
module_path: None,
},
])
.await
.expect("insert test logs");
let rows = runtime
.query_logs(&LogQuery {
search: Some("alphab".to_string()),
..Default::default()
})
.await
.expect("query matching logs");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].message.as_deref(), Some("alphabet"));
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
fn test_thread_metadata(
codex_home: &Path,
thread_id: ThreadId,