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 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()
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue