use anyhow::Result;
use owo_colors::OwoColorize;
use crate::api::client::{ApiClient, LogFilter};
use crate::api::models::LogLine;
use crate::output::{self, Format};
use crate::render::logs::{render_line, LineFormat};
use crate::render::theme;
pub struct LogQuery<'a> {
pub follow: bool,
pub tail: usize,
pub stream_filter: Option<&'a str>,
pub stage: Option<&'a str>,
pub number: Option<u32>,
pub timestamps: bool,
pub trace: Option<&'a str>,
}
pub async fn run(
client: &ApiClient,
project_reference: &str,
service_reference: &str,
query: LogQuery<'_>,
output_format: Format,
) -> Result<()> {
let format = LineFormat::with_stream()
.showing_time(query.timestamps)
.showing_traces(query.trace.is_none());
let trace = query.trace.map(str::to_lowercase);
let (_, service) = client.locate(project_reference, service_reference).await?;
let deployments = client.deployments(&service.id).await?;
let chosen = match query.number {
Some(wanted) => deployments.iter().find(|one| one.number == wanted),
None => deployments.first(),
};
let Some(deployment) = chosen else {
if output_format.is_json() {
return Ok(());
}
match query.number {
Some(wanted) => println!("`{}` no tiene ningún despliegue #{wanted}.", service.name),
None => println!("`{}` no se ha desplegado todavía.", service.name),
}
return Ok(());
};
let matches = |candidate: &str| query.stream_filter.is_none_or(|wanted| candidate == wanted);
if !query.follow {
let lines = client
.read_log(
&deployment.id,
0,
LogFilter {
stage: query.stage,
trace: trace.as_deref(),
stream: query.stream_filter,
tail: Some(query.tail),
},
)
.await?;
let start = lines.len().saturating_sub(query.tail);
let mut out = output::Lines::lock();
for line in lines
.iter()
.skip(start)
.filter(|line| matches(&line.stream))
{
if output_format.is_json() {
out.json(line);
} else {
out.line(render_line(line, format));
}
}
out.finish();
return Ok(());
}
if !output_format.is_json() {
println!(
" {} {} #{}{} · Ctrl-C para salir",
theme::ARROW.style(theme::accent()),
service.name.style(theme::strong()),
deployment.number,
query
.stage
.map_or_else(String::new, |step| format!(" · {step}")),
);
}
let in_trace = |line: &LogLine| {
trace
.as_deref()
.is_none_or(|wanted| line.trace_id.as_deref() == Some(wanted))
};
let show = |line: &LogLine| {
if output_format.is_json() {
output::emit(line);
} else {
println!("{}", render_line(line, format));
}
};
let recent = client
.read_log(
&deployment.id,
0,
LogFilter {
stage: query.stage,
trace: trace.as_deref(),
stream: query.stream_filter,
tail: Some(query.tail.max(1)),
},
)
.await?;
for line in recent.iter().skip(recent.len().saturating_sub(query.tail)) {
show(line);
}
client
.stream_log(
&deployment.id,
resumed_after(&recent),
query.stage,
|line| {
if matches(&line.stream) && in_trace(&line) {
show(&line);
}
},
)
.await
}
fn resumed_after(shown: &[LogLine]) -> u64 {
shown.last().map_or(0, |line| line.sequence)
}
#[cfg(test)]
mod tests {
use super::*;
fn numbered(sequence: u64) -> LogLine {
LogLine {
sequence,
at: None,
stream: "runtime".into(),
level: "info".into(),
message: "listening".into(),
stage: None,
trace_id: None,
}
}
#[test]
fn it_should_follow_on_from_the_last_line_already_shown() {
assert_eq!(
resumed_after(&[numbered(40), numbered(41), numbered(42)]),
42
);
}
#[test]
fn it_should_follow_from_the_start_when_nothing_was_shown() {
assert_eq!(resumed_after(&[]), 0);
}
}