mirror of
https://github.com/katanemo/plano.git
synced 2026-04-25 00:36:34 +02:00
Improve planoai up/down CLI progress output (#858)
This commit is contained in:
parent
82f34f82f2
commit
36fa42b364
2 changed files with 263 additions and 158 deletions
|
|
@ -3,6 +3,8 @@ import os
|
|||
import multiprocessing
|
||||
import subprocess
|
||||
import sys
|
||||
import contextlib
|
||||
import logging
|
||||
import rich_click as click
|
||||
from planoai import targets
|
||||
|
||||
|
|
@ -33,6 +35,7 @@ from planoai.consts import (
|
|||
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT,
|
||||
DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT,
|
||||
NATIVE_PID_FILE,
|
||||
PLANO_RUN_DIR,
|
||||
PLANO_DOCKER_IMAGE,
|
||||
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:
|
||||
console.print(f"\n[red]✗[/red] [red]Missing API keys![/red]\n")
|
||||
for key in missing_keys:
|
||||
|
|
@ -293,13 +310,21 @@ def build(docker):
|
|||
help="Run Plano inside Docker instead of natively.",
|
||||
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."""
|
||||
from rich.status import Status
|
||||
|
||||
console = _console()
|
||||
_print_cli_header(console)
|
||||
|
||||
with _temporary_cli_log_level("warning" if not verbose else None):
|
||||
# Use the utility function to find config 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
|
||||
access_keys = get_llm_provider_access_keys(plano_config_file=plano_config_file)
|
||||
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 = []
|
||||
if access_keys:
|
||||
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:
|
||||
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:
|
||||
try:
|
||||
trace_server = start_trace_listener_background(grpc_port=tracing_port)
|
||||
trace_server = start_trace_listener_background(
|
||||
grpc_port=tracing_port
|
||||
)
|
||||
console.print(
|
||||
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:
|
||||
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(
|
||||
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:
|
||||
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.",
|
||||
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."""
|
||||
console = _console()
|
||||
_print_cli_header(console)
|
||||
|
||||
with _temporary_cli_log_level("warning" if not verbose else None):
|
||||
if not docker:
|
||||
from planoai.native_runner import stop_native
|
||||
|
||||
|
|
@ -471,13 +537,26 @@ def down(docker):
|
|||
f"[{PLANO_COLOR}]Shutting down Plano...[/{PLANO_COLOR}]",
|
||||
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:
|
||||
with console.status(
|
||||
f"[{PLANO_COLOR}]Shutting down Plano (Docker)...[/{PLANO_COLOR}]",
|
||||
spinner="dots",
|
||||
):
|
||||
stop_docker_container()
|
||||
if not verbose:
|
||||
console.print(
|
||||
"[green]✓[/green] Plano stopped! [dim](docker mode)[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import signal
|
|||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
|
||||
from planoai.consts import (
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
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")
|
||||
if progress_callback:
|
||||
progress_callback("Configuration valid...")
|
||||
|
||||
log_dir = os.path.join(PLANO_RUN_DIR, "logs")
|
||||
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"),
|
||||
)
|
||||
log.info(f"Started brightstaff (PID {brightstaff_pid})")
|
||||
if progress_callback:
|
||||
progress_callback(f"Started brightstaff (PID: {brightstaff_pid})...")
|
||||
|
||||
# Start envoy
|
||||
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"),
|
||||
)
|
||||
log.info(f"Started envoy (PID {envoy_pid})")
|
||||
if progress_callback:
|
||||
progress_callback(f"Started envoy (PID: {envoy_pid})...")
|
||||
|
||||
# Save PIDs
|
||||
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
|
||||
gateway_ports = _get_gateway_ports(plano_config_file)
|
||||
log.info("Waiting for listeners to become healthy...")
|
||||
if progress_callback:
|
||||
progress_callback("Waiting for listeners to become healthy...")
|
||||
|
||||
start_time = time.time()
|
||||
timeout = 60
|
||||
|
|
@ -353,10 +368,15 @@ def _kill_pid(pid):
|
|||
|
||||
|
||||
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):
|
||||
log.info("No native Plano instance found (PID file missing).")
|
||||
return
|
||||
return False
|
||||
|
||||
with open(NATIVE_PID_FILE, "r") as f:
|
||||
pids = json.load(f)
|
||||
|
|
@ -364,12 +384,14 @@ def stop_native():
|
|||
envoy_pid = pids.get("envoy_pid")
|
||||
brightstaff_pid = pids.get("brightstaff_pid")
|
||||
|
||||
had_running_process = False
|
||||
for name, pid in [("envoy", envoy_pid), ("brightstaff", brightstaff_pid)]:
|
||||
if pid is None:
|
||||
continue
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
log.info(f"Sent SIGTERM to {name} (PID {pid})")
|
||||
had_running_process = True
|
||||
except ProcessLookupError:
|
||||
log.info(f"{name} (PID {pid}) already stopped")
|
||||
continue
|
||||
|
|
@ -394,7 +416,11 @@ def stop_native():
|
|||
pass
|
||||
|
||||
os.unlink(NATIVE_PID_FILE)
|
||||
if had_running_process:
|
||||
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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue