feat(logs): implement streaming logs with follow option
This commit is contained in:
@@ -43,7 +43,10 @@ pub enum ControlCommands {
|
|||||||
/// Status of instance
|
/// Status of instance
|
||||||
Status,
|
Status,
|
||||||
/// View logs produced by postgres
|
/// View logs produced by postgres
|
||||||
Logs { follow: bool },
|
Logs {
|
||||||
|
#[arg(short, long, default_value = "false")]
|
||||||
|
follow: bool,
|
||||||
|
},
|
||||||
/// (Sensitive) get connection details
|
/// (Sensitive) get connection details
|
||||||
Connection {
|
Connection {
|
||||||
#[arg(short, long, default_value = "dsn")]
|
#[arg(short, long, default_value = "dsn")]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use miette::miette;
|
|||||||
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
|
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
|
||||||
|
use futures::TryStreamExt;
|
||||||
use miette::Result;
|
use miette::Result;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -65,6 +66,24 @@ impl Controller {
|
|||||||
Self { ctx }
|
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<()> {
|
pub async fn show_connection(&self, format: ConnectionFormat) -> Result<()> {
|
||||||
let project = self.ctx.require_project()?;
|
let project = self.ctx.require_project()?;
|
||||||
let reconciler = Reconciler { ctx: &self.ctx };
|
let reconciler = Reconciler { ctx: &self.ctx };
|
||||||
@@ -84,34 +103,7 @@ impl Controller {
|
|||||||
println!("{}", dsn.to_string());
|
println!("{}", dsn.to_string());
|
||||||
}
|
}
|
||||||
ConnectionFormat::Human => {
|
ConnectionFormat::Human => {
|
||||||
let mut table = create_ui_table("Instance");
|
format_conn_human(project);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 {
|
fn create_ui_table(header: &'static str) -> Table {
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
table
|
table
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use thiserror::Error;
|
|||||||
|
|
||||||
use bollard::{
|
use bollard::{
|
||||||
Docker,
|
Docker,
|
||||||
|
container::LogOutput,
|
||||||
query_parameters::{
|
query_parameters::{
|
||||||
CreateContainerOptions, CreateImageOptions, InspectContainerOptions, ListImagesOptions,
|
CreateContainerOptions, CreateImageOptions, InspectContainerOptions, ListImagesOptions,
|
||||||
LogsOptions, StartContainerOptions, StopContainerOptions,
|
LogsOptions, StartContainerOptions, StopContainerOptions,
|
||||||
@@ -11,7 +12,7 @@ use bollard::{
|
|||||||
secret::ContainerCreateBody,
|
secret::ContainerCreateBody,
|
||||||
};
|
};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use futures::StreamExt;
|
use futures::{Stream, StreamExt};
|
||||||
use indicatif::MultiProgress;
|
use indicatif::MultiProgress;
|
||||||
use miette::{Context, IntoDiagnostic, Result};
|
use miette::{Context, IntoDiagnostic, Result};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -270,7 +271,11 @@ impl DockerController {
|
|||||||
.map_err(|_| miette!("Invalid version in label: {}", version_str))
|
.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 {
|
let options = Some(LogsOptions {
|
||||||
follow,
|
follow,
|
||||||
stdout: true,
|
stdout: true,
|
||||||
@@ -278,22 +283,11 @@ impl DockerController {
|
|||||||
..Default::default()
|
..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 {
|
logs
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ async fn main() -> Result<()> {
|
|||||||
ControlCommands::Stop => {}
|
ControlCommands::Stop => {}
|
||||||
ControlCommands::Restart => {}
|
ControlCommands::Restart => {}
|
||||||
ControlCommands::Destroy { accept } => {}
|
ControlCommands::Destroy { accept } => {}
|
||||||
ControlCommands::Logs { follow } => todo!(),
|
ControlCommands::Logs { follow } => {
|
||||||
|
let ctx = Context::new(name).await?;
|
||||||
|
Controller::new(ctx).logs(follow).await?;
|
||||||
|
}
|
||||||
ControlCommands::Status => {}
|
ControlCommands::Status => {}
|
||||||
// can't override an instance for this command, because password is in config
|
// can't override an instance for this command, because password is in config
|
||||||
ControlCommands::Connection { format } => {
|
ControlCommands::Connection { format } => {
|
||||||
|
|||||||
Reference in New Issue
Block a user