Search for exceptions
This commit is contained in:
22
src/main.rs
22
src/main.rs
@@ -32,6 +32,8 @@ enum Command {
|
|||||||
Signature(SignatureArgs),
|
Signature(SignatureArgs),
|
||||||
/// Search log file for lines matching a query and print timestamp + message
|
/// Search log file for lines matching a query and print timestamp + message
|
||||||
Search(SearchArgs),
|
Search(SearchArgs),
|
||||||
|
/// Search for Exception lines with expand, filtered by app signature
|
||||||
|
SearchExceptions(SearchExceptionsArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -93,6 +95,21 @@ struct SearchArgs {
|
|||||||
threads: usize,
|
threads: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct SearchExceptionsArgs {
|
||||||
|
/// Log file to search
|
||||||
|
#[arg(long)]
|
||||||
|
file: PathBuf,
|
||||||
|
|
||||||
|
/// Filter results to sessions with signature:<APP> only (repeatable)
|
||||||
|
#[arg(long, required = true)]
|
||||||
|
app: Vec<String>,
|
||||||
|
|
||||||
|
/// Number of parallel threads (0 = use all available cores, 1 = sequential)
|
||||||
|
#[arg(long, default_value = "0")]
|
||||||
|
threads: usize,
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_date(s: &str) -> Result<NaiveDate> {
|
fn parse_date(s: &str) -> Result<NaiveDate> {
|
||||||
NaiveDate::parse_from_str(s, "%Y/%m/%d")
|
NaiveDate::parse_from_str(s, "%Y/%m/%d")
|
||||||
.map_err(|e| anyhow!("Invalid date format '{}': {}. Expected YYYY/mm/dd", s, e))
|
.map_err(|e| anyhow!("Invalid date format '{}': {}. Expected YYYY/mm/dd", s, e))
|
||||||
@@ -110,6 +127,11 @@ fn main() -> Result<()> {
|
|||||||
search_args.expand,
|
search_args.expand,
|
||||||
search_args.threads,
|
search_args.threads,
|
||||||
),
|
),
|
||||||
|
Command::SearchExceptions(args) => search::run_search_exceptions(
|
||||||
|
args.file.to_str().unwrap(),
|
||||||
|
&args.app,
|
||||||
|
args.threads,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
144
src/search.rs
144
src/search.rs
@@ -23,6 +23,11 @@ static SESSION_ID_RE: LazyLock<Regex> =
|
|||||||
static SESSION_DESTROYED_RE: LazyLock<Regex> =
|
static SESSION_DESTROYED_RE: LazyLock<Regex> =
|
||||||
LazyLock::new(|| Regex::new(r"sessionDestroyed\b.*?\bsid=([^,\s]+)").unwrap());
|
LazyLock::new(|| Regex::new(r"sessionDestroyed\b.*?\bsid=([^,\s]+)").unwrap());
|
||||||
|
|
||||||
|
/// Extracts the app name from any signature line format.
|
||||||
|
/// Matches both `msg="signature:APP/..."` and `msg="MOBILE_CLIENT_LOG: signature:APP/..."`.
|
||||||
|
static SIGNATURE_APP_RE: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"signature:([^/\s]+)/").unwrap());
|
||||||
|
|
||||||
/// Matches changeSessionId messages and captures the long-form new and old session IDs.
|
/// Matches changeSessionId messages and captures the long-form new and old session IDs.
|
||||||
/// Example: changeSessionId: newSessionId: sDF080BBD / DF080BBD...node011 replaces oldSessionId: sF9EE9D52 / F9EE9D52...node011
|
/// Example: changeSessionId: newSessionId: sDF080BBD / DF080BBD...node011 replaces oldSessionId: sF9EE9D52 / F9EE9D52...node011
|
||||||
static CHANGE_SESSION_RE: LazyLock<Regex> = LazyLock::new(|| {
|
static CHANGE_SESSION_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
@@ -243,6 +248,10 @@ struct Pass1Result {
|
|||||||
seed_correlation_ids: HashSet<String>,
|
seed_correlation_ids: HashSet<String>,
|
||||||
change_session_map: HashMap<String, String>,
|
change_session_map: HashMap<String, String>,
|
||||||
sessions_with_signature: HashSet<String>,
|
sessions_with_signature: HashSet<String>,
|
||||||
|
/// Maps normalized session ID to app name from its signature line
|
||||||
|
session_app_map: HashMap<String, String>,
|
||||||
|
/// Maps seed correlation ID to the session ID from the same line (for app filtering)
|
||||||
|
seed_cid_sessions: HashMap<String, String>,
|
||||||
line_count: u64,
|
line_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +262,8 @@ impl Pass1Result {
|
|||||||
self.change_session_map.extend(other.change_session_map);
|
self.change_session_map.extend(other.change_session_map);
|
||||||
self.sessions_with_signature
|
self.sessions_with_signature
|
||||||
.extend(other.sessions_with_signature);
|
.extend(other.sessions_with_signature);
|
||||||
|
self.session_app_map.extend(other.session_app_map);
|
||||||
|
self.seed_cid_sessions.extend(other.seed_cid_sessions);
|
||||||
self.line_count += other.line_count;
|
self.line_count += other.line_count;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -264,10 +275,16 @@ fn process_line_pass1(trimmed: &str, query: &str, result: &mut Pass1Result) {
|
|||||||
.and_then(|c| c.get(1))
|
.and_then(|c| c.get(1))
|
||||||
.map(|m| normalize_session_id(m.as_str()));
|
.map(|m| normalize_session_id(m.as_str()));
|
||||||
|
|
||||||
if trimmed.contains(r#"msg="signature:"#)
|
// Detect signature lines using broad regex (matches both msg="signature:APP/..."
|
||||||
&& let Some(s) = sid
|
// and msg="MOBILE_CLIENT_LOG: signature:APP/...")
|
||||||
{
|
if let Some(sig_caps) = SIGNATURE_APP_RE.captures(trimmed) {
|
||||||
|
let app = sig_caps.get(1).unwrap().as_str();
|
||||||
|
if let Some(s) = sid {
|
||||||
result.sessions_with_signature.insert(s.to_string());
|
result.sessions_with_signature.insert(s.to_string());
|
||||||
|
result
|
||||||
|
.session_app_map
|
||||||
|
.insert(s.to_string(), app.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if trimmed.contains("changeSessionId:")
|
if trimmed.contains("changeSessionId:")
|
||||||
@@ -283,6 +300,15 @@ fn process_line_pass1(trimmed: &str, query: &str, result: &mut Pass1Result) {
|
|||||||
&& s != "noSession"
|
&& s != "noSession"
|
||||||
{
|
{
|
||||||
result.seed_session_ids.insert(s.to_string());
|
result.seed_session_ids.insert(s.to_string());
|
||||||
|
// Track which correlation IDs belong to which sessions (for app filtering)
|
||||||
|
if let Some(cid) = CORRELATION_ID_RE
|
||||||
|
.captures(trimmed)
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
{
|
||||||
|
result
|
||||||
|
.seed_cid_sessions
|
||||||
|
.insert(cid.as_str().to_string(), s.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(cid) = CORRELATION_ID_RE
|
if let Some(cid) = CORRELATION_ID_RE
|
||||||
.captures(trimmed)
|
.captures(trimmed)
|
||||||
@@ -550,6 +576,118 @@ fn run_pass2(
|
|||||||
Ok(total)
|
Ok(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- search_exceptions ---
|
||||||
|
|
||||||
|
/// Filter expanded session IDs to only those in chains containing a matching-app signature.
|
||||||
|
/// Builds a reverse change_session_map (old → [new]) and propagates forward from matching roots.
|
||||||
|
fn filter_expanded_by_app(
|
||||||
|
expanded_sids: &HashSet<String>,
|
||||||
|
change_map: &HashMap<String, String>,
|
||||||
|
session_app_map: &HashMap<String, String>,
|
||||||
|
app_filters: &[String],
|
||||||
|
) -> HashSet<String> {
|
||||||
|
// Build reverse map: old → [new1, new2, ...]
|
||||||
|
let mut reverse_map: HashMap<&str, Vec<&str>> = HashMap::new();
|
||||||
|
for (new_sid, old_sid) in change_map {
|
||||||
|
reverse_map
|
||||||
|
.entry(old_sid.as_str())
|
||||||
|
.or_default()
|
||||||
|
.push(new_sid.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all sessions with any matching app
|
||||||
|
let matching_roots: Vec<&str> = session_app_map
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, app)| app_filters.iter().any(|f| f == app.as_str()))
|
||||||
|
.map(|(sid, _)| sid.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Propagate forward from matching roots through the reverse map
|
||||||
|
let mut matching_sessions: HashSet<String> = HashSet::new();
|
||||||
|
let mut work_queue: Vec<&str> = matching_roots;
|
||||||
|
while let Some(current) = work_queue.pop() {
|
||||||
|
if !matching_sessions.insert(current.to_string()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(nexts) = reverse_map.get(current) {
|
||||||
|
work_queue.extend(nexts.iter().copied());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersect with expanded_sids
|
||||||
|
expanded_sids
|
||||||
|
.intersection(&matching_sessions)
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_search_exceptions(file_path: &str, app_filters: &[String], threads: usize) -> Result<()> {
|
||||||
|
let pool = build_thread_pool(threads)?;
|
||||||
|
let query = "Exception";
|
||||||
|
let num_threads = pool.current_num_threads();
|
||||||
|
let use_parallel = can_parallelize(file_path, num_threads);
|
||||||
|
|
||||||
|
// Pass 1: collect metadata
|
||||||
|
let pass1 = run_pass1(file_path, query, use_parallel, &pool)?;
|
||||||
|
|
||||||
|
// Expand seeds
|
||||||
|
let (expanded_sids, expanded_cids) = expand_seeds(
|
||||||
|
&pass1.seed_session_ids,
|
||||||
|
&pass1.seed_correlation_ids,
|
||||||
|
&pass1.change_session_map,
|
||||||
|
&pass1.sessions_with_signature,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by app
|
||||||
|
let filtered_sids = filter_expanded_by_app(
|
||||||
|
&expanded_sids,
|
||||||
|
&pass1.change_session_map,
|
||||||
|
&pass1.session_app_map,
|
||||||
|
app_filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter correlation IDs: keep only those whose seed line's session is in the filtered set
|
||||||
|
let filtered_cids: HashSet<String> = expanded_cids
|
||||||
|
.iter()
|
||||||
|
.filter(|cid| {
|
||||||
|
pass1
|
||||||
|
.seed_cid_sessions
|
||||||
|
.get(cid.as_str())
|
||||||
|
.is_some_and(|sid| filtered_sids.contains(sid))
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if filtered_sids.is_empty() && filtered_cids.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
"0 matching lines found (no sessions matching apps {:?})",
|
||||||
|
app_filters
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"Expanding: {} session IDs (filtered from {}), {} correlation IDs (filtered from {})",
|
||||||
|
filtered_sids.len(),
|
||||||
|
expanded_sids.len(),
|
||||||
|
filtered_cids.len(),
|
||||||
|
expanded_cids.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pass 2: filter and print
|
||||||
|
let match_count = run_pass2(
|
||||||
|
file_path,
|
||||||
|
query,
|
||||||
|
&filtered_sids,
|
||||||
|
&filtered_cids,
|
||||||
|
use_parallel,
|
||||||
|
&pool,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
eprintln!("{} lines output", match_count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user