Radish alpha
r
rad:zwTxygwuz5LDGBq255RA2CbNGrz8
Radicle CI broker
Radicle
Git
fix: remove random unrelated words from option doc help text
Merged liw opened 8 months ago

Signed-off-by: Lars Wirzenius liw@liw.fi

feat: add cibtool log --jsonl option

Signed-off-by: Lars Wirzenius liw@liw.fi

feat! make cibtool log read cib log output as well

Previously we only supported reading cib log output wrapped into journald messages as JSON.

This is a breaking change because the command line syntax changes slightly.

Signed-off-by: Lars Wirzenius liw@liw.fi

2 files changed +134 -29 6596d9ec b84fffaf
modified ci-broker.md
@@ -2411,11 +2411,11 @@ _Who:_ `cib-devs`
given a Radicle node, with CI configured with broker.yaml and adapter dummy.sh
given file journal.json

-
when I run cibtool log --journal journal.json --output x.json
+
when I run cibtool log --format journald journal.json --output x.json
when I run jq . x.json
then command is successful

-
when I run cibtool log --journal journal.json --broker-run-id 62c45727-a4d8-4a29-9dae-88c6e8b61655 --output x.json
+
when I run cibtool log --format journald journal.json --broker-run-id 62c45727-a4d8-4a29-9dae-88c6e8b61655 --output x.json
when I run grep -c timestamp x.json
then stdout has one line
~~~
modified src/bin/cibtoolcmd/log.rs
@@ -5,6 +5,7 @@ use std::{
    path::{Path, PathBuf},
};

+
use clap::ValueEnum;
use logger::LogLevel;
use serde::Deserialize;
use serde_json::Value;
@@ -19,10 +20,13 @@ use super::*;
/// default text format that's hard to parse.
#[derive(Debug, Parser)]
pub struct LogCmd {
-
    /// Read journalctl JSON output from this file. repository name.
-
    /// Default is to read from the standard input.
+
    /// Input is this format.
    #[clap(long)]
-
    journal: Option<PathBuf>,
+
    format: LogKind,
+

+
    /// One JSON object per line.
+
    #[clap(long)]
+
    jsonl: bool,

    /// Include messages only if they are at least of this log level.
    /// Default is to allow any log level.
@@ -45,6 +49,10 @@ pub struct LogCmd {
    /// output.
    #[clap(long)]
    output: Option<PathBuf>,
+

+
    /// Read input from this file. Default is to read from the
+
    /// standard input.
+
    log_file: Option<PathBuf>,
}

#[allow(clippy::unwrap_used)]
@@ -88,6 +96,26 @@ impl LogCmd {
        }
        true
    }
+

+
    fn write_pretty(&self, mut out: impl Write, journal: JournalLines) -> Result<(), LogError> {
+
        let messages = journal.flatten().filter(|msg| self.allowed(msg));
+
        for msg in messages {
+
            out.write_all(format!("{}\n", pretty(msg)?).as_bytes())
+
                .map_err(LogError::Write)?;
+
        }
+

+
        Ok(())
+
    }
+

+
    fn write_jsonl(&self, mut out: impl Write, journal: JournalLines) -> Result<(), LogError> {
+
        let messages = journal.flatten().filter(|msg| self.allowed(msg));
+
        for msg in messages {
+
            out.write_all(format!("{}\n", jsonl(msg)?).as_bytes())
+
                .map_err(LogError::Write)?;
+
        }
+

+
        Ok(())
+
    }
}

