wasmer-edge-cli 0.1.4

User CLI for Wasmer Edge
Documentation
use comfy_table::Table;
use edge_schema::pretty_duration::parse_timestamp_or_relative_time;
use futures::StreamExt;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
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.
    ///
    /// Format:
    /// * RFC 3339 (`2006-01-02T03:04:05-07:00`)
    /// * RFC 2822 (`Mon, 02 Jan 2006 03:04:05 MST`)
    /// * Simple date (`2022-11-11`)
    /// * Unix timestamp (`1136196245`)
    /// * Relative time (`10m` / `-1h`, `1d1h30s`)
    // TODO: should default to trailing logs once trailing is implemented.
    #[clap(long, value_parser = parse_timestamp_or_relative_time)]
    from: Option<OffsetDateTime>,

    /// The date of the latest log entry.
    ///
    /// Format:
    /// * RFC 3339 (`2006-01-02T03:04:05-07:00`)
    /// * RFC 2822 (`Mon, 02 Jan 2006 03:04:05 MST`)
    /// * Simple date (`2022-11-11`)
    /// * Unix timestamp (`1136196245`)
    /// * Relative time (`10m` / `1h`, `1d1h30s`)
    #[clap(long, value_parser = parse_timestamp_or_relative_time)]
    until: Option<OffsetDateTime>,

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

    #[clap(long, default_value = "false")]
    watch: bool,

    /// 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_stream = wasmer_api::backend::get_app_logs_paginated(
            &client,
            name.clone(),
            owner.to_string(),
            version.clone(),
            from,
            self.until,
            self.watch,
        )
        .await;

        let mut logs_stream = std::pin::pin!(logs_stream);

        let mut rem = self.max;

        while let Some(logs) = logs_stream.next().await {
            let logs = logs?;

            let limit = std::cmp::max(logs.len(), rem);

            let logs = &logs[..limit];
            if !logs.is_empty() {
                let rendered = self.fmt.format.render(logs);
                println!("{rendered}");

                rem -= limit;
            }

            if !self.watch && rem == 0 {
                break;
            }
        }

        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")
}

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