-q and -v migrated

This commit is contained in:
Andrei Cravtov
2026-06-12 14:23:34 +01:00
parent 98901d3234
commit fa6faced53
7 changed files with 95 additions and 57 deletions

View File

@@ -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

View File

@@ -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=<LEVEL>` 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

View File

@@ -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: ...

View File

@@ -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"

View File

@@ -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<PyModule>) -> PyResult<()> {
bootstrap_submodule(m)?;
app_submodule(m)?;

View File

@@ -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

View File

@@ -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>{level: <8}</level>] <level>{message}</level>",
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>{level: <8}</level> | {name}:{function}:{line} ] <level>{message}</level>",
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.")
"""