fn log_level(msg: &Value) -> Option<LogLevel> {
@@ -116,25 +144,43 @@ fn pretty(msg: Value) -> Result<String, LogError> {
    serde_json::to_string_pretty(&msg).map_err(|err| LogError::JsonSer(msg, err))
}

+
fn jsonl(msg: Value) -> Result<String, LogError> {
+
    serde_json::to_string(&msg).map_err(|err| LogError::JsonSer(msg, err))
+
}
+

impl Leaf for LogCmd {
    fn run(&self, _args: &Args) -> Result<(), CibToolError> {
-
        let journal = if let Some(filename) = &self.journal {
-
            JournalLines::from_file(filename)?
-
        } else {
-
            JournalLines::from_stdin()?
+
        let journal = match self.format {
+
            LogKind::Cib => {
+
                if let Some(filename) = &self.log_file {
+
                    JournalLines::cib_from_file(filename)?
+
                } else {
+
                    JournalLines::cib_from_stdin()?
+
                }
+
            }
+
            LogKind::Journald => {
+
                if let Some(filename) = &self.log_file {
+
                    JournalLines::journal_from_file(filename)?
+
                } else {
+
                    JournalLines::journal_from_stdin()?
+
                }
+
            }
        };

-
        let messages = journal.flatten().filter(|msg| self.allowed(msg));
        if let Some(filename) = &self.output {
-
            let mut file = File::create(filename)
+
            let file = File::create(filename)
                .map_err(|err| LogError::CreateOutput(filename.into(), err))?;
-
            for msg in messages {
-
                file.write_all(format!("{}\n", pretty(msg)?).as_bytes())
-
                    .map_err(|err| LogError::WriteOutput(filename.into(), err))?;
+
            if self.jsonl {
+
                self.write_jsonl(file, journal)?;
+
            } else {
+
                self.write_pretty(file, journal)?;
            }
        } else {
-
            for msg in messages {
-
                println!("{}", pretty(msg)?);
+
            let out = std::io::stdout();
+
            if self.jsonl {
+
                self.write_jsonl(out, journal)?;
+
            } else {
+
                self.write_pretty(out, journal)?;
            }
        }

@@ -143,22 +189,68 @@ impl Leaf for LogCmd {
}

struct JournalLines {
+
    kind: LogKind,
    data: Vec<u8>,
    next: usize,
}

impl JournalLines {
-
    fn from_file(filename: &Path) -> Result<Self, LogError> {
-
        let data = read(filename).map_err(|err| LogError::Read(filename.into(), err))?;
-
        Ok(Self { data, next: 0 })
+
    fn read_file(filename: &Path) -> Result<Vec<u8>, LogError> {
+
        read(filename).map_err(|err| LogError::Read(filename.into(), err))
    }

-
    fn from_stdin() -> Result<Self, LogError> {
+
    fn read_stdin() -> Result<Vec<u8>, LogError> {
        let mut data = vec![];
        std::io::stdin()
            .read_to_end(&mut data)
            .map_err(LogError::ReadStdin)?;
-
        Ok(Self { data, next: 0 })
+
        Ok(data)
+
    }
+

+
    fn journal_from_file(filename: &Path) -> Result<Self, LogError> {
+
        let data = Self::read_file(filename)?;
+
        Ok(Self {
+
            data,
+
            next: 0,
+
            kind: LogKind::Journald,
+
        })
+
    }
+

+
    fn journal_from_stdin() -> Result<Self, LogError> {
+
        let data = Self::read_stdin()?;
+
        Ok(Self {
+
            data,
+
            next: 0,
+
            kind: LogKind::Journald,
+
        })
+
    }
+

+
    fn parse_journal_line(line: String) -> Result<Value, LogError> {
+
        let jj: JournalJson = serde_json::from_str(&line)
+
            .map_err(|err| LogError::JsonParseJournal(line.clone(), err))?;
+
        serde_json::from_str(&jj.message).map_err(|err| (LogError::JsonParse(line, err)))
+
    }
+

+
    fn cib_from_file(filename: &Path) -> Result<Self, LogError> {
+
        let data = Self::read_file(filename)?;
+
        Ok(Self {
+
            data,
+
            next: 0,
+
            kind: LogKind::Cib,
+
        })
+
    }
+

+
    fn cib_from_stdin() -> Result<Self, LogError> {
+
        let data = Self::read_stdin()?;
+
        Ok(Self {
+
            data,
+
            next: 0,
+
            kind: LogKind::Cib,
+
        })
+
    }
+

+
    fn parse_cib_line(line: String) -> Result<Value, LogError> {
+
        serde_json::from_str(&line).map_err(|err| LogError::JsonParseCib(line.clone(), err))
    }
}

@@ -172,12 +264,9 @@ impl Iterator for JournalLines {
                let line = String::from_utf8_lossy(&self.data[self.next..i]).to_string();
                self.next = i + 1;

-
                match serde_json::from_str::<JournalJson>(&line) {
-
                    Err(err) => return Some(Err(LogError::JsonParseJournal(line, err))),
-
                    Ok(jj) => match serde_json::from_str(&jj.message) {
-
                        Err(err) => return Some(Err(LogError::JsonParse(line, err))),
-
                        Ok(value) => return Some(Ok(value)),
-
                    },
+
                match self.kind {
+
                    LogKind::Cib => return Some(Self::parse_cib_line(line)),
+
                    LogKind::Journald => return Some(Self::parse_journal_line(line)),
                }
            }
            i += 1;
@@ -186,12 +275,25 @@ impl Iterator for JournalLines {
    }
}

+
#[derive(Debug, Copy, Clone, ValueEnum)]
+
enum LogKind {
+
    Journald,
+
    Cib,
+
}
+

#[derive(Debug, Deserialize)]
struct JournalJson {
    #[serde(rename = "MESSAGE")]
    message: String,
}

+
#[derive(Debug, Deserialize)]
+
#[allow(dead_code)]
+
struct CibJson {
+
    name: String,
+
    value: Value,
+
}
+

#[derive(Debug, thiserror::Error)]
pub enum LogError {
    #[error("failed to read journal file {0}")]
@@ -206,12 +308,15 @@ pub enum LogError {
    #[error("failed to parse a log line as JSON: {0:?}")]
    JsonParse(String, #[source] serde_json::Error),

+
    #[error("failed to parse a cib log line as JSON: {0:?}")]
+
    JsonParseCib(String, #[source] serde_json::Error),
+

    #[error("failed to serialize a log line as pretty JSON: {0:?}")]
    JsonSer(serde_json::Value, #[source] serde_json::Error),

    #[error("failed to create output file {0}")]
    CreateOutput(PathBuf, #[source] std::io::Error),

-
    #[error("failed to write to output file {0}")]
-
    WriteOutput(PathBuf, #[source] std::io::Error),
+
    #[error("failed to write to output")]
+
    Write(#[source] std::io::Error),
}