use crate::platform::{event_bus::EventBus, log_sink::LogSink};
use chrono::Utc;
use shared::Event;
use std::collections::VecDeque;
use std::sync::Mutex;
use tokio::io::{AsyncBufReadExt, BufReader};
pub(super) const COMPOSE_TAIL_MAX: usize = 200;
pub(super) fn split_service_prefix(line: &str) -> Option<(&str, &str)> {
let (prefix, rest) = line.split_once('|')?;
let prefix = prefix.trim();
let (name, replica) = prefix.rsplit_once('-')?;
if replica.is_empty() || !replica.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
Some((name, rest.trim_start_matches(' ')))
}
pub(super) fn filter_main_service_line<'a>(line: &'a str, main_service: &str) -> Option<&'a str> {
match split_service_prefix(line)? {
(name, rest) if name == main_service => Some(rest),
_ => None,
}
}
pub(super) fn clean_compose_line(line: &str) -> Option<String> {
let line = line.rsplit('\r').next().unwrap_or(line);
let mut out = String::with_capacity(line.len());
let mut chars = line.chars();
while let Some(c) = chars.next() {
if c != '\u{1b}' {
out.push(c);
continue;
}
if chars.next() == Some('[') {
for c in chars.by_ref() {
if ('\u{40}'..='\u{7e}').contains(&c) {
break;
}
}
}
}
let out = out.trim_end();
(!out.trim().is_empty()).then(|| out.to_string())
}
pub(super) async fn record_job_line(
bus: &EventBus,
db: &impl LogSink,
job_id: &str,
job_run_id: &str,
mirror: Option<&(String, String)>,
stream: shared::protocol::LogStream,
line: String,
) {
let ts = Utc::now();
bus.publish(Event::JobLogLine {
job_run_id: job_run_id.to_string(),
job_id: job_id.to_string(),
line: line.clone(),
timestamp: ts,
stream: stream.clone(),
});
let _ = db.append_job_line(job_run_id, &stream, &line, ts).await;
if let Some((deployment_id, service_id)) = mirror {
let _ = db.append_build_line(deployment_id, &line, ts).await;
bus.publish(Event::BuildLog {
deployment_id: deployment_id.clone(),
service_id: service_id.clone(),
line,
timestamp: ts,
});
}
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn read_compose_output<R: tokio::io::AsyncRead + Unpin>(
reader: R,
stream: shared::protocol::LogStream,
main_service: &str,
bus: &EventBus,
db: &impl LogSink,
job_id: &str,
job_run_id: &str,
mirror: Option<&(String, String)>,
compose_tail: &Mutex<VecDeque<String>>,
) {
let mut lines = BufReader::new(reader).lines();
while let Ok(Some(raw_line)) = lines.next_line().await {
if let Some(line) = filter_main_service_line(&raw_line, main_service) {
if !line.trim().is_empty() {
let line = line.to_string();
record_job_line(bus, db, job_id, job_run_id, mirror, stream.clone(), line).await;
}
continue;
}
if split_service_prefix(&raw_line).is_some() {
continue;
}
if let Some(line) = clean_compose_line(&raw_line) {
let mut tail = compose_tail.lock().unwrap_or_else(|e| e.into_inner());
if tail.len() == COMPOSE_TAIL_MAX {
tail.pop_front();
}
tail.push_back(line);
}
}
}