agtrace_runtime/ops/
doctor.rs

1use crate::{Error, Result};
2use agtrace_engine::{DiagnoseResult, FailureExample, FailureType, categorize_parse_error};
3use agtrace_providers::ProviderAdapter;
4use agtrace_types::AgentEvent;
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    ) -> std::result::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            return Err(Error::InvalidOperation(format!(
126                "File not found: {}",
127                file_path
128            )));
129        }
130
131        match provider.parser.parse_file(path) {
132            Ok(events) => Ok(CheckResult {
133                file_path: file_path.to_string(),
134                provider_name: provider_name.to_string(),
135                status: CheckStatus::Success,
136                events,
137                error_message: None,
138            }),
139            Err(e) => Ok(CheckResult {
140                file_path: file_path.to_string(),
141                provider_name: provider_name.to_string(),
142                status: CheckStatus::Failure,
143                events: vec![],
144                error_message: Some(format!("{:#}", e)),
145            }),
146        }
147    }
148
149    pub fn inspect_file(file_path: &str, lines: usize, json_format: bool) -> Result<InspectResult> {
150        let path = Path::new(file_path);
151
152        if !path.exists() {
153            return Err(Error::InvalidOperation(format!(
154                "File not found: {}",
155                file_path
156            )));
157        }
158
159        let file = File::open(path)?;
160        let reader = BufReader::new(file);
161
162        let total_lines = std::fs::read_to_string(path)?.lines().count();
163
164        let mut rendered_lines = Vec::new();
165        for (idx, line) in reader.lines().take(lines).enumerate() {
166            let line = line?;
167            let content = if json_format {
168                match serde_json::from_str::<serde_json::Value>(&line) {
169                    Ok(json) => InspectContentType::Json(json),
170                    Err(_) => InspectContentType::Raw(line.clone()),
171                }
172            } else {
173                InspectContentType::Raw(line.clone())
174            };
175            rendered_lines.push(InspectLine {
176                number: idx + 1,
177                content,
178            });
179        }
180
181        Ok(InspectResult {
182            file_path: file_path.to_string(),
183            total_lines,
184            shown_lines: rendered_lines.len(),
185            lines: rendered_lines,
186        })
187    }
188}