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 }