agtrace_runtime/ops/
doctor.rs

1use agtrace_engine::{DiagnoseResult, FailureExample, FailureType, categorize_parse_error};
2use agtrace_providers::ProviderAdapter;
3use agtrace_types::AgentEvent;
4use anyhow::Result;
5use std::collections::HashMap;
6use std::fs::File;
7use std::io::{BufRead, BufReader};
8use std::path::{Path, PathBuf};
9use walkdir::WalkDir;
10
11#[derive(Debug, Clone)]
12pub enum CheckStatus {
13    Success,
14    Failure,
15}
16
17#[derive(Debug, Clone)]
18pub struct CheckResult {
19    pub file_path: String,
20    pub provider_name: String,
21    pub status: CheckStatus,
22    pub events: Vec<AgentEvent>,
23    pub error_message: Option<String>,
24}
25
26#[derive(Debug, Clone)]
27pub enum InspectContentType {
28    Raw(String),
29    Json(serde_json::Value),
30}
31
32#[derive(Debug, Clone)]
33pub struct InspectLine {
34    pub number: usize,
35    pub content: InspectContentType,
36}
37
38#[derive(Debug, Clone)]
39pub struct InspectResult {
40    pub file_path: String,
41    pub total_lines: usize,
42    pub shown_lines: usize,
43    pub lines: Vec<InspectLine>,
44}
45
46pub struct DoctorService;
47
48impl DoctorService {
49    pub fn diagnose_all(providers: &[(ProviderAdapter, PathBuf)]) -> Result<Vec<DiagnoseResult>> {
50        let mut results = Vec::new();
51        for (provider, root) in providers {
52            if root.exists() {
53                let res = Self::diagnose_provider(provider, root)?;
54                results.push(res);
55            }
56        }
57        Ok(results)
58    }
59
60    fn diagnose_provider(provider: &ProviderAdapter, log_root: &Path) -> Result<DiagnoseResult> {
61        let mut all_files = Vec::new();
62
63        for entry in WalkDir::new(log_root).into_iter().filter_map(|e| e.ok()) {
64            let path = entry.path();
65            if !path.is_file() {
66                continue;
67            }
68
69            if provider.discovery.probe(path).is_match() {
70                all_files.push(path.to_path_buf());
71            }
72        }
73
74        let files_to_check = all_files;
75
76        let mut result = DiagnoseResult {
77            provider_name: provider.id().to_string(),
78            total_files: files_to_check.len(),
79            successful: 0,
80            failures: HashMap::new(),
81        };
82
83        for file_path in files_to_check {
84            match Self::test_parse_file(provider, &file_path) {
85                Ok(_) => {
86                    result.successful += 1;
87                }
88                Err((failure_type, reason)) => {
89                    result
90                        .failures
91                        .entry(failure_type)
92                        .or_default()
93                        .push(FailureExample {
94                            path: file_path.display().to_string(),
95                            reason,
96                        });
97                }
98            }
99        }
100
101        Ok(result)
102    }
103
104    fn test_parse_file(
105        provider: &ProviderAdapter,
106        path: &Path,
107    ) -> Result<(), (FailureType, String)> {
108        match provider.parser.parse_file(path) {
109            Ok(_events) => Ok(()),
110            Err(e) => {
111                let error_msg = format!("{:?}", e);
112                Err(categorize_parse_error(&error_msg))
113            }
114        }
115    }
116
117    pub fn check_file(
118        file_path: &str,
119        provider: &ProviderAdapter,
120        provider_name: &str,
121    ) -> Result<CheckResult> {
122        let path = Path::new(file_path);
123
124        if !path.exists() {
125            anyhow::bail!("File not found: {}", file_path);
126        }
127
128        match provider.parser.parse_file(path) {
129            Ok(events) => Ok(CheckResult {
130                file_path: file_path.to_string(),
131                provider_name: provider_name.to_string(),
132                status: CheckStatus::Success,
133                events,
134                error_message: None,
135            }),
136            Err(e) => Ok(CheckResult {
137                file_path: file_path.to_string(),
138                provider_name: provider_name.to_string(),
139                status: CheckStatus::Failure,
140                events: vec![],
141                error_message: Some(format!("{:#}", e)),
142            }),
143        }
144    }
145
146    pub fn inspect_file(file_path: &str, lines: usize, json_format: bool) -> Result<InspectResult> {
147        let path = Path::new(file_path);
148
149        if !path.exists() {
150            anyhow::bail!("File not found: {}", file_path);
151        }
152
153        let file = File::open(path)?;
154        let reader = BufReader::new(file);
155
156        let total_lines = std::fs::read_to_string(path)?.lines().count();
157
158        let mut rendered_lines = Vec::new();
159        for (idx, line) in reader.lines().take(lines).enumerate() {
160            let line = line?;
161            let content = if json_format {
162                match serde_json::from_str::<serde_json::Value>(&line) {
163                    Ok(json) => InspectContentType::Json(json),
164                    Err(_) => InspectContentType::Raw(line.clone()),
165                }
166            } else {
167                InspectContentType::Raw(line.clone())
168            };
169            rendered_lines.push(InspectLine {
170                number: idx + 1,
171                content,
172            });
173        }
174
175        Ok(InspectResult {
176            file_path: file_path.to_string(),
177            total_lines,
178            shown_lines: rendered_lines.len(),
179            lines: rendered_lines,
180        })
181    }
182}