use bytesize::ByteSize;
use comfy_table::{Cell, Table};
use time::{Duration, OffsetDateTime};
use super::{AppIdentOpts, WasmerEnv};
use crate::{
commands::AsyncCliCommand,
opts::ItemFormatOpts,
utils::render::{CliRender, ListFormat},
};
#[derive(clap::Parser, Debug)]
pub struct CmdAppCdnStatus {
#[clap(flatten)]
pub env: WasmerEnv,
#[clap(flatten)]
pub fmt: ItemFormatOpts,
#[clap(flatten)]
pub ident: AppIdentOpts,
#[clap(long)]
pub with_metrics: bool,
}
#[async_trait::async_trait]
impl AsyncCliCommand for CmdAppCdnStatus {
type Output = ();
async fn run_async(self) -> Result<(), anyhow::Error> {
let client = self.env.client()?;
let (_ident, app) = self.ident.load_app(&client).await?;
let cdn_status = wasmer_backend_api::query::app_cdn_cache_status(
&client,
wasmer_backend_api::types::GetAppCdnCacheStatusVars {
app: app.id.clone(),
},
)
.await?;
let metrics = if self.with_metrics {
let end_at = OffsetDateTime::now_utc();
let start_at = end_at - Duration::days(30);
let metrics = wasmer_backend_api::query::app_cdn_cache_metrics(
&client,
wasmer_backend_api::types::GetAppCdnCacheMetricsVars {
app: app.id.clone(),
start_at: start_at.try_into()?,
end_at: end_at.try_into()?,
grouped_by: wasmer_backend_api::types::MetricGrouping::ByDay,
},
)
.await?;
Some(CdnCacheMetricsStatus::from(metrics))
} else {
None
};
let status = CdnCacheStatus {
app: app.name,
owner: app.owner.global_name,
enabled: cdn_status.cdn_cache_enabled,
last_purged_at: cdn_status.cdn_cache_purged_at.map(|dt| dt.0),
metrics,
};
println!("{}", self.fmt.get().render(&status));
Ok(())
}
}
#[derive(Debug, serde::Serialize)]
struct CdnCacheStatus {
app: String,
owner: String,
enabled: bool,
last_purged_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
metrics: Option<CdnCacheMetricsStatus>,
}
#[derive(Debug, serde::Serialize)]
struct CdnCacheMetricsStatus {
cached_requests: i64,
total_requests: i64,
cached_network_egress_bytes: i64,
total_network_egress_bytes: i64,
}
impl From<wasmer_backend_api::types::AppCdnCacheMetrics> for CdnCacheMetricsStatus {
fn from(value: wasmer_backend_api::types::AppCdnCacheMetrics) -> Self {
let requests = value.grouped_metrics.totals.requests;
Self {
cached_requests: requests.cached_requests.0,
total_requests: requests.total_requests.0,
cached_network_egress_bytes: requests.data_cached_bytes.0,
total_network_egress_bytes: requests.data_served_bytes.0,
}
}
}
impl CliRender for CdnCacheStatus {
fn render_item_table(&self) -> String {
let mut table = Table::new();
table.load_style(comfy_table::presets::NOTHING);
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
table.set_header(vec![
Cell::new("App"),
Cell::new("Owner"),
Cell::new("Status"),
Cell::new("Last purge"),
Cell::new("Cached requests"),
Cell::new("Cached egress"),
]);
let (cached_requests, cached_egress) = match &self.metrics {
Some(metrics) => (
format!("{} / {}", metrics.cached_requests, metrics.total_requests),
format!(
"{} / {}",
ByteSize(metrics.cached_network_egress_bytes as u64),
ByteSize(metrics.total_network_egress_bytes as u64)
),
),
None => ("-".to_string(), "-".to_string()),
};
table.add_row(vec![
Cell::new(&self.app),
Cell::new(&self.owner),
Cell::new(if self.enabled { "enabled" } else { "disabled" }),
Cell::new(self.last_purged_at.as_deref().unwrap_or("-")),
Cell::new(cached_requests),
Cell::new(cached_egress),
]);
table.to_string()
}
fn render_list(_items: &[Self], _format: ListFormat) -> String {
unreachable!("CDN cache status is rendered as a single item")
}
fn render_list_table(items: &[Self]) -> String {
items
.iter()
.map(Self::render_item_table)
.collect::<Vec<_>>()
.join("\n\n")
}
}