v1 init
This commit is contained in:
commit
fb485a6c1c
13 changed files with 3566 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
1892
Cargo.lock
generated
Normal file
1892
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal 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
19
README.md
Normal 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
356
src/ai/analyzer.rs
Normal 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
1
src/ai/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod analyzer;
|
||||
684
src/logcat/logcat_analyzer.rs
Normal file
684
src/logcat/logcat_analyzer.rs
Normal 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
1
src/logcat/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub(crate) mod logcat_analyzer;
|
||||
185
src/main.rs
Normal file
185
src/main.rs
Normal 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
39
src/model/ai.rs
Normal 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
51
src/model/log_entry.rs
Normal 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
3
src/model/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod ai;
|
||||
pub mod log_entry;
|
||||
pub mod source;
|
||||
320
src/model/source.rs
Normal file
320
src/model/source.rs
Normal 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,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue