wasmer-deploy-cli 0.1.29

CLI for Wasmer Deploy
Documentation
use comfy_table::Table;
use time::{
    format_description::well_known::{Rfc2822, Rfc3339},
    Date, OffsetDateTime, PrimitiveDateTime, Time,
};
use wasmer_api::backend::gql::Log;

use crate::{
    util::{render::CliRender, Identifier},
    ApiOpts, ListFormatOpts,
};

/// Show an app.
#[derive(clap::Parser, Debug)]
pub struct CmdAppLogs {
    #[clap(flatten)]
    api: ApiOpts,
    #[clap(flatten)]
    fmt: ListFormatOpts,

    /// The date of the earliest log entry.
    ///
    /// Defaults to the last 10 minutes.
    // TODO: should default to trailing logs once trailing is implemented.
    #[clap(long, value_parser = parse_timestamp)]
    from: Option<OffsetDateTime>,

    /// The date of the latest log entry.
    #[clap(long, value_parser = parse_timestamp)]
    until: Option<OffsetDateTime>,

    /// Maximum log lines to fetch.
    /// Defaults to 1000.
    #[clap(long, default_value = "1000")]
    max: usize,

    /// The name of the app.
    ///
    /// Eg:
    /// - name (assumes current user)
    /// - namespace/name
    /// - namespace/name@version
    ident: Identifier,
}

impl CmdAppLogs {
    pub async fn run(self) -> Result<(), anyhow::Error> {
        let client = self.api.client()?;

        let Identifier {
            name,
            owner,
            version,
        } = &self.ident;

        let owner = match owner {
            Some(owner) => owner.to_string(),
            None => {
                let user = wasmer_api::backend::current_user_with_namespaces(&client, None).await?;
                user.username
            }
        };

        let from = self
            .from
            .unwrap_or_else(|| OffsetDateTime::now_utc() - time::Duration::minutes(10));

        tracing::info!(
            package.name=%self.ident.name,
            package.owner=%owner,
            package.version=self.ident.version.as_deref(),
            range.start=%from,
            range.end=self.until.map(|ts| ts.to_string()),
            "Fetching logs",
        );

        let logs: Vec<Log> = wasmer_api::backend::get_app_logs_paginated(
            &client,
            name.clone(),
            owner.to_string(),
            version.clone(),
            from,
            self.until,
            self.max,
        )
        .await?;

        let rendered = self.fmt.format.render(&logs);
        println!("{rendered}");

        Ok(())
    }
}

impl CliRender for Log {
    fn render_item_table(&self) -> String {
        let mut table = Table::new();

        let Log { message, timestamp } = self;

        table.add_rows([
            vec![
                "Timestamp".to_string(),
                datetime_from_unix(*timestamp).format(&Rfc3339).unwrap(),
            ],
            vec!["Message".to_string(), message.to_string()],
        ]);
        table.to_string()
    }

    fn render_list_table(items: &[Self]) -> String {
        let mut table = Table::new();
        table.set_header(vec!["Timestamp".to_string(), "Message".to_string()]);

        for item in items {
            table.add_row([
                datetime_from_unix(item.timestamp).format(&Rfc3339).unwrap(),
                item.message.clone(),
            ]);
        }
        table.to_string()
    }
}

fn datetime_from_unix(timestamp: f64) -> OffsetDateTime {
    OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)
        .expect("Timestamp should always be valid")
}

/// Try to parse the string as a timestamp in a number of well-known formats.
///
/// Supported formats,
///
/// - RFC 3339 (`2006-01-02T03:04:05-07:00`)
/// - RFC 2822 (`Mon, 02 Jan 2006 03:04:05 MST`)
/// - Date (`2006-01-02`)
/// - Unix timestamp (`1136196245`)
fn parse_timestamp(s: &str) -> Result<OffsetDateTime, anyhow::Error> {
    type Parser = fn(&str) -> Result<OffsetDateTime, anyhow::Error>;

    let parsers: &[Parser] = &[
        |s| OffsetDateTime::parse(s, &Rfc3339).map_err(anyhow::Error::from),
        |s| OffsetDateTime::parse(s, &Rfc2822).map_err(anyhow::Error::from),
        |s| {
            Date::parse(s, time::macros::format_description!("[year]-[month]-[day]"))
                .map(|date| PrimitiveDateTime::new(date, Time::MIDNIGHT).assume_utc())
                .map_err(anyhow::Error::from)
        },
        |s| {
            OffsetDateTime::parse(s, time::macros::format_description!("[unix_timestamp]"))
                .map_err(anyhow::Error::from)
        },
        |s| {
            let (is_negative, v) = match s.strip_prefix('-') {
                Some(rest) => (true, rest),
                None => (false, s),
            };

            let duration = v.parse::<wasmer_deploy_util::pretty_duration::PrettyDuration>()?;

            let now = OffsetDateTime::now_utc();
            let time = if is_negative {
                now - duration.0
            } else {
                now + duration.0
            };

            Ok(time)
        },
    ];

    for parse in parsers {
        match parse(s) {
            Ok(dt) => return Ok(dt),
            Err(e) => {
                tracing::debug!(error = &*e, "Parse failed");
            }
        }
    }

    anyhow::bail!("Unable to parse the timestamp - no known format matched")
}

impl crate::cmd::AsyncCliCommand for CmdAppLogs {
    fn run_async(self) -> futures::future::BoxFuture<'static, Result<(), anyhow::Error>> {
        Box::pin(self.run())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_common_timestamps() {
        let expected = time::macros::datetime!(2006-01-02 03:04:05 -07:00);

        assert_eq!(
            parse_timestamp("2006-01-02T03:04:05-07:00").unwrap(),
            expected,
        );
        assert_eq!(parse_timestamp("2006-01-02T10:04:05Z").unwrap(), expected);
        assert_eq!(
            parse_timestamp("2006-01-02T10:04:05.000000000Z").unwrap(),
            expected
        );
        assert_eq!(
            parse_timestamp("Mon, 02 Jan 2006 03:04:05 MST").unwrap(),
            expected,
        );
        assert_eq!(
            parse_timestamp("2006-01-02").unwrap(),
            time::macros::datetime!(2006-01-02 00:00:00 +00:00),
        );
        assert_eq!(parse_timestamp("1136196245").unwrap(), expected);
    }
}