This commit is contained in:
Pakin 2025-08-04 08:11:35 +07:00
commit fb485a6c1c
13 changed files with 3566 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1892
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "log-analyzer"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = { version = "0.4.41", features = ["serde"] }
clap = { version = "4.5.42", features = ["derive"] }
regex = "1.11.1"
reqwest = { version = "0.12.22", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
tokio = { version = "1.47.1", features = ["full"] }
walkdir = "2.5.0"

19
README.md Normal file
View file

@ -0,0 +1,19 @@
# Analyze logs with source code correlation
./logcat_analyzer -f logcat.txt --source /path/to/android/src
# This scans your Java/Kotlin files and correlates them with log entries
# AI analysis with source code context
./logcat_analyzer -f logcat.txt --source ./app/src/main/java --ai
# Filter errors and show source locations
./logcat_analyzer -f logcat.txt --source ./src -l E
# Find crashes with exact code locations
./logcat_analyzer -f logcat.txt --source ./app/src --crashes
# Set up source directory
export ANDROID_SOURCE_DIR=/path/to/your/android/project/src
./logcat_analyzer -f logcat.txt --source $ANDROID_SOURCE_DIR --ai

356
src/ai/analyzer.rs Normal file
View file

@ -0,0 +1,356 @@
use crate::logcat::logcat_analyzer::LogcatAnalyzer;
use crate::model::{
ai::{AIAnalysisRequest, AIInsight, AnomalyType},
log_entry::{LogEntry, LogLevel},
};
pub struct LocalAiLogAnalyzer {
ollama_host: String,
model_name: String,
client: reqwest::Client,
}
impl LocalAiLogAnalyzer {
pub fn new(ollama_host: Option<String>, model_name: Option<String>) -> Self {
Self {
ollama_host: ollama_host.unwrap_or_else(|| "http://localhost:11434".to_string()),
model_name: model_name.unwrap_or_else(|| "llama3.1".to_string()),
client: reqwest::Client::new(),
}
}
pub async fn analyze_logs(
&self,
analyzer: &LogcatAnalyzer,
entries: &[&LogEntry],
) -> Result<Vec<AIInsight>, Box<dyn std::error::Error>> {
if !self.check_ollama_availability().await {
return Ok(self.generate_offline_insights(analyzer, entries));
}
let analysis_request = self.prepare_analysis_request(analyzer, entries);
let insights = self.call_ollama_api(analysis_request).await?;
Ok(insights)
}
fn prepare_analysis_request(
&self,
analyzer: &LogcatAnalyzer,
entries: &[&LogEntry],
) -> AIAnalysisRequest {
let log_summary = format!(
"Total entries: {}, Errors: {}, Crashes: {}, With source code: {}",
entries.len(),
entries
.iter()
.filter(|e| matches!(e.level, LogLevel::Error | LogLevel::Fatal))
.count(),
analyzer.find_crashes().len(),
entries
.iter()
.filter(|e| e.source_location.is_some())
.count()
);
let error_patterns: Vec<String> = entries
.iter()
.filter(|e| matches!(e.level, LogLevel::Error | LogLevel::Fatal))
.take(10)
.map(|e| {
let source_info = if let Some(source) = &e.source_location {
format!(
" [{}:{}]",
source.file_path,
source.line_number.unwrap_or(0)
)
} else {
String::new()
};
format!(
"[{}] {}: {}{}",
format!("{:?}", e.level),
e.tag,
e.message,
source_info
)
})
.collect();
let crash_entries: Vec<String> = analyzer
.find_crashes()
.iter()
.take(5)
.map(|e| {
let source_info = if let Some(source) = &e.source_location {
format!(
" [{}:{}]",
source.file_path,
source.line_number.unwrap_or(0)
)
} else {
String::new()
};
format!(
"[{}] {}: {}{}",
format!("{:?}", e.level),
e.tag,
e.message,
source_info
)
})
.collect();
let anomalies: Vec<String> = analyzer
.detect_anomalies()
.iter()
.take(5)
.map(|a| format!("{:?}: {}", a.anomaly_type, a.description))
.collect();
// Add source code context for critical entries
let source_code_context: Vec<String> = entries
.iter()
.filter(|e| {
e.source_location.is_some() && matches!(e.level, LogLevel::Error | LogLevel::Fatal)
})
.take(5)
.filter_map(|e| {
e.source_location.as_ref().map(|source| {
format!(
"File: {}, Method: {}, Context:\n{}",
source.file_path,
source.method_name.as_deref().unwrap_or("unknown"),
source.code_context.join("\n")
)
})
})
.collect();
// Add code quality issues if available
let code_quality_issues: Vec<String> =
if let Some(source_analyzer) = &analyzer.source_analyzer {
source_analyzer
.analyze_code_quality(entries)
.iter()
.map(|issue| {
format!(
"{:?}: {} ({})",
issue.issue_type, issue.description, issue.severity
)
})
.collect()
} else {
Vec::new()
};
AIAnalysisRequest {
log_summary,
error_patterns,
crash_entries,
anomalies,
source_code_context,
code_quality_issues,
context: "Android application logcat analysis with source code correlation".to_string(),
}
}
async fn check_ollama_availability(&self) -> bool {
match self
.client
.get(&format!("{}/api/tags", self.ollama_host))
.send()
.await
{
Ok(response) => response.status().is_success(),
Err(_) => false,
}
}
async fn call_ollama_api(
&self,
request: AIAnalysisRequest,
) -> Result<Vec<AIInsight>, Box<dyn std::error::Error>> {
let prompt = format!(
r#"You are an expert Android developer and log analysis specialist. Analyze the following Android logcat data WITH source code context and provide structured insights.
Log Summary: {}
Error Patterns: {:?}
Crash Entries: {:?}
Detected Anomalies: {:?}
Source Code Context: {:?}
Code Quality Issues: {:?}
IMPORTANT: You have access to source code context. Use this to provide deeper analysis about:
1. Root causes of errors based on the actual code
2. Specific code improvements and fixes
3. Code quality issues and best practices violations
4. Performance bottlenecks visible in the source
5. Security vulnerabilities in the code
Please analyze this data and provide insights in the following JSON format (respond with ONLY valid JSON, no additional text):
[{{
"category": "error_analysis",
"severity": "high",
"description": "Brief description of the issue with source code reference",
"recommendation": "Specific code changes and improvements",
"confidence": 0.8,
"source_file": "filename.java",
"line_number": 123
}}]
Categories can be: error_analysis, performance, crashes, security, code_quality, best_practices
Severity levels: low, medium, high, critical
Confidence should be between 0.0 and 1.0
Focus on actionable code-level insights that help developers fix specific issues in their Android application source code."#,
request.log_summary,
request.error_patterns,
request.crash_entries,
request.anomalies,
request.source_code_context,
request.code_quality_issues
);
let payload = serde_json::json!({
"model": self.model_name,
"prompt": prompt,
"stream": false,
"options": {
"temperature": 0.3,
"top_p": 0.9,
"max_tokens": 2000
}
});
let response = self
.client
.post(&format!("{}/api/generate", self.ollama_host))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await?;
if !response.status().is_success() {
return Err(format!("Ollama API error: {}", response.status()).into());
}
let response_json: serde_json::Value = response.json().await?;
if let Some(response_text) = response_json.get("response").and_then(|v| v.as_str()) {
self.parse_ollama_response(response_text)
} else {
Err("Invalid response format from Ollama".into())
}
}
fn parse_ollama_response(
&self,
response: &str,
) -> Result<Vec<AIInsight>, Box<dyn std::error::Error>> {
// Try to extract JSON from the response
let json_start = response.find('[').unwrap_or(0);
let json_end = response.rfind(']').map(|i| i + 1).unwrap_or(response.len());
let json_str = &response[json_start..json_end];
match serde_json::from_str::<Vec<AIInsight>>(json_str) {
Ok(insights) => Ok(insights),
Err(e) => {
println!("Warning: Failed to parse AI response as JSON: {}", e);
println!("Raw response: {}", response);
// Fallback to a generic insight
Ok(vec![AIInsight {
category: "ai_analysis".to_string(),
severity: "medium".to_string(),
description: "AI analysis completed with parsing issues".to_string(),
recommendation: "Review the log analysis manually for best results".to_string(),
confidence: 0.5,
}])
}
}
}
fn generate_offline_insights(
&self,
analyzer: &LogcatAnalyzer,
entries: &[&LogEntry],
) -> Vec<AIInsight> {
let mut insights = Vec::new();
// Analyze error frequency
let error_count = entries
.iter()
.filter(|e| matches!(e.level, LogLevel::Error))
.count();
let total_count = entries.len();
if error_count as f32 / total_count as f32 > 0.1 {
insights.push(AIInsight {
category: "error_analysis".to_string(),
severity: "high".to_string(),
description: format!(
"High error rate detected: {}/{} entries are errors",
error_count, total_count
),
recommendation: "Review error patterns and implement error handling".to_string(),
confidence: 0.9,
});
}
// Analyze crashes
let crashes = analyzer.find_crashes();
if !crashes.is_empty() {
insights.push(AIInsight {
category: "crashes".to_string(),
severity: "critical".to_string(),
description: format!("Found {} potential crashes or exceptions", crashes.len()),
recommendation: "Investigate crash logs and implement proper exception handling"
.to_string(),
confidence: 0.95,
});
}
// Analyze anomalies
let anomalies = analyzer.detect_anomalies();
for anomaly in anomalies {
let severity = match anomaly.severity {
s if s > 0.8 => "high",
s if s > 0.5 => "medium",
_ => "low",
};
insights.push(AIInsight {
category: "anomaly_detection".to_string(),
severity: severity.to_string(),
description: anomaly.description,
recommendation: self.get_anomaly_recommendation(&anomaly.anomaly_type),
confidence: anomaly.severity,
});
}
insights
}
fn get_anomaly_recommendation(&self, anomaly_type: &AnomalyType) -> String {
match anomaly_type {
AnomalyType::FrequencySpike => {
"Monitor system resources and optimize logging frequency".to_string()
}
AnomalyType::UnusualErrorPattern => {
"Investigate new error patterns and update error handling".to_string()
}
AnomalyType::MemoryLeak => {
"Profile memory usage and fix potential memory leaks".to_string()
}
AnomalyType::PerformanceDegradation => {
"Analyze performance bottlenecks and optimize critical paths".to_string()
}
AnomalyType::CrashLoop => {
"Fix underlying crash causes to prevent restart loops".to_string()
}
AnomalyType::SuspiciousActivity => {
"Review security implications and implement additional monitoring".to_string()
}
}
}
}

1
src/ai/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod analyzer;

View file

@ -0,0 +1,684 @@
use chrono::{Duration, NaiveDateTime};
use regex::Regex;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
use crate::model::{
ai::{AnomalyType, LogAnomaly},
log_entry::{LogEntry, LogLevel},
source::SourceCodeAnalyzer,
};
use crate::ai::analyzer::LocalAiLogAnalyzer;
pub struct LogcatAnalyzer {
pub entries: Vec<LogEntry>,
regex: Regex,
pub source_analyzer: Option<SourceCodeAnalyzer>,
}
impl LogcatAnalyzer {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let regex = Regex::new(
r"^(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([VDIWEF])\s+([^:]+):\s*(.*)$",
)?;
Ok(LogcatAnalyzer {
entries: Vec::new(),
regex,
source_analyzer: None,
})
}
pub fn with_source_code<P: AsRef<Path>>(
source_path: P,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut analyzer = Self::new()?;
analyzer.source_analyzer = Some(SourceCodeAnalyzer::new(source_path)?);
Ok(analyzer)
}
pub fn parse_file<P: AsRef<Path>>(
&mut self,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
use std::io::Read;
let mut file = File::open(path.as_ref())?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
// Convert bytes to string, replacing invalid UTF-8 sequences
let content = String::from_utf8_lossy(&buffer);
let mut line_count = 0;
let mut parsed_count = 0;
let mut error_count = 0;
for line in content.lines() {
line_count += 1;
println!("{}: {}", line_count, line);
// Skip empty lines and lines that are clearly malformed
if line.trim().is_empty() {
continue;
}
// Try to parse the line, but don't fail if it's malformed
match self.try_parse_line(line) {
Ok(Some(entry)) => {
self.entries.push(entry);
parsed_count += 1;
}
Ok(None) => {
// Line didn't match expected format, but that's okay
continue;
}
Err(e) => {
error_count += 1;
if error_count <= 5 {
eprintln!(
"Warning: Failed to parse line {}: {} (line: {})",
line_count,
e,
line.chars().take(100).collect::<String>()
);
}
// Still create an entry for unparsed lines if they seem like logs
if line.len() > 10
&& (line.contains(" I ")
|| line.contains(" E ")
|| line.contains(" W ")
|| line.contains(" D ")
|| line.contains(" V ")
|| line.contains(" F "))
{
self.entries.push(LogEntry {
timestamp: None,
pid: None,
tid: None,
level: LogLevel::Unknown("?".to_string()),
tag: "PARSE_ERROR".to_string(),
message: line.to_string(),
raw_line: line.to_string(),
source_location: None,
});
}
}
}
}
if error_count > 5 {
eprintln!(
"... and {} more parsing errors (showing first 5)",
error_count - 5
);
}
println!(
"Parsed {} lines: {} successfully parsed, {} errors, {} total entries",
line_count,
parsed_count,
error_count,
self.entries.len()
);
Ok(())
}
fn try_parse_line(&self, line: &str) -> Result<Option<LogEntry>, Box<dyn std::error::Error>> {
// First, clean the line of any problematic characters
let cleaned_line = self.clean_line(line);
if let Some(captures) = self.regex.captures(&cleaned_line) {
let timestamp_str = captures.get(1).ok_or("Missing timestamp")?.as_str();
let pid_str = captures.get(2).ok_or("Missing PID")?.as_str();
let tid_str = captures.get(3).ok_or("Missing TID")?.as_str();
let level_str = captures.get(4).ok_or("Missing log level")?.as_str();
let tag = captures
.get(5)
.ok_or("Missing tag")?
.as_str()
.trim()
.to_string();
let message = captures
.get(6)
.ok_or("Missing message")?
.as_str()
.to_string();
// Parse timestamp with error handling
let timestamp = match NaiveDateTime::parse_from_str(
&format!("2024-{}", timestamp_str),
"%Y-%m-%d %H:%M:%S%.3f",
) {
Ok(ts) => Some(ts),
Err(_) => {
// Try alternative timestamp formats
self.parse_alternative_timestamp(timestamp_str)
}
};
// Parse PID and TID with error handling
let pid = pid_str.parse().ok();
let tid = tid_str.parse().ok();
let level = LogLevel::from_str(level_str);
// Try to find source code location if source analyzer is available
let source_location = if let Some(source_analyzer) = &self.source_analyzer {
source_analyzer.find_log_source(&tag, &message)
} else {
None
};
Ok(Some(LogEntry {
timestamp,
pid,
tid,
level,
tag,
message,
raw_line: line.to_string(),
source_location,
}))
} else {
// Try alternative parsing patterns for non-standard log formats
self.try_alternative_parsing(&cleaned_line)
}
}
fn clean_line(&self, line: &str) -> String {
// Remove or replace problematic characters
line.chars()
.filter(|c| c.is_ascii() || c.is_alphanumeric() || " :.-/()[]{}".contains(*c))
.collect::<String>()
.replace('\0', "") // Remove null bytes
.replace('\r', "") // Remove carriage returns
}
fn parse_alternative_timestamp(&self, timestamp_str: &str) -> Option<NaiveDateTime> {
// Try various timestamp formats commonly found in Android logs
let formats = vec![
"%m-%d %H:%M:%S%.3f",
"%Y-%m-%d %H:%M:%S%.3f",
"%m-%d %H:%M:%S",
"%H:%M:%S%.3f",
"%H:%M:%S",
];
for format in formats {
if let Ok(ts) = NaiveDateTime::parse_from_str(
&if format.starts_with("%Y") {
timestamp_str.to_string()
} else {
format!("2024-{}", timestamp_str)
},
format,
) {
return Some(ts);
}
}
None
}
fn try_alternative_parsing(
&self,
line: &str,
) -> Result<Option<LogEntry>, Box<dyn std::error::Error>> {
// Try to parse common alternative log formats
// Simple format: LEVEL/TAG: message
if let Some(caps) = Regex::new(r"^([VDIWEF])/([^:]+):\s*(.*)$")?.captures(line) {
let level = LogLevel::from_str(caps.get(1).unwrap().as_str());
let tag = caps.get(2).unwrap().as_str().trim().to_string();
let message = caps.get(3).unwrap().as_str().to_string();
return Ok(Some(LogEntry {
timestamp: None,
pid: None,
tid: None,
level,
tag,
message,
raw_line: line.to_string(),
source_location: None,
}));
}
// System.out format: message (no level/tag)
if line.len() > 5 && !line.starts_with("--") && !line.starts_with("==") {
return Ok(Some(LogEntry {
timestamp: None,
pid: None,
tid: None,
level: LogLevel::Info, // Assume info level for unknown format
tag: "UNKNOWN".to_string(),
message: line.to_string(),
raw_line: line.to_string(),
source_location: None,
}));
}
Ok(None)
}
fn parse_line(&self, line: &str) -> Option<LogEntry> {
if let Some(captures) = self.regex.captures(line) {
let timestamp_str = captures.get(1)?.as_str();
let pid_str = captures.get(2)?.as_str();
let tid_str = captures.get(3)?.as_str();
let level_str = captures.get(4)?.as_str();
let tag = captures.get(5)?.as_str().trim().to_string();
let message = captures.get(6)?.as_str().to_string();
let timestamp = NaiveDateTime::parse_from_str(
&format!("2024-{}", timestamp_str),
"%Y-%m-%d %H:%M:%S%.3f",
)
.ok();
let pid = pid_str.parse().ok();
let tid = tid_str.parse().ok();
let level = LogLevel::from_str(level_str);
// Try to find source code location if source analyzer is available
let source_location = if let Some(source_analyzer) = &self.source_analyzer {
source_analyzer.find_log_source(&tag, &message)
} else {
None
};
Some(LogEntry {
timestamp,
pid,
tid,
level,
tag,
message,
raw_line: line.to_string(),
source_location,
})
} else {
Some(LogEntry {
timestamp: None,
pid: None,
tid: None,
level: LogLevel::Unknown("?".to_string()),
tag: "UNPARSED".to_string(),
message: line.to_string(),
raw_line: line.to_string(),
source_location: None,
})
}
}
// Enhanced anomaly detection
pub fn detect_anomalies(&self) -> Vec<LogAnomaly> {
let mut anomalies = Vec::new();
// Detect frequency spikes
anomalies.extend(self.detect_frequency_spikes());
// Detect unusual error patterns
anomalies.extend(self.detect_unusual_error_patterns());
// Detect potential memory leaks
anomalies.extend(self.detect_memory_issues());
// Detect performance degradation
anomalies.extend(self.detect_performance_issues());
// Detect crash loops
anomalies.extend(self.detect_crash_loops());
anomalies
}
fn detect_frequency_spikes(&self) -> Vec<LogAnomaly> {
let mut anomalies = Vec::new();
let mut tag_frequencies: HashMap<String, Vec<usize>> = HashMap::new();
// Group entries by tag and collect indices
for (idx, entry) in self.entries.iter().enumerate() {
tag_frequencies
.entry(entry.tag.clone())
.or_default()
.push(idx);
}
// Detect tags with unusually high frequency
for (tag, indices) in tag_frequencies {
if indices.len() > 100 {
// Threshold for spike detection
let frequency_per_minute = if let (Some(first), Some(last)) = (
self.entries.get(indices[0]).and_then(|e| e.timestamp),
self.entries
.get(*indices.last().unwrap())
.and_then(|e| e.timestamp),
) {
let duration = last.signed_duration_since(first).num_minutes().max(1);
indices.len() as f32 / duration as f32
} else {
indices.len() as f32
};
if frequency_per_minute > 10.0 {
anomalies.push(LogAnomaly {
anomaly_type: AnomalyType::FrequencySpike,
description: format!(
"High frequency logging from tag '{}': {:.1} entries/minute",
tag, frequency_per_minute
),
entries: indices,
severity: (frequency_per_minute / 100.0).min(1.0),
});
}
}
}
anomalies
}
fn detect_unusual_error_patterns(&self) -> Vec<LogAnomaly> {
let mut anomalies = Vec::new();
let mut error_patterns: HashMap<String, Vec<usize>> = HashMap::new();
// Collect error patterns
for (idx, entry) in self.entries.iter().enumerate() {
if matches!(entry.level, LogLevel::Error | LogLevel::Fatal) {
// Extract first few words as pattern
let pattern = entry
.message
.split_whitespace()
.take(5)
.collect::<Vec<_>>()
.join(" ");
error_patterns.entry(pattern).or_default().push(idx);
}
}
// Find patterns that appear frequently
for (pattern, indices) in error_patterns {
if indices.len() > 5 {
anomalies.push(LogAnomaly {
anomaly_type: AnomalyType::UnusualErrorPattern,
description: format!(
"Recurring error pattern: '{}' ({} occurrences)",
pattern,
indices.len()
),
entries: indices.clone(),
severity: (indices.clone().len() as f32 / 20.0).min(1.0),
});
}
}
anomalies
}
fn detect_memory_issues(&self) -> Vec<LogAnomaly> {
let mut anomalies = Vec::new();
let memory_keywords = ["OutOfMemoryError", "GC_", "memory", "heap", "oom"];
let memory_entries: Vec<usize> = self
.entries
.iter()
.enumerate()
.filter(|(_, entry)| {
memory_keywords.iter().any(|keyword| {
entry
.message
.to_lowercase()
.contains(&keyword.to_lowercase())
|| entry.tag.to_lowercase().contains(&keyword.to_lowercase())
})
})
.map(|(idx, _)| idx)
.collect();
if memory_entries.len() > 10 {
anomalies.push(LogAnomaly {
anomaly_type: AnomalyType::MemoryLeak,
description: format!(
"Potential memory issues detected: {} related entries",
memory_entries.len()
),
entries: memory_entries,
severity: 0.8,
});
}
anomalies
}
fn detect_performance_issues(&self) -> Vec<LogAnomaly> {
let mut anomalies = Vec::new();
let perf_keywords = ["slow", "timeout", "ANR", "blocked", "lag", "performance"];
let perf_entries: Vec<usize> = self
.entries
.iter()
.enumerate()
.filter(|(_, entry)| {
perf_keywords.iter().any(|keyword| {
entry
.message
.to_lowercase()
.contains(&keyword.to_lowercase())
})
})
.map(|(idx, _)| idx)
.collect();
if perf_entries.len() > 5 {
anomalies.push(LogAnomaly {
anomaly_type: AnomalyType::PerformanceDegradation,
description: format!(
"Performance issues detected: {} related entries",
perf_entries.len()
),
entries: perf_entries,
severity: 0.7,
});
}
anomalies
}
fn detect_crash_loops(&self) -> Vec<LogAnomaly> {
let mut anomalies = Vec::new();
let crashes = self.find_crashes();
if crashes.len() > 3 {
// Check if crashes are happening in quick succession
let mut crash_times = Vec::new();
for crash in &crashes {
if let Some(timestamp) = crash.timestamp {
crash_times.push(timestamp);
}
}
crash_times.sort();
let mut quick_crashes = 0;
for window in crash_times.windows(2) {
if let [first, second] = window {
if second.signed_duration_since(*first) < Duration::minutes(5) {
quick_crashes += 1;
}
}
}
if quick_crashes > 2 {
let crash_indices: Vec<usize> = crashes
.iter()
.map(|crash| {
self.entries
.iter()
.position(|e| e.raw_line == crash.raw_line)
.unwrap_or(0)
})
.collect();
anomalies.push(LogAnomaly {
anomaly_type: AnomalyType::CrashLoop,
description: format!(
"Potential crash loop detected: {} crashes in quick succession",
quick_crashes
),
entries: crash_indices,
severity: 0.9,
});
}
}
anomalies
}
// Existing methods remain the same
pub fn filter_by_level(&self, min_level: LogLevel) -> Vec<&LogEntry> {
let min_priority = min_level.priority();
self.entries
.iter()
.filter(|entry| entry.level.priority() >= min_priority)
.collect()
}
pub fn filter_by_tag(&self, tag: &str) -> Vec<&LogEntry> {
self.entries
.iter()
.filter(|entry| entry.tag.contains(tag))
.collect()
}
pub fn filter_by_message(&self, pattern: &str) -> Vec<&LogEntry> {
self.entries
.iter()
.filter(|entry| entry.message.contains(pattern))
.collect()
}
pub fn get_error_summary(&self) -> HashMap<String, usize> {
let mut summary = HashMap::new();
for entry in &self.entries {
if matches!(entry.level, LogLevel::Error | LogLevel::Fatal) {
*summary.entry(entry.tag.clone()).or_insert(0) += 1;
}
}
summary
}
pub fn get_tag_statistics(&self) -> HashMap<String, HashMap<String, usize>> {
let mut stats = HashMap::new();
for entry in &self.entries {
let tag_stats = stats.entry(entry.tag.clone()).or_insert_with(HashMap::new);
let level_name = format!("{:?}", entry.level);
*tag_stats.entry(level_name).or_insert(0) += 1;
}
stats
}
pub fn find_crashes(&self) -> Vec<&LogEntry> {
self.entries
.iter()
.filter(|entry| {
entry.message.to_lowercase().contains("crash")
|| entry.message.to_lowercase().contains("exception")
|| entry.message.to_lowercase().contains("fatal")
|| entry.tag.to_lowercase().contains("crash")
|| entry.message.contains("FATAL EXCEPTION")
|| entry.message.contains("AndroidRuntime")
})
.collect()
}
pub fn get_total_entries(&self) -> usize {
self.entries.len()
}
pub async fn print_ai_analysis(&self, ai_analyzer: &LocalAiLogAnalyzer) {
println!("\n=== AI-Powered Analysis ===");
let all_entries: Vec<&LogEntry> = self.entries.iter().collect();
match ai_analyzer.analyze_logs(self, &all_entries).await {
Ok(insights) => {
for insight in insights {
println!(
"\n🔍 {} Analysis (Confidence: {:.1}%)",
insight.category.replace("_", " ").to_uppercase(),
insight.confidence * 100.0
);
println!(" Severity: {}", insight.severity.to_uppercase());
println!(" Issue: {}", insight.description);
println!(" Recommendation: {}", insight.recommendation);
}
}
Err(e) => println!("AI analysis failed: {}", e),
}
}
pub fn print_summary(&self) {
println!("=== Logcat Analysis Summary ===");
println!("Total log entries: {}", self.get_total_entries());
// Level distribution
let mut level_counts = HashMap::new();
for entry in &self.entries {
let level_name = format!("{:?}", entry.level);
*level_counts.entry(level_name).or_insert(0) += 1;
}
println!("\nLog Level Distribution:");
for (level, count) in level_counts {
println!(" {}: {}", level, count);
}
// Error summary
let errors = self.get_error_summary();
if !errors.is_empty() {
println!("\nError Summary (by tag):");
let mut error_vec: Vec<_> = errors.iter().collect();
error_vec.sort_by(|a, b| b.1.cmp(a.1));
for (tag, count) in error_vec {
println!(" {}: {} errors", tag, count);
}
}
// Anomaly detection
let anomalies = self.detect_anomalies();
if !anomalies.is_empty() {
println!("\n🚨 Anomalies Detected:");
for anomaly in &anomalies {
println!(
" {:?} (Severity: {:.1}): {}",
anomaly.anomaly_type,
anomaly.severity * 100.0,
anomaly.description
);
}
}
// Crash detection
let crashes = self.find_crashes();
if !crashes.is_empty() {
println!("\n💥 Potential Crashes Found: {}", crashes.len());
for crash in crashes.iter().take(5) {
println!(
" [{}] {}: {}",
format!("{:?}", crash.level),
crash.tag,
crash.message.chars().take(80).collect::<String>()
);
}
}
}
}

