mirror of
https://github.com/wassname/ray.git
synced 2026-06-29 19:00:58 +08:00
[Autoscaler] CLI Logger docs (#9690)
Co-authored-by: Richard Liaw <rliaw@berkeley.edu>
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
"""Logger implementing the Command Line Interface.
|
||||
|
||||
A replacement for the standard Python `logging` API
|
||||
designed for implementing a better CLI UX for the cluster launcher.
|
||||
|
||||
Supports color, bold text, italics, underlines, etc.
|
||||
(depending on TTY features)
|
||||
as well as indentation and other structured output.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import click
|
||||
@@ -8,18 +18,53 @@ import colorful as cf
|
||||
colorama.init()
|
||||
|
||||
|
||||
def _strip_codes(msg):
|
||||
return msg # todo
|
||||
|
||||
|
||||
# we could bold "{}" strings automatically but do we want that?
|
||||
# todo:
|
||||
def _format_msg(msg,
|
||||
*args,
|
||||
_tags=None,
|
||||
_numbered=None,
|
||||
_no_format=None,
|
||||
**kwargs):
|
||||
"""Formats a message for printing.
|
||||
|
||||
Renders `msg` using the built-in `str.format` and the passed-in
|
||||
`*args` and `**kwargs`.
|
||||
|
||||
Args:
|
||||
*args (Any): `.format` arguments for `msg`.
|
||||
_tags (Dict[str, Any]):
|
||||
key-value pairs to display at the end of
|
||||
the message in square brackets.
|
||||
|
||||
If a tag is set to `True`, it is printed without the value,
|
||||
the presence of the tag treated as a "flag".
|
||||
|
||||
E.g. `_format_msg("hello", _tags=dict(from=mom, signed=True))`
|
||||
`hello [from=Mom, signed]`
|
||||
_numbered (Tuple[str, int, int]):
|
||||
`(brackets, i, n)`
|
||||
|
||||
The `brackets` string is composed of two "bracket" characters,
|
||||
`i` is the index, `n` is the total.
|
||||
|
||||
The string `{i}/{n}` surrounded by the "brackets" is
|
||||
prepended to the message.
|
||||
|
||||
This is used to number steps in a procedure, with different
|
||||
brackets specifying different major tasks.
|
||||
|
||||
E.g. `_format_msg("hello", _numbered=("[]", 0, 5))`
|
||||
`[0/5] hello`
|
||||
_no_format (bool):
|
||||
If `_no_format` is `True`,
|
||||
`.format` will not be called on the message.
|
||||
|
||||
Useful if the output is user-provided or may otherwise
|
||||
contain an unexpected formatting string (e.g. "{}").
|
||||
|
||||
Returns:
|
||||
The formatted message.
|
||||
"""
|
||||
|
||||
if isinstance(msg, str) or isinstance(msg, ColorfulString):
|
||||
tags_str = ""
|
||||
if _tags is not None:
|
||||
@@ -59,6 +104,41 @@ def _format_msg(msg,
|
||||
|
||||
|
||||
class _CliLogger():
|
||||
"""Singleton class for CLI logging.
|
||||
|
||||
Attributes:
|
||||
strip (bool):
|
||||
If `strip` is `True`, all TTY control sequences will be
|
||||
removed from the output.
|
||||
old_style (bool):
|
||||
If `old_style` is `True`, the old logging calls are used instead
|
||||
of the new CLI UX. This is enabled by default and remains for
|
||||
backwards compatibility.
|
||||
color_mode (str):
|
||||
Can be "true", "false", or "auto".
|
||||
|
||||
Determines the value of `strip` using a human-readable string
|
||||
that can be set from command line arguments.
|
||||
|
||||
Also affects the `colorful` settings.
|
||||
|
||||
If `color_mode` is "auto", `strip` is set to `not stdout.isatty()`
|
||||
indent_level (int):
|
||||
The current indentation level.
|
||||
|
||||
All messages will be indented by prepending `" " * indent_level`
|
||||
vebosity (int):
|
||||
Output verbosity.
|
||||
|
||||
Low verbosity will disable `verbose` and `very_verbose` messages.
|
||||
dump_command_output (bool):
|
||||
Determines whether the old behavior of dumping command output
|
||||
to console will be used, or the new behavior of redirecting to
|
||||
a file.
|
||||
|
||||
! Currently unused.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.strip = False
|
||||
self.old_style = True
|
||||
@@ -68,31 +148,53 @@ class _CliLogger():
|
||||
|
||||
self.dump_command_output = False
|
||||
|
||||
self.info = {}
|
||||
# store whatever colorful has detected for future use if
|
||||
# the color ouput is toggled (colorful detects # of supported colors,
|
||||
# so it has some non-trivial logic to determine this)
|
||||
self._autodetected_cf_colormode = cf.colormode
|
||||
|
||||
def detect_colors(self):
|
||||
"""Update color output settings.
|
||||
|
||||
Parse the `color_mode` string and optionally disable or force-enable
|
||||
color output
|
||||
(8-color ANSI if no terminal detected to be safe) in colorful.
|
||||
"""
|
||||
|
||||
if self.color_mode == "true":
|
||||
self.strip = False
|
||||
if self._autodetected_cf_colormode != cf.NO_COLORS:
|
||||
cf.colormode = self._autodetected_cf_colormode
|
||||
else:
|
||||
cf.colormode = cf.ANSI_8_COLORS
|
||||
return
|
||||
if self.color_mode == "false":
|
||||
self.strip = True
|
||||
cf.disable()
|
||||
return
|
||||
if self.color_mode == "auto":
|
||||
self.strip = sys.stdout.isatty()
|
||||
self.strip = not sys.stdout.isatty()
|
||||
# colorful autodetects tty settings
|
||||
return
|
||||
|
||||
raise ValueError("Invalid log color setting: " + self.color_mode)
|
||||
|
||||
def newline(self):
|
||||
"""Print a line feed.
|
||||
"""
|
||||
self._print("")
|
||||
|
||||
def _print(self, msg, linefeed=True):
|
||||
"""Proxy for printing messages.
|
||||
|
||||
Args:
|
||||
msg (str): Message to print.
|
||||
linefeed (bool):
|
||||
If `linefeed` is `False` no linefeed is printed at the
|
||||
end of the message.
|
||||
"""
|
||||
|
||||
if self.old_style:
|
||||
return
|
||||
|
||||
if self.strip:
|
||||
msg = _strip_codes(msg)
|
||||
|
||||
rendered_message = " " * self.indent_level + msg
|
||||
|
||||
if not linefeed:
|
||||
@@ -102,7 +204,9 @@ class _CliLogger():
|
||||
|
||||
print(rendered_message)
|
||||
|
||||
def indented(self, cls=False):
|
||||
def indented(self):
|
||||
"""Context manager that starts an indented block of output.
|
||||
"""
|
||||
cli_logger = self
|
||||
|
||||
class IndentedContextManager():
|
||||
@@ -112,20 +216,38 @@ class _CliLogger():
|
||||
def __exit__(self, type, value, tb):
|
||||
cli_logger.indent_level -= 1
|
||||
|
||||
if cls:
|
||||
# fixme: this does not work :()
|
||||
return IndentedContextManager
|
||||
return IndentedContextManager()
|
||||
|
||||
def timed(self, msg, *args, **kwargs):
|
||||
"""
|
||||
TODO: Unimplemented special type of output grouping that displays
|
||||
a timer for its execution. The method was not removed so we
|
||||
can mark places where this might be useful in case we ever
|
||||
implement this.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
return self.group(msg, *args, **kwargs)
|
||||
|
||||
def group(self, msg, *args, **kwargs):
|
||||
"""Print a group title in a special color and start an indented block.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
self._print(_format_msg(cf.cornflowerBlue(msg), *args, **kwargs))
|
||||
|
||||
return self.indented()
|
||||
|
||||
def verbatim_error_ctx(self, msg, *args, **kwargs):
|
||||
"""Context manager for printing multi-line error messages.
|
||||
|
||||
Displays a start sequence "!!! {optional message}"
|
||||
and a matching end sequence "!!!".
|
||||
|
||||
The string "!!!" can be used as a "tombstone" for searching.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
cli_logger = self
|
||||
|
||||
class VerbatimErorContextManager():
|
||||
@@ -138,40 +260,87 @@ class _CliLogger():
|
||||
return VerbatimErorContextManager()
|
||||
|
||||
def labeled_value(self, key, msg, *args, **kwargs):
|
||||
"""Displays a key-value pair with special formatting.
|
||||
|
||||
Args:
|
||||
key (str): Label that is prepended to the message.
|
||||
|
||||
For other arguments, see `_format_msg`.
|
||||
"""
|
||||
self._print(
|
||||
cf.cyan(key) + ": " + _format_msg(cf.bold(msg), *args, **kwargs))
|
||||
|
||||
def verbose(self, msg, *args, **kwargs):
|
||||
"""Prints a message if verbosity is not 0.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.verbosity > 0:
|
||||
self.print(msg, *args, **kwargs)
|
||||
|
||||
def verbose_error(self, msg, *args, **kwargs):
|
||||
"""Logs an error if verbosity is not 0.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.verbosity > 0:
|
||||
self.error(msg, *args, **kwargs)
|
||||
|
||||
def very_verbose(self, msg, *args, **kwargs):
|
||||
"""Prints if verbosity is > 1.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.verbosity > 1:
|
||||
self.print(msg, *args, **kwargs)
|
||||
|
||||
def success(self, msg, *args, **kwargs):
|
||||
"""Prints a formatted success message.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
self._print(_format_msg(cf.green(msg), *args, **kwargs))
|
||||
|
||||
def warning(self, msg, *args, **kwargs):
|
||||
"""Prints a formatted warning message.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
self._print(_format_msg(cf.yellow(msg), *args, **kwargs))
|
||||
|
||||
def error(self, msg, *args, **kwargs):
|
||||
"""Prints a formatted error message.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
self._print(_format_msg(cf.red(msg), *args, **kwargs))
|
||||
|
||||
def print(self, msg, *args, **kwargs):
|
||||
"""Prints a message.
|
||||
|
||||
For arguments, see `_format_msg`.
|
||||
"""
|
||||
self._print(_format_msg(msg, *args, **kwargs))
|
||||
|
||||
def abort(self, msg=None, *args, **kwargs):
|
||||
"""Prints an error and aborts execution.
|
||||
|
||||
Print an error and throw an exception to terminate the program
|
||||
(the exception will not print a message).
|
||||
"""
|
||||
if msg is not None:
|
||||
self.error(msg, *args, **kwargs)
|
||||
|
||||
raise SilentClickException("Exiting due to cli_logger.abort()")
|
||||
|
||||
def doassert(self, val, msg, *args, **kwargs):
|
||||
"""Handle assertion without throwing a scary exception.
|
||||
|
||||
Args:
|
||||
val (bool): Value to check.
|
||||
|
||||
For other arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.old_style:
|
||||
return
|
||||
|
||||
@@ -179,34 +348,105 @@ class _CliLogger():
|
||||
self.abort(msg, *args, **kwargs)
|
||||
|
||||
def old_debug(self, logger, msg, *args, **kwargs):
|
||||
"""Old debug logging proxy.
|
||||
|
||||
Pass along an old debug log iff new logging is disabled.
|
||||
Supports the new formatting features.
|
||||
|
||||
Args:
|
||||
logger (logging.Logger):
|
||||
Logger to use if old logging behavior is selected.
|
||||
|
||||
For other arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.old_style:
|
||||
logger.debug(_format_msg(msg, *args, **kwargs))
|
||||
return
|
||||
|
||||
def old_info(self, logger, msg, *args, **kwargs):
|
||||
"""Old info logging proxy.
|
||||
|
||||
Pass along an old info log iff new logging is disabled.
|
||||
Supports the new formatting features.
|
||||
|
||||
Args:
|
||||
logger (logging.Logger):
|
||||
Logger to use if old logging behavior is selected.
|
||||
|
||||
For other arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.old_style:
|
||||
logger.info(_format_msg(msg, *args, **kwargs))
|
||||
return
|
||||
|
||||
def old_warning(self, logger, msg, *args, **kwargs):
|
||||
"""Old warning logging proxy.
|
||||
|
||||
Pass along an old warning log iff new logging is disabled.
|
||||
Supports the new formatting features.
|
||||
|
||||
Args:
|
||||
logger (logging.Logger):
|
||||
Logger to use if old logging behavior is selected.
|
||||
|
||||
For other arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.old_style:
|
||||
logger.warning(_format_msg(msg, *args, **kwargs))
|
||||
return
|
||||
|
||||
def old_error(self, logger, msg, *args, **kwargs):
|
||||
"""Old error logging proxy.
|
||||
|
||||
Pass along an old error log iff new logging is disabled.
|
||||
Supports the new formatting features.
|
||||
|
||||
Args:
|
||||
logger (logging.Logger):
|
||||
Logger to use if old logging behavior is selected.
|
||||
|
||||
For other arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.old_style:
|
||||
logger.error(_format_msg(msg, *args, **kwargs))
|
||||
return
|
||||
|
||||
def old_exception(self, logger, msg, *args, **kwargs):
|
||||
"""Old exception logging proxy.
|
||||
|
||||
Pass along an old exception log iff new logging is disabled.
|
||||
Supports the new formatting features.
|
||||
|
||||
Args:
|
||||
logger (logging.Logger):
|
||||
Logger to use if old logging behavior is selected.
|
||||
|
||||
For other arguments, see `_format_msg`.
|
||||
"""
|
||||
if self.old_style:
|
||||
logger.exception(_format_msg(msg, *args, **kwargs))
|
||||
return
|
||||
|
||||
def render_list(self, xs, separator=cf.reset(", ")):
|
||||
"""Render a list of bolded values using a non-bolded separator.
|
||||
"""
|
||||
return separator.join([str(cf.bold(x)) for x in xs])
|
||||
|
||||
def confirm(self, yes, msg, *args, _abort=False, _default=False, **kwargs):
|
||||
"""Display a confirmation dialog.
|
||||
|
||||
Valid answers are "y/yes/true/1" and "n/no/false/0".
|
||||
|
||||
Args:
|
||||
yes (bool): If `yes` is `True` the dialog will default to "yes"
|
||||
and continue without waiting for user input.
|
||||
_abort (bool):
|
||||
If `_abort` is `True`,
|
||||
"no" means aborting the program.
|
||||
_default (bool):
|
||||
The default action to take if the user just presses enter
|
||||
with no input.
|
||||
"""
|
||||
if self.old_style:
|
||||
return
|
||||
|
||||
@@ -274,6 +514,10 @@ class _CliLogger():
|
||||
return res
|
||||
|
||||
def old_confirm(self, msg, yes):
|
||||
"""Old confirm dialog proxy.
|
||||
|
||||
Let `click` display a confirm dialog iff new logging is disabled.
|
||||
"""
|
||||
if not self.old_style:
|
||||
return
|
||||
|
||||
@@ -281,7 +525,8 @@ class _CliLogger():
|
||||
|
||||
|
||||
class SilentClickException(click.ClickException):
|
||||
"""
|
||||
"""`ClickException` that does not print a message.
|
||||
|
||||
Some of our tooling relies on catching ClickException in particular.
|
||||
|
||||
However the default prints a message, which is undesirable since we expect
|
||||
|
||||
Reference in New Issue
Block a user