agtrace_runtime/ops/
doctor.rs1use 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}