1use std::path::Path;
2use std::time::Instant;
3
4pub struct SimpleTracer;
6
7impl SimpleTracer {
8 pub fn is_enabled() -> bool {
10 std::env::var("RGEN_TRACE").is_ok()
11 }
12
13 pub fn trace_level() -> TraceLevel {
15 match std::env::var("RGEN_TRACE")
16 .unwrap_or_default()
17 .to_lowercase()
18 .as_str()
19 {
20 "error" => TraceLevel::Error,
21 "warn" => TraceLevel::Warn,
22 "info" => TraceLevel::Info,
23 "debug" => TraceLevel::Debug,
24 "trace" => TraceLevel::Trace,
25 "1" | "true" | "yes" => TraceLevel::Debug,
26 "0" | "false" | "no" => TraceLevel::Error,
27 _ => TraceLevel::Info,
28 }
29 }
30
31 pub fn trace(level: TraceLevel, message: &str, context: Option<&str>) {
33 if !Self::is_enabled() {
34 return;
35 }
36
37 let current_level = Self::trace_level();
38 if level as u8 > current_level as u8 {
39 return;
40 }
41
42 let prefix = match level {
43 TraceLevel::Error => "ERROR",
44 TraceLevel::Warn => "WARN ",
45 TraceLevel::Info => "INFO ",
46 TraceLevel::Debug => "DEBUG",
47 TraceLevel::Trace => "TRACE",
48 };
49
50 if let Some(ctx) = context {
51 eprintln!("[RGEN {}] {}: {}", prefix, ctx, message);
52 } else {
53 eprintln!("[RGEN {}] {}", prefix, message);
54 }
55 }
56
57 pub fn template_start(template_path: &Path) {
59 Self::trace(
60 TraceLevel::Info,
61 &format!("Starting template processing: {}", template_path.display()),
62 None,
63 );
64 }
65
66 pub fn template_complete(template_path: &Path, output_path: &Path, content_size: usize) {
68 Self::trace(
69 TraceLevel::Info,
70 &format!(
71 "Template processing complete: {} -> {} ({} bytes)",
72 template_path.display(),
73 output_path.display(),
74 content_size
75 ),
76 None,
77 );
78 }
79
80 pub fn frontmatter_processed(frontmatter: &crate::template::Frontmatter) {
82 Self::trace(
83 TraceLevel::Debug,
84 &format!(
85 "Frontmatter processed: to={:?}, inject={}, vars={}",
86 frontmatter.to,
87 frontmatter.inject,
88 frontmatter.vars.len()
89 ),
90 Some("frontmatter"),
91 );
92 }
93
94 pub fn context_blessed(vars_count: usize) {
96 Self::trace(
97 TraceLevel::Debug,
98 &format!(
99 "Context blessed: {} variables (Name, locals added)",
100 vars_count
101 ),
102 Some("context"),
103 );
104 }
105
106 pub fn rdf_loading(files: &[String], inline_blocks: usize, triples: usize) {
108 Self::trace(
109 TraceLevel::Info,
110 &format!(
111 "RDF loaded: {} files, {} inline blocks, {} triples",
112 files.len(),
113 inline_blocks,
114 triples
115 ),
116 Some("rdf"),
117 );
118 }
119
120 pub fn sparql_query(query: &str, result_count: Option<usize>) {
122 let count_str = result_count
123 .map(|c| c.to_string())
124 .unwrap_or_else(|| "N/A".to_string());
125 Self::trace(
126 TraceLevel::Debug,
127 &format!("SPARQL query: {} results", count_str),
128 Some("sparql"),
129 );
130 Self::trace(TraceLevel::Trace, query, Some("sparql"));
131 }
132
133 pub fn file_injection(target_path: &Path, mode: &str, success: bool) {
135 let status = if success { "completed" } else { "failed" };
136 Self::trace(
137 TraceLevel::Info,
138 &format!("File injection {}: {} mode", status, mode),
139 Some(&format!("injection:{}", target_path.display())),
140 );
141 }
142
143 pub fn shell_hook(command: &str, timing: &str, exit_code: i32) {
145 let status = if exit_code == 0 {
146 "completed"
147 } else {
148 "failed"
149 };
150 Self::trace(
151 TraceLevel::Info,
152 &format!(
153 "Shell hook {}: {} (exit code: {})",
154 status, timing, exit_code
155 ),
156 Some(&format!("hook:{}", command)),
157 );
158 }
159
160 pub fn performance(operation: &str, duration_ms: u64) {
162 Self::trace(
163 TraceLevel::Debug,
164 &format!("Performance: {} took {}ms", operation, duration_ms),
165 Some("performance"),
166 );
167 }
168
169 pub fn dry_run(output_path: &Path, content_size: usize) {
171 Self::trace(
172 TraceLevel::Info,
173 &format!(
174 "DRY RUN: Would generate {} ({} bytes)",
175 output_path.display(),
176 content_size
177 ),
178 Some("dry_run"),
179 );
180 }
181
182 pub fn backup_created(original_path: &Path, backup_path: &Path) {
184 Self::trace(
185 TraceLevel::Info,
186 &format!(
187 "Backup created: {} -> {}",
188 original_path.display(),
189 backup_path.display()
190 ),
191 Some("backup"),
192 );
193 }
194
195 pub fn skip_condition(condition: &str, reason: &str) {
197 Self::trace(
198 TraceLevel::Info,
199 &format!("Skipped: {} ({})", condition, reason),
200 Some("skip"),
201 );
202 }
203
204 pub fn error(error: &anyhow::Error, context: &str) {
206 Self::trace(
207 TraceLevel::Error,
208 &format!("Error in {}: {}", context, error),
209 Some("error"),
210 );
211 }
212
213 pub fn warning(message: &str, context: Option<&str>) {
215 Self::trace(TraceLevel::Warn, message, context);
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
220pub enum TraceLevel {
221 Error = 0,
222 Warn = 1,
223 Info = 2,
224 Debug = 3,
225 Trace = 4,
226}
227
228pub struct SimpleTimer {
230 start: Instant,
231 operation: String,
232}
233
234impl SimpleTimer {
235 pub fn start(operation: &str) -> Self {
237 Self {
238 start: Instant::now(),
239 operation: operation.to_string(),
240 }
241 }
242
243 pub fn finish(self) {
245 let duration = self.start.elapsed();
246 SimpleTracer::performance(&self.operation, duration.as_millis() as u64);
247 }
248}
249
250#[macro_export]
252macro_rules! time_operation {
253 ($name:expr, $block:block) => {{
254 let _timer = $crate::simple_tracing::SimpleTimer::start($name);
255 let result = $block;
256 _timer.finish();
257 result
258 }};
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use std::fs;
265 use tempfile::TempDir;
266
267 #[test]
268 fn test_trace_level_ordering() {
269 assert!(TraceLevel::Error < TraceLevel::Warn);
270 assert!(TraceLevel::Warn < TraceLevel::Info);
271 assert!(TraceLevel::Info < TraceLevel::Debug);
272 assert!(TraceLevel::Debug < TraceLevel::Trace);
273 }
274
275 #[test]
276 fn test_simple_timer() {
277 let timer = SimpleTimer::start("test_operation");
278 std::thread::sleep(std::time::Duration::from_millis(10));
279 timer.finish(); }
281
282 #[test]
283 fn test_tracing_methods() {
284 let temp_dir = TempDir::new().unwrap();
285 let test_path = temp_dir.path().join("test.tmpl");
286 fs::write(&test_path, "test content").unwrap();
287
288 SimpleTracer::template_start(&test_path);
290 SimpleTracer::template_complete(&test_path, &test_path, 100);
291
292 let frontmatter = crate::template::Frontmatter::default();
293 SimpleTracer::frontmatter_processed(&frontmatter);
294
295 SimpleTracer::context_blessed(5);
296 SimpleTracer::rdf_loading(&["file1.ttl".to_string()], 2, 100);
297 SimpleTracer::sparql_query("SELECT * WHERE { ?s ?p ?o }", Some(10));
298
299 SimpleTracer::file_injection(&test_path, "append", true);
300 SimpleTracer::shell_hook("echo 'test'", "before", 0);
301 SimpleTracer::performance("test_operation", 50);
302 SimpleTracer::dry_run(&test_path, 500);
303 SimpleTracer::backup_created(&test_path, &temp_dir.path().join("backup.tmpl"));
304 SimpleTracer::skip_condition("skip_if", "pattern found");
305
306 let error = anyhow::anyhow!("Test error");
307 SimpleTracer::error(&error, "test context");
308 SimpleTracer::warning("Test warning", Some("test context"));
309 SimpleTracer::warning("Test warning", None);
310 }
311
312 #[test]
313 fn test_tracing_environment_variables() {
314 let test_values = [
316 "error", "warn", "info", "debug", "trace", "1", "0", "true", "false",
317 ];
318
319 for value in &test_values {
320 std::env::set_var("RGEN_TRACE", value);
321 let level = SimpleTracer::trace_level();
322 assert!(matches!(
323 level,
324 TraceLevel::Error
325 | TraceLevel::Warn
326 | TraceLevel::Info
327 | TraceLevel::Debug
328 | TraceLevel::Trace
329 ));
330 }
331 }
332
333 #[test]
334 fn test_time_operation_macro() {
335 let result = crate::time_operation!("test_op", {
336 std::thread::sleep(std::time::Duration::from_millis(2));
337 42
338 });
339 assert_eq!(result, 42);
340 }
341}