feat(logs): implement streaming logs with follow option

This commit is contained in:
hdbg
2025-12-06 20:22:43 +01:00
parent 676c53fabb
commit c6929255e3
4 changed files with 71 additions and 48 deletions

View File

@@ -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")]

View File

@@ -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

View File

@@ -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<Item = Result<LogOutput>> {
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
}
}

View File

@@ -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 } => {