zernel/commands/
log.rs

1// Copyright (C) 2026 Dyber, Inc. — Proprietary
2
3use crate::experiments::store::ExperimentStore;
4use crate::experiments::tracker;
5use anyhow::Result;
6use std::path::PathBuf;
7
8/// Show training logs for an experiment.
9pub async fn run(id: Option<String>, follow: bool, grep: Option<String>) -> Result<()> {
10    let db_path = tracker::experiments_db_path();
11    let store = ExperimentStore::open(&db_path)?;
12
13    // Resolve experiment ID
14    let exp_id = match id {
15        Some(id) => id,
16        None => {
17            // Get the latest experiment
18            let exps = store.list(1)?;
19            match exps.first() {
20                Some(exp) => exp.id.clone(),
21                None => {
22                    println!("No experiments yet. Run `zernel run <script>` first.");
23                    return Ok(());
24                }
25            }
26        }
27    };
28
29    // Verify experiment exists
30    let exp = store
31        .get(&exp_id)?
32        .ok_or_else(|| anyhow::anyhow!("experiment not found: {exp_id}"))?;
33
34    let log_path = experiment_log_path(&exp_id);
35
36    println!("Log for experiment: {} ({})", exp.id, exp.name);
37    println!("  Status: {}", exp.status);
38    if let Some(d) = exp.duration_secs {
39        println!("  Duration: {d:.1}s");
40    }
41    println!("  Log file: {}", log_path.display());
42    println!();
43
44    if !log_path.exists() {
45        println!("(no log file found — logs are saved for experiments run with zernel >= v0.1.0)");
46        return Ok(());
47    }
48
49    let content = std::fs::read_to_string(&log_path)?;
50
51    if let Some(ref pattern) = grep {
52        // Filtered output
53        let mut found = 0;
54        for (i, line) in content.lines().enumerate() {
55            if line.contains(pattern.as_str()) {
56                println!("{:>6}: {line}", i + 1);
57                found += 1;
58            }
59        }
60        if found == 0 {
61            println!("No lines matching '{pattern}'");
62        } else {
63            println!("\n{found} matching line(s)");
64        }
65    } else if follow && exp.status.to_string() == "Running" {
66        // Follow mode for active experiments
67        println!("--- following (Ctrl+C to stop) ---");
68        println!();
69
70        // Print existing content
71        print!("{content}");
72
73        // Tail the file
74        let mut last_len = content.len();
75        loop {
76            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
77            if let Ok(new_content) = std::fs::read_to_string(&log_path) {
78                if new_content.len() > last_len {
79                    print!("{}", &new_content[last_len..]);
80                    last_len = new_content.len();
81                }
82            }
83        }
84    } else {
85        // Full output
86        print!("{content}");
87    }
88
89    Ok(())
90}
91
92/// Get the log file path for an experiment.
93pub fn experiment_log_path(exp_id: &str) -> PathBuf {
94    tracker::zernel_dir()
95        .join("experiments")
96        .join(exp_id)
97        .join("output.log")
98}