diff --git a/cli/src/clients/datafusion_helpers/mod.rs b/cli/src/clients/datafusion_helpers/mod.rs index 5e3b098290..69b87adc11 100644 --- a/cli/src/clients/datafusion_helpers/mod.rs +++ b/cli/src/clients/datafusion_helpers/mod.rs @@ -15,8 +15,8 @@ use chrono::{DateTime, Duration, Local}; use clap::ValueEnum; use restate_types::journal_v2::Entry; use restate_types::{identifiers::AwakeableIdentifier, invocation::ServiceType}; -use serde::Deserialize; -use serde_with::{DeserializeAs, serde_as}; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeAs, SerializeAs, serde_as}; mod v2; @@ -36,7 +36,16 @@ pub struct SimpleInvocation { } #[derive( - ValueEnum, Copy, Clone, Eq, Hash, PartialEq, Debug, Default, serde_with::DeserializeFromStr, + ValueEnum, + Copy, + Clone, + Eq, + Hash, + PartialEq, + Debug, + Default, + serde_with::DeserializeFromStr, + serde_with::SerializeDisplay, )] pub enum InvocationState { #[default] @@ -86,7 +95,7 @@ impl Display for InvocationState { } #[serde_as] -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Invocation { pub id: String, pub target: String, @@ -152,6 +161,15 @@ impl<'de> DeserializeAs<'de, ServiceType> for DatafusionServiceType { } } +impl SerializeAs for DatafusionServiceType { + fn serialize_as(source: &ServiceType, serializer: S) -> Result + where + S: serde::Serializer, + { + source.serialize(serializer) + } +} + impl FromStr for DatafusionServiceType { type Err = String; diff --git a/cli/src/commands/invocations/list.rs b/cli/src/commands/invocations/list.rs index 02796082a7..794fee63e5 100644 --- a/cli/src/commands/invocations/list.rs +++ b/cli/src/commands/invocations/list.rs @@ -18,10 +18,10 @@ use cling::prelude::*; use indicatif::ProgressBar; use itertools::Itertools; -use restate_cli_util::c_eprintln; -use restate_cli_util::ui::console::Styled; +use restate_cli_util::ui::console::{Styled, StyledTable}; use restate_cli_util::ui::stylesheet::Style; use restate_cli_util::ui::watcher::Watch; +use restate_cli_util::{CliContext, OutputFormat, c_eprintln, c_println}; use crate::cli_env::CliEnv; use crate::clients::datafusion_helpers::{InvocationState, find_and_count_active_invocations}; @@ -191,24 +191,69 @@ async fn list(env: &CliEnv, opts: &List) -> Result<()> { find_and_count_active_invocations(&sql_client, &active_filter_str, order_by, opts.limit) .await?; - // Render Output UI progress.finish_and_clear(); - // Sample of active invocations - if !results.is_empty() { - // Truncate the output to fit the requested limit - results.truncate(opts.limit); - for inv in &results { - render_invocation_compact(inv); + match CliContext::get().output_format() { + OutputFormat::Human => { + // Sample of active invocations + if !results.is_empty() { + // Truncate the output to fit the requested limit + results.truncate(opts.limit); + for inv in &results { + render_invocation_compact(inv); + } + } + c_eprintln!( + "Showing {}/{} invocations. Query took {:?}", + results.len(), + count_estimate, + Styled(Style::Notice, start_time.elapsed()) + ); + } + OutputFormat::Table => { + if !results.is_empty() { + results.truncate(opts.limit); + let mut table = comfy_table::Table::new_styled(); + table.set_styled_header(vec!["ID", "TARGET", "STATUS", "DEPLOYMENT", "CREATED AT"]); + for inv in &results { + let deployment = inv + .pinned_deployment_id + .as_deref() + .or(inv.last_attempt_deployment_id.as_deref()) + .unwrap_or("-"); + table.add_row(vec![ + inv.id.as_str(), + inv.target.as_str(), + &inv.status.to_string(), + deployment, + &inv.created_at.to_rfc3339(), + ]); + } + c_println!("{}", table); + } + c_eprintln!( + "Showing {}/{} invocations. Query took {:?}", + results.len(), + count_estimate, + Styled(Style::Notice, start_time.elapsed()) + ); + } + OutputFormat::Json => { + results.truncate(opts.limit); + serde_json::to_writer(std::io::stdout(), &results)?; + println!(); + } + OutputFormat::Jsonl => { + results.truncate(opts.limit); + let stdout = std::io::stdout(); + let mut out = stdout.lock(); + for inv in &results { + serde_json::to_writer(&mut out, inv)?; + use std::io::Write; + writeln!(out)?; + } } } - c_eprintln!( - "Showing {}/{} invocations. Query took {:?}", - results.len(), - count_estimate, - Styled(Style::Notice, start_time.elapsed()) - ); - Ok(()) } diff --git a/crates/cli-util/README.md b/crates/cli-util/README.md index 9a2be304f2..8f7f57cd52 100644 --- a/crates/cli-util/README.md +++ b/crates/cli-util/README.md @@ -280,5 +280,6 @@ These options are available to all commands via `CommonOpts`: | `-y`, `--yes` | Auto-confirm prompts | | `--table-style` | `compact` (default) or `borders` | | `--time-format` | `human` (default), `iso8601`, or `rfc2822` | +| `--output` | `human` (default), `table`, `json`, or `jsonl` | | `--connect-timeout` | Connection timeout in ms | | `--request-timeout` | Request timeout in ms | diff --git a/crates/cli-util/src/context.rs b/crates/cli-util/src/context.rs index d150c04b11..6cf56f239d 100644 --- a/crates/cli-util/src/context.rs +++ b/crates/cli-util/src/context.rs @@ -51,7 +51,9 @@ use dotenvy::dotenv; use tracing::{info, warn}; use tracing_log::AsTrace; -use crate::opts::{CommonOpts, ConfirmMode, NetworkOpts, TableStyle, TimeFormat, UiOpts}; +use crate::opts::{ + CommonOpts, ConfirmMode, NetworkOpts, OutputFormat, TableStyle, TimeFormat, UiOpts, +}; use crate::os_env::OsEnv; static GLOBAL_CLI_CONTEXT: OnceLock> = OnceLock::new(); @@ -229,6 +231,11 @@ impl CliContext { self.ui.time_format } + /// Get the user's preferred output format for list and describe commands. + pub fn output_format(&self) -> OutputFormat { + self.ui.output + } + /// Whether colors and styling should be used in output. /// /// This is determined by color detection at context creation time. diff --git a/crates/cli-util/src/lib.rs b/crates/cli-util/src/lib.rs index 108554b954..a3f4ce21b2 100644 --- a/crates/cli-util/src/lib.rs +++ b/crates/cli-util/src/lib.rs @@ -65,7 +65,7 @@ mod os_env; pub mod ui; pub use context::CliContext; -pub use opts::CommonOpts; +pub use opts::{CommonOpts, OutputFormat}; pub use os_env::OsEnv; // Re-export comfy-table for console c_* macros (used internally by macros) diff --git a/crates/cli-util/src/opts.rs b/crates/cli-util/src/opts.rs index f9bf900acd..667682b81f 100644 --- a/crates/cli-util/src/opts.rs +++ b/crates/cli-util/src/opts.rs @@ -44,6 +44,20 @@ pub enum TimeFormat { Rfc2822, } +#[derive(ValueEnum, Clone, Copy, Eq, PartialEq, Default, Debug)] +#[clap(rename_all = "kebab-case")] +pub enum OutputFormat { + /// Human-friendly output with colors and icons (default) + #[default] + Human, + /// Plain table suitable for shell pipelines + Table, + /// JSON array + Json, + /// Newline-delimited JSON + Jsonl, +} + /// Silent (no) logging by default in CLI #[derive(Clone, Default)] pub(crate) struct Quiet; @@ -73,6 +87,10 @@ pub(crate) struct UiOpts { #[arg(long, default_value = "human", global = true)] pub time_format: TimeFormat, + + /// Output format for list and describe commands + #[arg(long, default_value = "human", global = true)] + pub output: OutputFormat, } #[derive(Args, Clone, Default)] diff --git a/release-notes/unreleased/3872-cli-output-flag.md b/release-notes/unreleased/3872-cli-output-flag.md new file mode 100644 index 0000000000..b23fb1f576 --- /dev/null +++ b/release-notes/unreleased/3872-cli-output-flag.md @@ -0,0 +1,33 @@ +# Release Notes for Issue #3872: Universal --output flag for list commands + +## New Feature + +### What Changed + +`restate inv list` (and `restate inv ls`) now supports a `--output` flag for +machine-readable output, making it easy to pipe invocation IDs into other commands. + +``` +--output human (default) | table | json | jsonl +``` + +- **`human`** — existing pretty output with colors and icons (unchanged default) +- **`table`** — plain columns: ID, TARGET, STATUS, DEPLOYMENT, CREATED AT +- **`json`** — JSON array of all results +- **`jsonl`** — one JSON object per line (newline-delimited JSON) + +### Examples + +```bash +# Cancel all backing-off invocations +restate inv list --status backing-off --output jsonl | jq -r .id | xargs restate inv cancel + +# Pipe into grep then cancel +restate inv list --output table | grep MyService | awk '{print $1}' | xargs restate inv cancel + +# Pretty-print full JSON +restate inv list --all --output json | jq . +``` + +### Related Issues +- Issue #3872: Universal --output option for all get/list/describe commands