fundaia 0.55.3

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
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);
    }
}