/ src / bin / cmd / log.rs
log.rs
  1  use std::{
  2      io::Write,
  3      path::{Path, PathBuf},
  4  };
  5  
  6  use clap::{Parser, ValueEnum};
  7  
  8  use super::{AmbientError, Config, Leaf};
  9  use ambient_ci::{
 10      project::{ProjectError, State},
 11      runlog::{RunLog, RunLogError, SynthLog},
 12  };
 13  
 14  /// Show run log.
 15  ///
 16  /// Output can be the raw run log from the VM, a JSON Lines log with
 17  /// everything that Ambient did durig the CI run (including outside
 18  /// the VM) or an HTML version of that.
 19  #[derive(Debug, Parser)]
 20  pub struct Log {
 21      /// Output the console log, not the run log. Output format is always "raw".
 22      #[clap(conflicts_with = "format", long)]
 23      console: bool,
 24  
 25      /// What format should log be?
 26      #[clap(long, default_value = "raw")]
 27      format: Format,
 28  
 29      /// Which project log should be written out?
 30      #[clap(conflicts_with = "filename")]
 31      project: Option<String>,
 32  
 33      /// Read run log from this file.
 34      #[clap(long)]
 35      filename: Option<PathBuf>,
 36  
 37      /// Write log to this file, not stdout.
 38      #[clap(long)]
 39      output: Option<PathBuf>,
 40  }
 41  
 42  impl Log {
 43      fn state(&self, config: &Config, project: &str) -> Result<State, LogError> {
 44          let statedir = config.state();
 45          if !statedir.exists() {
 46              return Err(LogError::NoStateDir(project.to_string(), statedir.into()))?;
 47          }
 48          State::from_file(statedir, project).map_err(LogError::Project)
 49      }
 50  
 51      fn read(&self, filename: &Path) -> Result<Vec<u8>, LogError> {
 52          std::fs::read(filename).map_err(|err| LogError::Read(filename.to_path_buf(), err))
 53      }
 54  
 55      fn write(&self, data: &[u8]) -> Result<(), LogError> {
 56          if let Some(output) = &self.output {
 57              std::fs::write(output, data)
 58                  .map_err(|err| LogError::Write(output.to_path_buf(), err))?;
 59          } else {
 60              std::io::stdout()
 61                  .write_all(data)
 62                  .map_err(LogError::WriteStdout)?;
 63          }
 64          Ok(())
 65      }
 66  }
 67  
 68  impl Leaf for Log {
 69      fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
 70          match (self.format, self.console, &self.project, &self.filename) {
 71              (Format::Raw, false, Some(project), None) => {
 72                  // project run log, raw
 73                  let state = self.state(config, project)?;
 74                  let data = self.read(&state.raw_log_filename())?;
 75                  self.write(&data)?;
 76              }
 77              (Format::Raw, false, None, Some(filename)) => {
 78                  // named file run log, raw
 79                  let data = self.read(filename)?;
 80                  self.write(&data)?;
 81              }
 82              (Format::Raw, true, Some(project), None) => {
 83                  // project console log, raw
 84                  let state = self.state(config, project)?;
 85                  let data = self.read(&state.console_log_filename())?;
 86                  self.write(&data)?;
 87              }
 88              (Format::Raw, true, None, Some(filename)) => {
 89                  // named file console log, raw
 90                  let data = self.read(filename)?;
 91                  self.write(&data)?;
 92              }
 93              (Format::Json, false, Some(project), None) => {
 94                  // project run log, JSON
 95                  let state = self.state(config, project)?;
 96                  let data = self.read(&state.raw_log_filename())?;
 97                  self.write(&data)?;
 98              }
 99              (Format::Json, false, None, Some(filename)) => {
100                  // name file run log, JSON
101                  let data = self.read(filename)?;
102                  self.write(&data)?;
103              }
104              (Format::Html, false, Some(project), None) => {
105                  // project run log, HTML
106                  let state = self.state(config, project)?;
107                  let data = self.read(&state.run_log_filename())?;
108                  let runlog = RunLog::parse_jsonl(data)
109                      .map_err(|err| LogError::LoadJson(state.run_log_filename(), err))?;
110                  let mut synth = SynthLog::new(runlog.msgs());
111                  let mut data = self.read(&state.console_log_filename())?;
112                  data.retain(|byte| *byte != b'\r' && *byte != b'\x1b');
113                  synth.set_console_log(data);
114                  self.write(synth.to_html().to_string().as_bytes())?;
115              }
116  
117              (_, _, None, None)
118              | (_, _, Some(_), Some(_))
119              | (Format::Json, true, _, _)
120              | (Format::Html, true, _, _)
121              | (Format::Html, false, None, Some(_)) => {
122                  Err(LogError::Usage)?;
123              }
124          }
125  
126          Ok(())
127      }
128  }
129  
130  #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
131  enum Format {
132      Raw,
133      Json,
134      Html,
135  }
136  
137  #[derive(Debug, thiserror::Error)]
138  pub enum LogError {
139      #[error("state directory for project {0} does not exist: {1}")]
140      NoStateDir(String, PathBuf),
141  
142      #[error(transparent)]
143      Project(#[from] ProjectError),
144  
145      #[error("failed to load Ambient JSON run log file {0}")]
146      LoadJson(PathBuf, #[source] RunLogError),
147  
148      #[error("failed to read log file {0}")]
149      Read(PathBuf, #[source] std::io::Error),
150  
151      #[error("failed to write output to {0}")]
152      Write(PathBuf, #[source] std::io::Error),
153  
154      #[error("failed to write output to stdout")]
155      WriteStdout(#[source] std::io::Error),
156  
157      #[error(
158          "the combination of output format, console log, project, and file names is not acceptable"
159      )]
160      Usage,
161  }