Improve planoai up/down CLI progress output (#858)

This commit is contained in:
Musa 2026-03-31 17:26:32 -04:00 committed by GitHub
parent 82f34f82f2
commit 36fa42b364
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 263 additions and 158 deletions

View file

@ -3,6 +3,8 @@ import os
import multiprocessing import multiprocessing
import subprocess import subprocess
import sys import sys
import contextlib
import logging
import rich_click as click import rich_click as click
from planoai import targets from planoai import targets
@ -33,6 +35,7 @@ from planoai.consts import (
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT, DEFAULT_OTEL_TRACING_GRPC_ENDPOINT,
DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT, DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT,
NATIVE_PID_FILE, NATIVE_PID_FILE,
PLANO_RUN_DIR,
PLANO_DOCKER_IMAGE, PLANO_DOCKER_IMAGE,
PLANO_DOCKER_NAME, PLANO_DOCKER_NAME,
) )
@ -101,6 +104,20 @@ def _print_cli_header(console) -> None:
) )
@contextlib.contextmanager
def _temporary_cli_log_level(level: str | None):
if level is None:
yield
return
current_level = logging.getLevelName(logging.getLogger().level).lower()
set_log_level(level)
try:
yield
finally:
set_log_level(current_level)
def _print_missing_keys(console, missing_keys: list[str]) -> None: def _print_missing_keys(console, missing_keys: list[str]) -> None:
console.print(f"\n[red]✗[/red] [red]Missing API keys![/red]\n") console.print(f"\n[red]✗[/red] [red]Missing API keys![/red]\n")
for key in missing_keys: for key in missing_keys:
@ -293,13 +310,21 @@ def build(docker):
help="Run Plano inside Docker instead of natively.", help="Run Plano inside Docker instead of natively.",
is_flag=True, is_flag=True,
) )
def up(file, path, foreground, with_tracing, tracing_port, docker): @click.option(
"--verbose",
"-v",
default=False,
help="Show detailed startup logs with timestamps.",
is_flag=True,
)
def up(file, path, foreground, with_tracing, tracing_port, docker, verbose):
"""Starts Plano.""" """Starts Plano."""
from rich.status import Status from rich.status import Status
console = _console() console = _console()
_print_cli_header(console) _print_cli_header(console)
with _temporary_cli_log_level("warning" if not verbose else None):
# Use the utility function to find config file # Use the utility function to find config file
plano_config_file = find_config_file(path, file) plano_config_file = find_config_file(path, file)
@ -362,12 +387,16 @@ def up(file, path, foreground, with_tracing, tracing_port, docker):
# Check access keys # Check access keys
access_keys = get_llm_provider_access_keys(plano_config_file=plano_config_file) access_keys = get_llm_provider_access_keys(plano_config_file=plano_config_file)
access_keys = set(access_keys) access_keys = set(access_keys)
access_keys = [item[1:] if item.startswith("$") else item for item in access_keys] access_keys = [
item[1:] if item.startswith("$") else item for item in access_keys
]
missing_keys = [] missing_keys = []
if access_keys: if access_keys:
if file: if file:
app_env_file = os.path.join(os.path.dirname(os.path.abspath(file)), ".env") app_env_file = os.path.join(
os.path.dirname(os.path.abspath(file)), ".env"
)
else: else:
app_env_file = os.path.abspath(os.path.join(path, ".env")) app_env_file = os.path.abspath(os.path.join(path, ".env"))
@ -403,7 +432,9 @@ def up(file, path, foreground, with_tracing, tracing_port, docker):
) )
else: else:
try: try:
trace_server = start_trace_listener_background(grpc_port=tracing_port) trace_server = start_trace_listener_background(
grpc_port=tracing_port
)
console.print( console.print(
f"[green]✓[/green] Trace collector listening on [cyan]0.0.0.0:{tracing_port}[/cyan]" f"[green]✓[/green] Trace collector listening on [cyan]0.0.0.0:{tracing_port}[/cyan]"
) )
@ -431,8 +462,35 @@ def up(file, path, foreground, with_tracing, tracing_port, docker):
if not docker: if not docker:
from planoai.native_runner import start_native from planoai.native_runner import start_native
if not verbose:
with Status(
f"[{PLANO_COLOR}]Starting Plano...[/{PLANO_COLOR}]",
spinner="dots",
) as status:
def _update_progress(message: str) -> None:
console.print(f"[dim]{message}[/dim]")
start_native( start_native(
plano_config_file, env, foreground=foreground, with_tracing=with_tracing plano_config_file,
env,
foreground=foreground,
with_tracing=with_tracing,
progress_callback=_update_progress,
)
console.print("")
console.print(
"[green]✓[/green] Plano is running! [dim](native mode)[/dim]"
)
log_dir = os.path.join(PLANO_RUN_DIR, "logs")
console.print(f"Logs: {log_dir}")
console.print("Run 'planoai down' to stop.")
else:
start_native(
plano_config_file,
env,
foreground=foreground,
with_tracing=with_tracing,
) )
else: else:
start_plano(plano_config_file, env, foreground=foreground) start_plano(plano_config_file, env, foreground=foreground)
@ -459,11 +517,19 @@ def up(file, path, foreground, with_tracing, tracing_port, docker):
help="Stop a Docker-based Plano instance.", help="Stop a Docker-based Plano instance.",
is_flag=True, is_flag=True,
) )
def down(docker): @click.option(
"--verbose",
"-v",
default=False,
help="Show detailed shutdown logs with timestamps.",
is_flag=True,
)
def down(docker, verbose):
"""Stops Plano.""" """Stops Plano."""
console = _console() console = _console()
_print_cli_header(console) _print_cli_header(console)
with _temporary_cli_log_level("warning" if not verbose else None):
if not docker: if not docker:
from planoai.native_runner import stop_native from planoai.native_runner import stop_native
@ -471,13 +537,26 @@ def down(docker):
f"[{PLANO_COLOR}]Shutting down Plano...[/{PLANO_COLOR}]", f"[{PLANO_COLOR}]Shutting down Plano...[/{PLANO_COLOR}]",
spinner="dots", spinner="dots",
): ):
stop_native() stopped = stop_native()
if not verbose:
if stopped:
console.print(
"[green]✓[/green] Plano stopped! [dim](native mode)[/dim]"
)
else:
console.print(
"[dim]No Plano instance was running (native mode)[/dim]"
)
else: else:
with console.status( with console.status(
f"[{PLANO_COLOR}]Shutting down Plano (Docker)...[/{PLANO_COLOR}]", f"[{PLANO_COLOR}]Shutting down Plano (Docker)...[/{PLANO_COLOR}]",
spinner="dots", spinner="dots",
): ):
stop_docker_container() stop_docker_container()
if not verbose:
console.print(
"[green]✓[/green] Plano stopped! [dim](docker mode)[/dim]"
)
@click.command() @click.command()

