From c6929255e3725a43bc56a07f4129b5bff24e0b9d Mon Sep 17 00:00:00 2001 From: hdbg Date: Sat, 6 Dec 2025 20:22:43 +0100 Subject: [PATCH] feat(logs): implement streaming logs with follow option --- src/cli.rs | 5 ++- src/controller.rs | 79 ++++++++++++++++++++++++++-------------- src/controller/docker.rs | 30 ++++++--------- src/main.rs | 5 ++- 4 files changed, 71 insertions(+), 48 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 87e54ff..c202817 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -43,7 +43,10 @@ pub enum ControlCommands { /// Status of instance Status, /// View logs produced by postgres - Logs { follow: bool }, + Logs { + #[arg(short, long, default_value = "false")] + follow: bool, + }, /// (Sensitive) get connection details Connection { #[arg(short, long, default_value = "dsn")] diff --git a/src/controller.rs b/src/controller.rs index 4a365f5..8e6f002 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -3,6 +3,7 @@ use miette::miette; use colored::Colorize; use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL}; +use futures::TryStreamExt; use miette::Result; use crate::{ @@ -65,6 +66,24 @@ impl Controller { Self { ctx } } + pub async fn logs(&self, follow: bool) -> Result<()> { + let instance = self.ctx.require_instance()?; + + let mut logs = self + .ctx + .docker + .stream_logs(&instance.container_id, follow) + .await; + + while let Some(log) = logs.try_next().await? { + let bytes = log.into_bytes(); + let line = String::from_utf8_lossy(bytes.as_ref()); + print!("{line}"); + } + + Ok(()) + } + pub async fn show_connection(&self, format: ConnectionFormat) -> Result<()> { let project = self.ctx.require_project()?; let reconciler = Reconciler { ctx: &self.ctx }; @@ -84,34 +103,7 @@ impl Controller { println!("{}", dsn.to_string()); } ConnectionFormat::Human => { - let mut table = create_ui_table("Instance"); - table.add_row(vec![ - Cell::new("Project").fg(Color::White), - Cell::new(&project.name).add_attribute(Attribute::Bold), - ]); - table.add_row(vec![ - Cell::new("PostgreSQL Version").fg(Color::White), - Cell::new(project.config.version.to_string()).add_attribute(Attribute::Bold), - ]); - table.add_row(vec![ - Cell::new("Host").fg(Color::White), - Cell::new("127.0.0.1").add_attribute(Attribute::Bold), - ]); - - table.add_row(vec![ - Cell::new("Port").fg(Color::White), - Cell::new(project.config.port.to_string()).add_attribute(Attribute::Bold), - ]); - table.add_row(vec![ - Cell::new("Username").fg(Color::White), - Cell::new(USERNAME).add_attribute(Attribute::Bold), - ]); - - table.add_row(vec![ - Cell::new("Password").fg(Color::White), - Cell::new(project.config.password.clone()).fg(Color::DarkGrey), - ]); - println!("{}", table); + format_conn_human(project); } } @@ -174,6 +166,37 @@ impl Controller { } } +fn format_conn_human(project: &Project) { + let mut table = create_ui_table("Instance"); + table.add_row(vec![ + Cell::new("Project").fg(Color::White), + Cell::new(&project.name).add_attribute(Attribute::Bold), + ]); + table.add_row(vec![ + Cell::new("PostgreSQL Version").fg(Color::White), + Cell::new(project.config.version.to_string()).add_attribute(Attribute::Bold), + ]); + table.add_row(vec![ + Cell::new("Host").fg(Color::White), + Cell::new("127.0.0.1").add_attribute(Attribute::Bold), + ]); + + table.add_row(vec![ + Cell::new("Port").fg(Color::White), + Cell::new(project.config.port.to_string()).add_attribute(Attribute::Bold), + ]); + table.add_row(vec![ + Cell::new("Username").fg(Color::White), + Cell::new(USERNAME).add_attribute(Attribute::Bold), + ]); + + table.add_row(vec![ + Cell::new("Password").fg(Color::White), + Cell::new(project.config.password.clone()).fg(Color::DarkGrey), + ]); + println!("{}", table); +} + fn create_ui_table(header: &'static str) -> Table { let mut table = Table::new(); table diff --git a/src/controller/docker.rs b/src/controller/docker.rs index d9b57f4..59af0c0 100644 --- a/src/controller/docker.rs +++ b/src/controller/docker.rs @@ -4,6 +4,7 @@ use thiserror::Error; use bollard::{ Docker, + container::LogOutput, query_parameters::{ CreateContainerOptions, CreateImageOptions, InspectContainerOptions, ListImagesOptions, LogsOptions, StartContainerOptions, StopContainerOptions, @@ -11,7 +12,7 @@ use bollard::{ secret::ContainerCreateBody, }; use colored::Colorize; -use futures::StreamExt; +use futures::{Stream, StreamExt}; use indicatif::MultiProgress; use miette::{Context, IntoDiagnostic, Result}; use tracing::info; @@ -270,7 +271,11 @@ impl DockerController { .map_err(|_| miette!("Invalid version in label: {}", version_str)) } - pub async fn stream_logs(&self, container_id: &str, follow: bool) -> Result<()> { + pub async fn stream_logs( + &self, + container_id: &str, + follow: bool, + ) -> impl Stream> { let options = Some(LogsOptions { follow, stdout: true, @@ -278,22 +283,11 @@ impl DockerController { ..Default::default() }); - let mut logs = self.daemon.logs(container_id, options); + let logs = self + .daemon + .logs(container_id, options) + .map(|k| k.into_diagnostic().wrap_err("Failed streaming logs")); - while let Some(entry) = logs.next().await { - match entry { - Ok(output) => { - print!("{output}"); - std::io::stdout().flush().ok(); - } - Err(err) => { - return Err(err) - .into_diagnostic() - .wrap_err("Failed to stream container logs"); - } - } - } - - Ok(()) + logs } } diff --git a/src/main.rs b/src/main.rs index 0ebcfb6..dbd8e2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,10 @@ async fn main() -> Result<()> { ControlCommands::Stop => {} ControlCommands::Restart => {} ControlCommands::Destroy { accept } => {} - ControlCommands::Logs { follow } => todo!(), + ControlCommands::Logs { follow } => { + let ctx = Context::new(name).await?; + Controller::new(ctx).logs(follow).await?; + } ControlCommands::Status => {} // can't override an instance for this command, because password is in config ControlCommands::Connection { format } => {