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,
};
#[derive(clap::Parser, Debug)]
pub struct CmdAppLogs {
#[clap(flatten)]
api: ApiOpts,
#[clap(flatten)]
fmt: ListFormatOpts,
#[clap(long, value_parser = parse_timestamp)]
from: Option<OffsetDateTime>,
#[clap(long, value_parser = parse_timestamp)]
until: Option<OffsetDateTime>,
#[clap(long, default_value = "1000")]
max: usize,
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")
}
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);
}
}