fundaia 0.10.0

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;
use crate::api::models::LogLine;
use crate::output::{self, Format};
use crate::render::logs::{render_line, LineFormat};
use crate::render::theme;

/// A service's log, from whichever deployment is asked for.
///
/// Scoped to a deployment rather than to a service, because that is what the
/// platform stores: the runtime output of one container, plus everything its
/// build printed.
///
/// The *newest* by default, not the active one. Asking for the active one meant
/// a service that was asleep or had failed printed "no deployment to read
/// from" — the two states where somebody is most likely to want the log, and a
/// failed deployment's log is the whole reason to ask for it.
/// Everything the flags decide: which deployment, how much of it, how it looks.
///
/// Grouped rather than passed one by one, because the alternative reads
/// `run(&client, &p, &s, false, 200, None, None, true)` at the call site and
/// only the compiler can tell which `true` is which.
pub struct LogQuery<'a> {
    pub follow: bool,
    pub tail: usize,
    pub stream_filter: Option<&'a str>,
    /// One of the six deployment steps, filtered by the server rather than here.
    ///
    /// Unlike the stream filter, which is a client-side `filter` over what
    /// arrived: a build is several thousand lines, and asking for all of them to
    /// print the twenty the checkout printed is a download to throw away.
    pub stage: Option<&'a str>,
    pub number: Option<u32>,
    pub timestamps: bool,
}

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);

    let project = client.find_project(project_reference).await?;
    let service = client.find_service(&project.id, 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.cloned() 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);

    // One JSON object per line, following or not, because a log *is* a stream
    // of records — this is the shape `jq` and an agent's own loop both read.
    let print = |line: &LogLine| {
        if output_format.is_json() {
            output::emit(line);
        } else {
            println!("{}", render_line(line, format));
        }
    };

    if !query.follow {
        let lines = client.read_log(&deployment.id, 0, query.stage).await?;
        // Tailed here rather than server-side: the endpoint pages forward from a
        // sequence, so "the last N" is a question only the client can answer
        // without a second round trip to find out how many there are.
        let start = lines.len().saturating_sub(query.tail);

        for line in lines
            .iter()
            .skip(start)
            .filter(|line| matches(&line.stream))
        {
            print(line);
        }
        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}")),
        );
    }

    client
        .stream_log(&deployment.id, 0, query.stage, |line| {
            if matches(&line.stream) {
                print(&line);
            }
        })
        .await
}