1
src/logcat/mod.rs Normal file
View file

@ -0,0 +1 @@
pub(crate) mod logcat_analyzer;

185
src/main.rs Normal file
View file

@ -0,0 +1,185 @@
use crate::ai::analyzer::LocalAiLogAnalyzer;
use crate::logcat::logcat_analyzer::LogcatAnalyzer;
use crate::model::log_entry::LogEntry;
use crate::model::log_entry::LogLevel;
use clap::{Arg, Command};
use tokio;
mod ai;
mod logcat;
mod model;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let matches = Command::new("AI-Powered Logcat Analyzer")
.version("2.0")
.author("Your Name")
.about("Analyzes Android logcat files with AI insights")
.arg(
Arg::new("file")
.short('f')
.long("file")
.value_name("FILE")
.help("Logcat file to analyze")
.required(true),
)
.arg(
Arg::new("level")
.short('l')
.long("level")
.value_name("LEVEL")
.help("Minimum log level (V, D, I, W, E, F)"),
)
.arg(
Arg::new("tag")
.short('t')
.long("tag")
.value_name("TAG")
.help("Filter by tag"),
)
.arg(
Arg::new("message")
.short('m')
.long("message")
.value_name("PATTERN")
.help("Filter by message content"),
)
.arg(
Arg::new("crashes")
.short('c')
.long("crashes")
.help("Show only potential crashes")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("ai")
.long("ai")
.help("Enable AI-powered analysis")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("ollama-host")
.long("ollama-host")
.value_name("HOST")
.help("Ollama server host (default: http://localhost:11434)"),
)
.arg(
Arg::new("model")
.long("model")
.value_name("MODEL")
.help("Ollama model name (default: llama3.1)"),
)
.arg(
Arg::new("source")
.short('s')
.long("source")
.value_name("SOURCE_DIR")
.help("Android source code directory for correlation analysis"),
)
.get_matches();
let file_path = matches.get_one::<String>("file").unwrap();
// Initialize analyzer with or without source code
let mut analyzer = if let Some(source_dir) = matches.get_one::<String>("source") {
println!(
"Initializing with source code analysis from: {}",
source_dir
);
LogcatAnalyzer::with_source_code(source_dir)?
} else {
LogcatAnalyzer::new()?
};
println!("Parsing logcat file: {}", file_path);
analyzer.parse_file(file_path)?;
// Apply filters
let mut filtered_entries: Vec<&LogEntry> = analyzer.entries.iter().collect();
if let Some(level_str) = matches.get_one::<String>("level") {
let level = LogLevel::from_str(level_str);
filtered_entries = analyzer.filter_by_level(level);
}
if let Some(tag) = matches.get_one::<String>("tag") {
filtered_entries = analyzer.filter_by_tag(tag);
}
if let Some(pattern) = matches.get_one::<String>("message") {
filtered_entries = analyzer.filter_by_message(pattern);
}
if matches.get_flag("crashes") {
filtered_entries = analyzer.find_crashes();
}
// Print summary with enhanced analysis
analyzer.print_summary();
// AI Analysis
if matches.get_flag("ai") {
let ollama_host = matches
.get_one::<String>("ollama-host")
.cloned()
.or_else(|| std::env::var("OLLAMA_HOST").ok());
let model_name = matches
.get_one::<String>("model")
.cloned()
.or_else(|| std::env::var("OLLAMA_MODEL").ok());
let ai_analyzer = LocalAiLogAnalyzer::new(ollama_host, model_name);
analyzer.print_ai_analysis(&ai_analyzer).await;
}
// Print filtered results with source info
if !filtered_entries.is_empty() && !matches.get_flag("ai") {
println!(
"\n=== Filtered Results ({} entries) ===",
filtered_entries.len()
);
for entry in filtered_entries.iter().take(20) {
if let Some(timestamp) = &entry.timestamp {
print!("{} ", timestamp.format("%m-%d %H:%M:%S%.3f"));
}
if let Some(pid) = entry.pid {
print!("{:5} ", pid);
}
// Include source location if available
let source_info = if let Some(source) = &entry.source_location {
format!(
" [{}:{}]",
source.file_path,
source.line_number.unwrap_or(0)
)
} else {
String::new()
};
println!(
"[{:?}] {}: {}{}",
entry.level, entry.tag, entry.message, source_info
);
// Show code context for errors
if matches!(entry.level, LogLevel::Error | LogLevel::Fatal) {
if let Some(source) = &entry.source_location {
if !source.code_context.is_empty() {
println!(" Code context:");
for (i, line) in source.code_context.iter().take(3).enumerate() {
println!(" {}", line);
}
}
}
}
}
if filtered_entries.len() > 20 {
println!("... and {} more entries", filtered_entries.len() - 20);
}
}
Ok(())
}

