From fa6faced53b7680dbd889cf933e43192bae6cfd5 Mon Sep 17 00:00:00 2001 From: Andrei Cravtov Date: Fri, 12 Jun 2026 14:23:34 +0100 Subject: [PATCH] -q and -v migrated --- AGENTS.md | 4 +-- README.md | 12 +++++++ rust/exo_rs/exo_rs.pyi | 5 +++ rust/exo_rs/src/config/app.rs | 4 +-- rust/exo_rs/src/config/mod.rs | 19 +++++++++-- src/exo/main.py | 45 ++++++++++++++----------- src/exo/shared/logging.py | 63 ++++++++++++++++++----------------- 7 files changed, 95 insertions(+), 57 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 537ff3667..5be09e0bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,8 +15,8 @@ cd dashboard && npm install && npm run build && cd .. # Run exo (starts both master and worker with API at http://localhost:52415) uv run exo -# Run with verbose logging -uv run exo -v # or -vv for more verbose +# Run with debug logging +uv run exo --verbosity=debug # or: uv run exo -v debug # Run tests (excludes slow tests by default) uv run pytest diff --git a/README.md b/README.md index 404b8d594..8fd738896 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,7 @@ exo supports several environment variables for configuration: | `EXO_OFFLINE` | Startup default for `--offline` when the CLI flag is omitted. Uses only local models and skips internet checks. | `false` | | `EXO_ENABLE_IMAGE_MODELS` | Enable image model support | `false` | | `EXO_ZENOH_NAMESPACE` | Custom namespace for cluster isolation | None | +| `EXO_VERBOSITY` | Startup default for `--verbosity=` when the CLI flag is omitted. Valid levels: `off`, `error`, `warn`, `info`, `debug`, `trace`. | `info` | | `EXO_FAST_SYNCH` | Startup default for `--fast-synch=true\|false` when the CLI flag is omitted (for JACCL backend). Leave unset for automatic behavior. | Auto | | `EXO_TRACING_ENABLED` | Enable distributed tracing for performance analysis | `false` | @@ -335,6 +336,17 @@ EXO_ENABLE_IMAGE_MODELS=true uv run exo # Use custom namespace for cluster isolation EXO_ZENOH_NAMESPACE=my-dev-cluster uv run exo +# Set logging verbosity (CLI or ENV arg) +uv run exo --verbosity=debug +uv run exo -v debug +EXO_VERBOSITY=debug uv run exo + +# Only show error logs +uv run exo --quiet + +# Disable logging output +uv run exo --verbosity=off + # Force MLX FAST_SYNCH off (CLI or ENV arg) uv run exo --fast-synch=false EXO_FAST_SYNCH=false uv run exo diff --git a/rust/exo_rs/exo_rs.pyi b/rust/exo_rs/exo_rs.pyi index c323ffc65..3a4568d3c 100644 --- a/rust/exo_rs/exo_rs.pyi +++ b/rust/exo_rs/exo_rs.pyi @@ -430,3 +430,8 @@ class VerbosityFilter(enum.Enum): Debug = ... Trace = ... + def __lt__(self, other: object, /) -> bool: ... + def __le__(self, other: object, /) -> bool: ... + def __gt__(self, other: object, /) -> bool: ... + def __ge__(self, other: object, /) -> bool: ... + diff --git a/rust/exo_rs/src/config/app.rs b/rust/exo_rs/src/config/app.rs index 04a5c2c14..7a516e01f 100644 --- a/rust/exo_rs/src/config/app.rs +++ b/rust/exo_rs/src/config/app.rs @@ -39,7 +39,7 @@ pub struct AppArgs { long = "quiet", action = ArgAction::SetTrue, conflicts_with = "verbosity", - help = "Disable non-error logging (alias for --verbosity=off)" + help = "Only show error logs (alias for --verbosity=error)" )] #[serde(skip)] verbosity_off: bool, @@ -49,7 +49,7 @@ pub struct AppArgs { env = "EXO_VERBOSITY", value_enum, default_value = "info", // TODO: when config.toml introduced, remove this - default_value_if("verbosity_off", "true", Some("off")), + default_value_if("verbosity_off", "true", Some("error")), value_name = "LEVEL", conflicts_with = "verbosity_off", help = "Set the verbosity filter" diff --git a/rust/exo_rs/src/config/mod.rs b/rust/exo_rs/src/config/mod.rs index 9e16dffb1..c1b030965 100644 --- a/rust/exo_rs/src/config/mod.rs +++ b/rust/exo_rs/src/config/mod.rs @@ -14,11 +14,12 @@ pub mod path; /// Verbosity level used by EXO's logger. #[gen_stub_pyclass_enum] -#[pyclass(eq, eq_int, from_py_object)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] +#[pyclass(eq, eq_int, ord, from_py_object)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ValueEnum)] #[serde(rename_all = "lowercase")] +#[repr(u8)] pub enum VerbosityFilter { - Off, + Off = 0, Error, Warn, Info, @@ -26,6 +27,18 @@ pub enum VerbosityFilter { Trace, } +pyo3_stub_gen::inventory::submit! { + pyo3_stub_gen::derive::gen_methods_from_python! { + r#" + class VerbosityFilter: + def __lt__(self, other: object, /) -> bool: ... + def __le__(self, other: object, /) -> bool: ... + def __gt__(self, other: object, /) -> bool: ... + def __ge__(self, other: object, /) -> bool: ... + "# + } +} + pub fn config_submodule(m: &Bound) -> PyResult<()> { bootstrap_submodule(m)?; app_submodule(m)?; diff --git a/src/exo/main.py b/src/exo/main.py index 3f9f474b5..a7f25fdc4 100644 --- a/src/exo/main.py +++ b/src/exo/main.py @@ -10,7 +10,13 @@ from typing import Self import anyio from anyio.lowlevel import checkpoint as anyio_checkpoint from daemon import DaemonContext # pyright: ignore[reportMissingTypeStubs] -from exo_rs import AppSettings, BootstrapSettings, CliArgs, Pidfile, PidfileError +from exo_rs import ( + AppSettings, + BootstrapSettings, + CliArgs, + Pidfile, + PidfileError, +) from loguru import logger from pydantic import PositiveInt @@ -343,7 +349,7 @@ def main_inner(args: "Args", cli_args: CliArgs): mp.set_start_method("spawn", force=True) # TODO: Refactor the current verbosity system - logger_setup(config.bootstrap().log_files.exo_log, args.verbosity) + logger_setup(config.bootstrap().log_files.exo_log, config.app().verbosity) logger.info(f"pid = {os.getpid()}") if os.getenv("EXO_LIBP2P_NAMESPACE"): @@ -377,7 +383,6 @@ def main_inner(args: "Args", cli_args: CliArgs): class Args(FrozenModel): - verbosity: int = 0 api_port: PositiveInt = 52415 bootstrap_peers: list[str] = [] namespace: str @@ -387,22 +392,6 @@ class Args(FrozenModel): @classmethod def parse(cls) -> Self: parser = argparse.ArgumentParser(prog="EXO") - default_verbosity = 0 - parser.add_argument( - "-q", - "--quiet", - action="store_const", - const=-1, - dest="verbosity", - default=default_verbosity, - ) - parser.add_argument( - "-v", - "--verbose", - action="count", - dest="verbosity", - default=default_verbosity, - ) parser.add_argument( "--api-port", type=int, @@ -498,6 +487,22 @@ class Args(FrozenModel): default=argparse.SUPPRESS, help=argparse.SUPPRESS, ) + parser.add_argument( + "-q", + "--quiet", + action="store_true", + dest="_verbosity_off_ignored", + default=argparse.SUPPRESS, + help=argparse.SUPPRESS, + ) + parser.add_argument( + "-v", + "--verbosity", + nargs="?", + dest="_verbosity_ignored", + default=argparse.SUPPRESS, + help=argparse.SUPPRESS, + ) args = parser.parse_args() parsed_args = vars(args) @@ -511,5 +516,7 @@ class Args(FrozenModel): parsed_args.pop("_offline_ignored", None) parsed_args.pop("_no_batch_ignored", None) parsed_args.pop("_fast_synch_ignored", None) + parsed_args.pop("_verbosity_off_ignored", None) + parsed_args.pop("_verbosity_ignored", None) return cls(**parsed_args) # pyright: ignore[reportAny] - We are intentionally validating here, we can't do it statically diff --git a/src/exo/shared/logging.py b/src/exo/shared/logging.py index 80e476354..73eb8bcf2 100644 --- a/src/exo/shared/logging.py +++ b/src/exo/shared/logging.py @@ -4,6 +4,7 @@ from collections.abc import Iterator from pathlib import Path import zstandard +from exo_rs import VerbosityFilter from hypercorn import Config from hypercorn.logging import Logger as HypercornLogger from loguru import logger @@ -43,7 +44,27 @@ class _InterceptHandler(logging.Handler): logger.opt(depth=3, exception=record.exc_info).log(level, record.getMessage()) -def logger_setup(log_file: Path | None, verbosity: int = 0): +def _loguru_log_level(verbosity: VerbosityFilter): + match verbosity: + case VerbosityFilter.Off: + raise ValueError( + "VerbosityFilter.Off does not translate to a loguru log-level" + ) + case VerbosityFilter.Error: + return "ERROR" + case VerbosityFilter.Warn: + return "WARNING" + case VerbosityFilter.Info: + return "INFO" + case VerbosityFilter.Debug: + return "DEBUG" + case VerbosityFilter.Trace: + return "TRACE" + + +def logger_setup( + log_file: Path | None, verbosity: VerbosityFilter = VerbosityFilter.Info +): """Set up logging for this process - formatting, file handles, verbosity and output""" logging.getLogger("exo_rs").setLevel(logging.INFO) @@ -56,11 +77,17 @@ def logger_setup(log_file: Path | None, verbosity: int = 0): # replace all stdlib loggers with _InterceptHandlers that log to loguru logging.basicConfig(handlers=[_InterceptHandler()], level=0) - if verbosity == 0: + # if Off then no logging - return early + if verbosity == VerbosityFilter.Off: + return + + # info (or less verbose than info) gets a different formatter + level = _loguru_log_level(verbosity) + if verbosity <= VerbosityFilter.Info: logger.add( sys.__stderr__, # type: ignore format="[ {time:hh:mm:ss.SSSSA} | {level: <8}] {message}", - level="INFO", + level=level, colorize=True, enqueue=True, ) @@ -68,7 +95,7 @@ def logger_setup(log_file: Path | None, verbosity: int = 0): logger.add( sys.__stderr__, # type: ignore format="[ {time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} ] {message}", - level="DEBUG", + level=level, colorize=True, enqueue=True, ) @@ -77,7 +104,7 @@ def logger_setup(log_file: Path | None, verbosity: int = 0): logger.add( log_file, format="[ {time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} ] {message}", - level="DEBUG" if verbosity > 0 else "INFO", + level=level, colorize=False, enqueue=True, rotation=lambda _, __: next(rotate_once), @@ -89,29 +116,3 @@ def logger_setup(log_file: Path | None, verbosity: int = 0): def logger_cleanup(): """Flush all queues before shutting down so any in-flight logs are written to disk""" logger.complete() - - -""" --- TODO: Capture MLX Log output: -import contextlib -import sys -from loguru import logger - -class StreamToLogger: - - def __init__(self, level="INFO"): - self._level = level - - def write(self, buffer): - for line in buffer.rstrip().splitlines(): - logger.opt(depth=1).log(self._level, line.rstrip()) - - def flush(self): - pass - -logger.remove() -logger.add(sys.__stdout__) - -stream = StreamToLogger() -with contextlib.redirect_stdout(stream): - print("Standard output is sent to added handlers.") -"""