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