39
src/model/ai.rs Normal file
View file

@ -0,0 +1,39 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct AIAnalysisRequest {
pub log_summary: String,
pub error_patterns: Vec<String>,
pub crash_entries: Vec<String>,
pub anomalies: Vec<String>,
pub source_code_context: Vec<String>,
pub code_quality_issues: Vec<String>,
pub context: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AIInsight {
pub category: String,
pub severity: String,
pub description: String,
pub recommendation: String,
pub confidence: f32,
}
#[derive(Debug)]
pub struct LogAnomaly {
pub anomaly_type: AnomalyType,
pub description: String,
pub entries: Vec<usize>, // indices into the log entries
pub severity: f32,
}
#[derive(Debug)]
pub enum AnomalyType {
FrequencySpike,
UnusualErrorPattern,
MemoryLeak,
PerformanceDegradation,
CrashLoop,
SuspiciousActivity,
}

51
src/model/log_entry.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::model::source::SourceLocation;
use chrono::NaiveDateTime;
#[derive(Debug, Clone)]
pub struct LogEntry {
pub timestamp: Option<NaiveDateTime>,
pub pid: Option<u32>,
pub tid: Option<u32>,
pub level: LogLevel,
pub tag: String,
pub message: String,
pub raw_line: String,
pub source_location: Option<SourceLocation>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LogLevel {
Verbose,
Debug,
Info,
Warning,
Error,
Fatal,
Unknown(String),
}
impl LogLevel {
pub fn from_str(s: &str) -> Self {
match s.to_uppercase().as_str() {
"V" => LogLevel::Verbose,
"D" => LogLevel::Debug,
"I" => LogLevel::Info,
"W" => LogLevel::Warning,
"E" => LogLevel::Error,
"F" => LogLevel::Fatal,
_ => LogLevel::Unknown(s.to_string()),
}
}
pub fn priority(&self) -> u8 {
match self {
LogLevel::Verbose => 2,
LogLevel::Debug => 3,
LogLevel::Info => 4,
LogLevel::Warning => 5,
LogLevel::Error => 6,
LogLevel::Fatal => 7,
LogLevel::Unknown(_) => 0,
}
}
}

3
src/model/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod ai;
pub mod log_entry;
pub mod source;

320
src/model/source.rs Normal file
View file

@ -0,0 +1,320 @@
use crate::model::log_entry::LogLevel;
use std::{
collections::{HashMap, HashSet},
fs::read_to_string,
path::{Path, PathBuf},
};
use regex::Regex;
use walkdir::WalkDir;
use crate::model::log_entry::LogEntry;
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub file_path: String,
pub line_number: Option<u32>,
pub method_name: Option<String>,
pub class_name: Option<String>,
pub code_context: Vec<String>,
}
pub struct SourceCodeAnalyzer {
pub source_root: PathBuf,
pub java_files: HashMap<String, String>,
pub kotlin_files: HashMap<String, String>,
pub log_patterns: Vec<Regex>,
}
impl SourceCodeAnalyzer {
pub fn new<P: AsRef<Path>>(source_root: P) -> Result<Self, Box<dyn std::error::Error>> {
let mut analyzer = SourceCodeAnalyzer {
source_root: source_root.as_ref().to_path_buf(),
java_files: HashMap::new(),
kotlin_files: HashMap::new(),
log_patterns: Vec::new(),
};
analyzer.init_log_patterns()?;
analyzer.scan_source_files()?;
Ok(analyzer)
}
fn init_log_patterns(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let patterns = vec![
r#"Log\.([dviwe])\s*\(\s*["']([^"']+)["']\s*,\s*["']([^"']+)["']\s*\)"#, // Log.d("TAG", "message")
r#"Log\.([dviwe])\s*\(\s*([A-Z_][A-Z0-9_]*)\s*,\s*["']([^"']+)["']\s*\)"#, // Log.d(TAG, "message")
r#"android\.util\.Log\.([dviwe])\s*\([^)]+\)"#, // android.util.Log.d(...)
r#"logger\.([dviwe])\s*\(\s*["']([^"']+)["']\s*\)"#, // logger.d("message")
r#"Log\.([dviwe])\s*\(\s*TAG\s*,\s*[^)]+\)"#, // Log.d(TAG, variable)
r#"System\.out\.println\s*\(\s*["']([^"']+)["']\s*\)"#, // System.out.println("message")
];
for pattern in patterns {
self.log_patterns.push(Regex::new(pattern)?);
}
Ok(())
}
fn scan_source_files(&mut self) -> Result<(), Box<dyn std::error::Error>> {
for entry in WalkDir::new(&self.source_root) {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(extension) = path.extension() {
match extension.to_str() {
Some("java") => {
if let Ok(content) = read_to_string(path) {
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
self.java_files.insert(filename.to_string(), content);
}
}
}
Some("kt") => {
if let Ok(content) = read_to_string(path) {
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
self.kotlin_files.insert(filename.to_string(), content);
}
}
}
_ => {}
}
}
}
}
Ok(())
}
pub fn find_log_source(&self, tag: &str, message: &str) -> Option<SourceLocation> {
if let Some(location) = self.search_in_files(&self.java_files, tag, message) {
return Some(location);
}
self.search_in_files(&self.kotlin_files, tag, message)
}
fn search_in_files(
&self,
files: &HashMap<String, String>,
tag: &str,
message: &str,
) -> Option<SourceLocation> {
for (filename, content) in files {
if let Some(location) = self.search_in_file(filename, content, tag, message) {
return Some(location);
}
}
None
}
fn search_in_file(
&self,
filename: &str,
content: &str,
tag: &str,
message: &str,
) -> Option<SourceLocation> {
let lines: Vec<&str> = content.lines().collect();
for (line_idx, line) in lines.iter().enumerate() {
for pattern in &self.log_patterns {
if let Some(captures) = pattern.captures(line) {
let found_tag = captures.get(2).map(|m| m.as_str()).unwrap_or("");
let found_message = captures.get(3).map(|m| m.as_str()).unwrap_or("");
if self.tags_match(tag, found_tag) && self.message_match(message, found_message)
{
let context = self.extract_code_context(&lines, line_idx);
let class_name = self.extract_class_name(content);
let method_name = self.extract_method_name(&lines, line_idx);
return Some(SourceLocation {
file_path: filename.to_string(),
line_number: Some((line_idx + 1) as u32),
method_name,
class_name,
code_context: context,
});
}
}
}
if line.contains(&format!("\"{}\"", message))
|| line.contains(&format!("'{}'", message))
{
let context = self.extract_code_context(&lines, line_idx);
let class_name = self.extract_class_name(content);
let method_name = self.extract_method_name(&lines, line_idx);
return Some(SourceLocation {
file_path: filename.to_string(),
line_number: Some((line_idx + 1) as u32),
method_name,
class_name,
code_context: context,
});
}
}
None
}
fn tags_match(&self, log_tag: &str, code_tag: &str) -> bool {
if log_tag == code_tag {
return true;
}
if code_tag == "TAG" {
return true;
}
log_tag.contains(code_tag) || code_tag.contains(log_tag)
}
fn message_match(&self, log_message: &str, code_message: &str) -> bool {
if log_message == code_message {
return true;
}
let log_words: HashSet<&str> = log_message.split_whitespace().collect();
let code_words: HashSet<&str> = code_message.split_whitespace().collect();
let common_words = log_words.intersection(&code_words).count();
let total_words = log_words.len().max(code_words.len());
if total_words > 0 {
let similarity = common_words as f32 / total_words as f32;
similarity > 0.6
} else {
false
}
}
fn extract_code_context(&self, lines: &[&str], center_line: usize) -> Vec<String> {
let start = center_line.saturating_sub(3);
let end = (center_line + 4).min(lines.len());
lines[start..end]
.iter()
.enumerate()
.map(|(i, line)| {
let line_num = start + i + 1;
let marker = if start + i == center_line {
">>>"
} else {
" "
};
format!("{} {:3}: {}", marker, line_num, line.trim())
})
.collect()
}
fn extract_class_name(&self, content: &str) -> Option<String> {
let class_regex = Regex::new(r"(?:public\s+)?(?:class|interface)\s+(\w+)").ok()?;
class_regex
.captures(content)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
fn extract_method_name(&self, lines: &[&str], line_idx: usize) -> Option<String> {
for i in (0..line_idx).rev() {
let line = lines[i].trim();
let method_patterns = vec![
Regex::new(r"(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)?(\w+)\s*\([^)]*\)\s*\{").ok()?,
Regex::new(r"fun\s+(\w+)\s*\([^)]*\)").ok()?,
];
for pattern in method_patterns {
if let Some(captures) = pattern.captures(line) {
if let Some(method_name) = captures.get(1) {
return Some(method_name.as_str().to_string());
}
}
}
if line.contains("class ") || line.contains("interface ") {
break;
}
}
None
}
pub fn analyze_code_quality(&self, entries: &[&LogEntry]) -> Vec<CodeQualityIssue> {
let mut issues = Vec::new();
// Analyze logging patterns
let mut log_frequency: HashMap<String, usize> = HashMap::new();
let mut error_sources: HashMap<String, usize> = HashMap::new();
for entry in entries {
if let Some(source) = &entry.source_location {
*log_frequency.entry(source.file_path.clone()).or_insert(0) += 1;
if matches!(entry.level, LogLevel::Error | LogLevel::Fatal) {
*error_sources.entry(source.file_path.clone()).or_insert(0) += 1;
}
}
}
// Find files with excessive logging
for (file, count) in log_frequency {
if count > 50 {
issues.push(CodeQualityIssue {
issue_type: CodeIssueType::ExcessiveLogging,
file_path: file.clone(),
description: format!(
"File {} has {} log statements, consider reducing verbosity",
file, count
),
severity: if count > 100 { "high" } else { "medium" }.to_string(),
recommendation:
"Review logging strategy and remove debug logs from production code"
.to_string(),
});
}
}
// Find files with many errors
for (file, count) in error_sources {
if count > 10 {
issues.push(CodeQualityIssue {
issue_type: CodeIssueType::ErrorProne,
file_path: file.clone(),
description: format!(
"File {} generates {} error logs, indicating potential issues",
file, count
),
severity: "high".to_string(),
recommendation: "Review error handling and fix underlying causes of errors"
.to_string(),
});
}
}
issues
}
}
#[derive(Debug)]
pub struct CodeQualityIssue {
pub issue_type: CodeIssueType,
pub file_path: String,
pub description: String,
pub severity: String,
pub recommendation: String,
}
#[derive(Debug)]
pub enum CodeIssueType {
ExcessiveLogging,
ErrorProne,
PerformanceIssue,
SecurityIssue,
BadPractice,
}