use std::time::Duration;
use crate::client;
use crate::config::ProjectConfig;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Client(#[from] crate::client::ClientError),
#[error(transparent)]
Json(#[from] serde_json::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, clap::Args)]
pub struct LogsArgs {
#[arg(long, env = "BOATRAMP_SERVER", global = true)]
server: Option<String>,
#[arg(long, env = "BOATRAMP_SITE", global = true)]
site: Option<String>,
#[arg(long)]
function: Option<String>,
#[arg(long)]
stream: Option<String>,
#[arg(long, default_value_t = 200)]
limit: usize,
#[arg(long, short)]
follow: bool,
}
enum Target {
Site(String),
Function(String),
}
async fn fetch(
cp: &client::ControlPlane,
target: &Target,
limit: usize,
after: u64,
stream: Option<&str>,
) -> Result<client::LogsResponse> {
Ok(match target {
Target::Site(site) => cp.fetch_logs(site, limit, after, stream).await?,
Target::Function(function) => {
cp.fetch_function_logs(function, limit, after, stream)
.await?
}
})
}
#[derive(Debug, serde::Serialize)]
#[serde(transparent)]
pub struct LogsOutput {
response: client::LogsResponse,
}
impl crate::output::Rendered for LogsOutput {
fn render_human(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> {
for entry in &self.response.entries {
writeln!(w, "[{}] {}", entry.stream, entry.line)?;
}
Ok(())
}
}
pub async fn run(args: LogsArgs, config: &ProjectConfig) -> Result<LogsOutput> {
let stream = args.stream.as_deref();
let http = client::http_client(client::token(config).as_deref());
let project = client::resolve_project(config);
let (cp, target) = if let Some(function) = args.function {
let server = client::resolve_server(args.server, config)?;
(
client::ControlPlane::new(server, http, project),
Target::Function(function),
)
} else {
let (server, site) = client::resolve_target(args.server, args.site, config)?;
(
client::ControlPlane::new(server, http, project),
Target::Site(site),
)
};
let first = fetch(&cp, &target, args.limit, 0, stream).await?;
if !args.follow {
if first.dropped > 0 {
tracing::warn!(
"(\u{2026} {} line(s) dropped by the rate cap)",
first.dropped
);
}
return Ok(LogsOutput { response: first });
}
let mut cursor = first.entries.last().map(|e| e.seq).unwrap_or(0);
for entry in &first.entries {
print_line(entry);
}
if first.dropped > 0 {
tracing::warn!(
"(\u{2026} {} line(s) dropped by the rate cap)",
first.dropped
);
}
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
let next = fetch(&cp, &target, args.limit.max(1000), cursor, stream).await?;
for entry in &next.entries {
print_line(entry);
cursor = cursor.max(entry.seq);
}
}
}
fn print_line(entry: &client::LogEntry) {
println!("[{}] {}", entry.stream, entry.line);
}
#[derive(Debug, clap::Args)]
pub struct StatsArgs {
#[arg(long, env = "BOATRAMP_SERVER", global = true)]
server: Option<String>,
#[arg(long, env = "BOATRAMP_SITE", global = true)]
site: Option<String>,
#[arg(long)]
instances: bool,
}
#[derive(Debug, serde::Serialize)]
#[serde(transparent)]
pub struct StatsOutput {
stats: serde_json::Value,
}
impl crate::output::Rendered for StatsOutput {
fn render_human(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> {
writeln!(
w,
"{}",
serde_json::to_string_pretty(&self.stats).map_err(std::io::Error::other)?
)
}
}
pub async fn stats(args: StatsArgs, config: &ProjectConfig) -> Result<StatsOutput> {
if args.instances {
let server = client::resolve_server(args.server, config)?;
let cp = client::ControlPlane::new(
server,
client::http_client(client::token(config).as_deref()),
client::resolve_project(config),
);
let stats = cp.fetch_instance_stats().await?;
return Ok(StatsOutput { stats });
}
let (server, site) = client::resolve_target(args.server, args.site, config)?;
let cp = client::ControlPlane::new(
server,
client::http_client(client::token(config).as_deref()),
client::resolve_project(config),
);
let stats = cp.fetch_handler_stats(&site).await?;
Ok(StatsOutput { stats })
}