use crate::api::func;
use crate::error::Error;
use crate::function::Function;
use crate::runner::{Runnable, Runner};
use crate::writer::Writer;
use chrono::{DateTime, Utc};
use eyre::Context;
use kinetics_parser::Parser;
use serde_json::json;
#[derive(clap::Args, Clone)]
pub(crate) struct LogsCommand {
#[arg()]
name: String,
#[arg(short, long)]
period: Option<String>,
}
impl Runnable for LogsCommand {
fn runner(&self, writer: &Writer) -> impl Runner {
LogsRunner {
command: self.clone(),
writer,
}
}
}
struct LogsRunner<'a> {
command: LogsCommand,
writer: &'a Writer,
}
impl Runner for LogsRunner<'_> {
async fn run(&mut self) -> Result<(), Error> {
let project = self.project().await?;
let all_functions = Parser::new(Some(&project.path))
.map_err(|e| self.error(None, None, Some(e.into())))?
.functions
.into_iter()
.map(|f| Function::new(&project, &f))
.collect::<eyre::Result<Vec<Function>>>()
.map_err(|e| self.error(None, None, Some(e.into())))?;
let function = Function::find_by_name(&all_functions, &self.command.name).map_err(|e| {
self.error(
Some("Could not find requested function"),
None,
Some(e.into()),
)
})?;
let client = self.api_client().await?;
self.writer.text(&format!(
"\n{} {} {}...\n\n",
console::style("Fetching logs").bold().green(),
console::style("for").dim(),
console::style(&function.name).bold()
))?;
let response = client
.post("/function/logs")
.json(&func::logs::Request {
project_name: project.name.clone(),
function_name: function.name.clone(),
period: self.command.period.to_owned(),
})
.send()
.await
.wrap_err("Failed to send request to logs endpoint")
.map_err(|e| self.server_error(Some(e.into())))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or("Unknown error".to_string());
log::error!("Failed to fetch logs from API ({}): {}", status, error_text);
return Err(self.server_error(None));
}
let logs_response: func::logs::Response = response
.json()
.await
.wrap_err("Invalid response from server")
.map_err(|e| self.error(None, None, Some(e.into())))?;
if logs_response.events.is_empty() {
self.writer.text(&format!(
"{}\n",
console::style(format!(
"No logs found for this function in the last {}.",
self.command.period.clone().unwrap_or("1 hour".into())
))
.yellow(),
))?;
self.writer.json(json!({"success": true, "logs": []}))?;
return Ok(());
}
let mut events_json: Vec<String> = vec![];
for event in logs_response.events {
let datetime = match DateTime::<Utc>::from_timestamp_millis(event.timestamp) {
Some(dt) => dt,
None => {
log::warn!("Invalid timestamp: {}", event.timestamp);
continue;
}
};
let formatted_time = datetime.format("%Y-%m-%d %H:%M:%S").to_string();
let line = format!("{} {}", console::style(formatted_time).dim(), event.message);
self.writer.text(&line)?;
events_json.push(line);
}
self.writer
.json(json!({"success": true, "logs": events_json}))?;
Ok(())
}
}