1mod agent_output;
7mod errors;
8mod log_rotation;
9mod orchestration;
10mod performance;
11mod stream_handler;
12mod trace_layer;
13
14#[cfg(test)]
15mod integration_tests;
16
17pub use agent_output::{AgentOutputContent, AgentOutputEntry, AgentOutputLogger};
18pub use errors::{DiagnosticError, ErrorLogger};
19pub use log_rotation::{create_log_file, rotate_logs};
20pub use orchestration::{OrchestrationEvent, OrchestrationLogger};
21pub use performance::{PerformanceLogger, PerformanceMetric};
22pub use stream_handler::DiagnosticStreamHandler;
23pub use trace_layer::{DiagnosticTraceLayer, TraceEntry};
24
25use chrono::Local;
26use std::fs;
27use std::path::{Path, PathBuf};
28use std::sync::{Arc, Mutex};
29
30pub struct DiagnosticsCollector {
35 enabled: bool,
36 session_dir: Option<PathBuf>,
37 orchestration_logger: Option<Arc<Mutex<orchestration::OrchestrationLogger>>>,
38 performance_logger: Option<Arc<Mutex<performance::PerformanceLogger>>>,
39 error_logger: Option<Arc<Mutex<errors::ErrorLogger>>>,
40}
41
42impl DiagnosticsCollector {
43 pub fn new(base_path: &Path) -> std::io::Result<Self> {
47 let enabled = std::env::var("RALPH_DIAGNOSTICS")
48 .map(|v| v == "1")
49 .unwrap_or(false);
50
51 Self::with_enabled(base_path, enabled)
52 }
53
54 pub fn with_enabled(base_path: &Path, enabled: bool) -> std::io::Result<Self> {
56 let (session_dir, orchestration_logger, performance_logger, error_logger) = if enabled {
57 let timestamp = Local::now().format("%Y-%m-%dT%H-%M-%S");
58 let dir = base_path
59 .join(".ralph")
60 .join("diagnostics")
61 .join(timestamp.to_string());
62 fs::create_dir_all(&dir)?;
63
64 let orch_logger = orchestration::OrchestrationLogger::new(&dir)?;
65 let perf_logger = performance::PerformanceLogger::new(&dir)?;
66 let err_logger = errors::ErrorLogger::new(&dir)?;
67 (
68 Some(dir),
69 Some(Arc::new(Mutex::new(orch_logger))),
70 Some(Arc::new(Mutex::new(perf_logger))),
71 Some(Arc::new(Mutex::new(err_logger))),
72 )
73 } else {
74 (None, None, None, None)
75 };
76
77 Ok(Self {
78 enabled,
79 session_dir,
80 orchestration_logger,
81 performance_logger,
82 error_logger,
83 })
84 }
85
86 pub fn disabled() -> Self {
88 Self {
89 enabled: false,
90 session_dir: None,
91 orchestration_logger: None,
92 performance_logger: None,
93 error_logger: None,
94 }
95 }
96
97 pub fn is_enabled(&self) -> bool {
99 self.enabled
100 }
101
102 pub fn session_dir(&self) -> Option<&Path> {
104 self.session_dir.as_deref()
105 }
106
107 pub fn wrap_stream_handler<H>(&self, handler: H) -> Result<DiagnosticStreamHandler<H>, H> {
111 if let Some(session_dir) = &self.session_dir {
112 match AgentOutputLogger::new(session_dir) {
113 Ok(logger) => {
114 let logger = Arc::new(Mutex::new(logger));
115 Ok(DiagnosticStreamHandler::new(handler, logger))
116 }
117 Err(_) => Err(handler), }
119 } else {
120 Err(handler) }
122 }
123
124 pub fn log_orchestration(&self, iteration: u32, hat: &str, event: OrchestrationEvent) {
128 if let Some(logger) = &self.orchestration_logger
129 && let Ok(mut logger) = logger.lock()
130 {
131 let _ = logger.log(iteration, hat, event);
132 }
133 }
134
135 pub fn log_performance(&self, iteration: u32, hat: &str, metric: PerformanceMetric) {
139 if let Some(logger) = &self.performance_logger
140 && let Ok(mut logger) = logger.lock()
141 {
142 let _ = logger.log(iteration, hat, metric);
143 }
144 }
145
146 pub fn log_error(&self, iteration: u32, hat: &str, error: DiagnosticError) {
150 if let Some(logger) = &self.error_logger
151 && let Ok(mut logger) = logger.lock()
152 {
153 logger.set_context(iteration, hat);
154 logger.log(error);
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use tempfile::TempDir;
163
164 #[test]
165 fn test_diagnostics_disabled_by_default() {
166 let temp = TempDir::new().unwrap();
167
168 let collector = DiagnosticsCollector::with_enabled(temp.path(), false).unwrap();
169
170 assert!(!collector.is_enabled());
171 assert!(collector.session_dir().is_none());
172 }
173
174 #[test]
175 fn test_diagnostics_enabled_creates_directory() {
176 let temp = TempDir::new().unwrap();
177
178 let collector = DiagnosticsCollector::with_enabled(temp.path(), true).unwrap();
179
180 assert!(collector.is_enabled());
181 assert!(collector.session_dir().is_some());
182 assert!(collector.session_dir().unwrap().exists());
183 }
184
185 #[test]
186 fn test_session_directory_format() {
187 let temp = TempDir::new().unwrap();
188
189 let collector = DiagnosticsCollector::with_enabled(temp.path(), true).unwrap();
190
191 let session_dir = collector.session_dir().unwrap();
192 let dir_name = session_dir.file_name().unwrap().to_str().unwrap();
193
194 assert!(dir_name.len() == 19); assert!(dir_name.chars().nth(4) == Some('-'));
197 assert!(dir_name.chars().nth(7) == Some('-'));
198 assert!(dir_name.chars().nth(10) == Some('T'));
199 assert!(dir_name.chars().nth(13) == Some('-'));
200 assert!(dir_name.chars().nth(16) == Some('-'));
201 }
202
203 #[test]
204 fn test_performance_logger_integration() {
205 let temp = TempDir::new().unwrap();
206 let collector = DiagnosticsCollector::with_enabled(temp.path(), true).unwrap();
207
208 collector.log_performance(
210 1,
211 "ralph",
212 PerformanceMetric::IterationDuration { duration_ms: 1500 },
213 );
214 collector.log_performance(
215 1,
216 "builder",
217 PerformanceMetric::AgentLatency { duration_ms: 800 },
218 );
219 collector.log_performance(
220 1,
221 "builder",
222 PerformanceMetric::TokenCount {
223 input: 1000,
224 output: 500,
225 },
226 );
227
228 let perf_file = collector.session_dir().unwrap().join("performance.jsonl");
230 assert!(perf_file.exists(), "performance.jsonl should exist");
231
232 let content = std::fs::read_to_string(perf_file).unwrap();
234 let lines: Vec<_> = content.lines().collect();
235 assert_eq!(lines.len(), 3, "Should have 3 performance entries");
236
237 for line in lines {
239 let _: performance::PerformanceEntry = serde_json::from_str(line).unwrap();
240 }
241 }
242
243 #[test]
244 fn test_error_logger_integration() {
245 let temp = TempDir::new().unwrap();
246 let collector = DiagnosticsCollector::with_enabled(temp.path(), true).unwrap();
247
248 collector.log_error(
250 1,
251 "ralph",
252 DiagnosticError::ParseError {
253 source: "agent_output".to_string(),
254 message: "Invalid JSON".to_string(),
255 input: "{invalid".to_string(),
256 },
257 );
258 collector.log_error(
259 2,
260 "builder",
261 DiagnosticError::ValidationFailure {
262 rule: "tests_required".to_string(),
263 message: "Missing test evidence".to_string(),
264 evidence: "tests: missing".to_string(),
265 },
266 );
267
268 let error_file = collector.session_dir().unwrap().join("errors.jsonl");
270 assert!(error_file.exists(), "errors.jsonl should exist");
271
272 let content = std::fs::read_to_string(error_file).unwrap();
274 let lines: Vec<_> = content.lines().collect();
275 assert_eq!(lines.len(), 2, "Should have 2 error entries");
276
277 for line in lines {
279 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
280 assert!(parsed.get("error_type").is_some());
281 assert!(parsed.get("message").is_some());
282 assert!(parsed.get("context").is_some());
283 }
284 }
285}