View file

@ -6,6 +6,7 @@ import signal
import subprocess import subprocess
import sys import sys
import time import time
from collections.abc import Callable
from planoai.consts import ( from planoai.consts import (
NATIVE_PID_FILE, NATIVE_PID_FILE,
@ -157,7 +158,13 @@ def render_native_config(plano_config_file, env, with_tracing=False):
return envoy_config_path, plano_config_rendered_path return envoy_config_path, plano_config_rendered_path
def start_native(plano_config_file, env, foreground=False, with_tracing=False): def start_native(
plano_config_file,
env,
foreground=False,
with_tracing=False,
progress_callback: Callable[[str], None] | None = None,
):
"""Start Envoy and brightstaff natively.""" """Start Envoy and brightstaff natively."""
from planoai.core import _get_gateway_ports from planoai.core import _get_gateway_ports
@ -174,6 +181,8 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
) )
log.info("Configuration rendered") log.info("Configuration rendered")
if progress_callback:
progress_callback("Configuration valid...")
log_dir = os.path.join(PLANO_RUN_DIR, "logs") log_dir = os.path.join(PLANO_RUN_DIR, "logs")
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
@ -194,6 +203,8 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
os.path.join(log_dir, "brightstaff.log"), os.path.join(log_dir, "brightstaff.log"),
) )
log.info(f"Started brightstaff (PID {brightstaff_pid})") log.info(f"Started brightstaff (PID {brightstaff_pid})")
if progress_callback:
progress_callback(f"Started brightstaff (PID: {brightstaff_pid})...")
# Start envoy # Start envoy
envoy_pid = _daemon_exec( envoy_pid = _daemon_exec(
@ -210,6 +221,8 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
os.path.join(log_dir, "envoy.log"), os.path.join(log_dir, "envoy.log"),
) )
log.info(f"Started envoy (PID {envoy_pid})") log.info(f"Started envoy (PID {envoy_pid})")
if progress_callback:
progress_callback(f"Started envoy (PID: {envoy_pid})...")
# Save PIDs # Save PIDs
os.makedirs(PLANO_RUN_DIR, exist_ok=True) os.makedirs(PLANO_RUN_DIR, exist_ok=True)
@ -225,6 +238,8 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
# Health check # Health check
gateway_ports = _get_gateway_ports(plano_config_file) gateway_ports = _get_gateway_ports(plano_config_file)
log.info("Waiting for listeners to become healthy...") log.info("Waiting for listeners to become healthy...")
if progress_callback:
progress_callback("Waiting for listeners to become healthy...")
start_time = time.time() start_time = time.time()
timeout = 60 timeout = 60
@ -353,10 +368,15 @@ def _kill_pid(pid):
def stop_native(): def stop_native():
"""Stop natively-running Envoy and brightstaff processes.""" """Stop natively-running Envoy and brightstaff processes.
Returns:
bool: True if at least one process was running and received a stop signal,
False if no running native Plano process was found.
"""
if not os.path.exists(NATIVE_PID_FILE): if not os.path.exists(NATIVE_PID_FILE):
log.info("No native Plano instance found (PID file missing).") log.info("No native Plano instance found (PID file missing).")
return return False
with open(NATIVE_PID_FILE, "r") as f: with open(NATIVE_PID_FILE, "r") as f:
pids = json.load(f) pids = json.load(f)
@ -364,12 +384,14 @@ def stop_native():
envoy_pid = pids.get("envoy_pid") envoy_pid = pids.get("envoy_pid")
brightstaff_pid = pids.get("brightstaff_pid") brightstaff_pid = pids.get("brightstaff_pid")
had_running_process = False
for name, pid in [("envoy", envoy_pid), ("brightstaff", brightstaff_pid)]: for name, pid in [("envoy", envoy_pid), ("brightstaff", brightstaff_pid)]:
if pid is None: if pid is None:
continue continue
try: try:
os.kill(pid, signal.SIGTERM) os.kill(pid, signal.SIGTERM)
log.info(f"Sent SIGTERM to {name} (PID {pid})") log.info(f"Sent SIGTERM to {name} (PID {pid})")
had_running_process = True
except ProcessLookupError: except ProcessLookupError:
log.info(f"{name} (PID {pid}) already stopped") log.info(f"{name} (PID {pid}) already stopped")
continue continue
@ -394,7 +416,11 @@ def stop_native():
pass pass
os.unlink(NATIVE_PID_FILE) os.unlink(NATIVE_PID_FILE)
if had_running_process:
log.info("Plano stopped (native mode).") log.info("Plano stopped (native mode).")
else:
log.info("No native Plano instance was running.")
return had_running_process
def native_validate_config(plano_config_file): def native_validate_config(plano_config_file):