Skip to content

Commit 312da09

Browse files
d4l3kfacebook-github-bot
authored andcommitted
cli: clean up outputs + prettify (#211)
Summary: This cleans up the CLI output to make it be more readable and prettier by using colors. * All stderr logs are prefixed with `torchx <timestamp> <log level>`. This allows distinguishing stdout with the local scheduler. * runopts output has been reworked * added `--log_level` CLI flag * reduced log spam when using run Pull Request resolved: #211 Test Plan: NOTE: actual PR has "dim" timestamps not white/light gray since light gray does not render well at all on light backgrounds ### dim timestamps (actual) ![2021-09-29-183353_743x87_scrot](https://user-images.githubusercontent.com/909104/135371260-a35d3588-180d-4604-94e7-14466e044c6e.png) ![2021-09-29-183345_843x101_scrot](https://user-images.githubusercontent.com/909104/135371263-75c5489c-0fdb-4619-b34e-be4cdb5d3b74.png) ### run (wrong color timestamps) ![2021-09-29-181820_883x266_scrot](https://user-images.githubusercontent.com/909104/135369973-79d8c031-8f3f-41b7-b827-5e093cc471a2.png) ### log ![2021-09-29-181732_939x46_scrot](https://user-images.githubusercontent.com/909104/135369974-8a58486f-4e4f-42f4-8f5a-e24a26f3f3c4.png) ### runopts ![2021-09-29-181710_474x578_scrot](https://user-images.githubusercontent.com/909104/135369976-8f19293c-45b1-4297-b53a-96d7c218f7d8.png) ### log level 0 (wrong color timestamps) ![2021-09-29-182000_914x653_scrot](https://user-images.githubusercontent.com/909104/135369972-9ee2e536-1b5b-4650-ab6a-9a45a9ea74ca.png) Reviewed By: kiukchung Differential Revision: D31295428 Pulled By: d4l3k fbshipit-source-id: a4eb6ffb0ff4744581e48559c8b61f9169877a19
1 parent 228fcff commit 312da09

File tree

7 files changed

+75
-42
lines changed

7 files changed

+75
-42
lines changed

torchx/cli/cmd_log.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,12 @@
1717
from pyre_extensions import none_throws
1818
from torchx import specs
1919
from torchx.cli.cmd_base import SubCommand
20+
from torchx.cli.colors import GREEN, ENDC
2021
from torchx.runner import Runner, get_runner
2122
from torchx.specs.api import make_app_handle
2223

2324
logger: logging.Logger = logging.getLogger(__name__)
2425

25-
# only print colors if outputting directly to a terminal
26-
if sys.stdout.isatty():
27-
GREEN = "\033[32m"
28-
ENDC = "\033[0m"
29-
else:
30-
GREEN = ""
31-
ENDC = ""
32-
33-
3426
ID_FORMAT = "SCHEDULER://[SESSION_NAME]/APP_ID/[ROLE_NAME/[REPLICA_IDS,...]]"
3527

3628

torchx/cli/cmd_run.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,15 @@ def run(self, args: argparse.Namespace) -> None:
158158

159159
def _wait_and_exit(self, runner: Runner, app_handle: str) -> None:
160160
logger.info("Waiting for the app to finish...")
161+
161162
status = runner.wait(app_handle, wait_interval=1)
162-
logger.info(status)
163163
if not status:
164164
raise RuntimeError(f"unknown status, wait returned {status}")
165+
166+
logger.info(f"Job finished: {status.state}")
167+
165168
if status.state != specs.AppState.SUCCEEDED:
169+
logger.error(status)
166170
sys.exit(1)
171+
else:
172+
logger.debug(status)

torchx/cli/cmd_runopts.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import logging
1010

1111
from torchx.cli.cmd_base import SubCommand
12+
from torchx.cli.colors import GREEN, ENDC
1213
from torchx.runner.api import get_runner
1314

1415
logger: logging.Logger = logging.getLogger(__name__)
@@ -24,11 +25,9 @@ def add_arguments(self, subparser: argparse.ArgumentParser) -> None:
2425
)
2526

2627
def run(self, args: argparse.Namespace) -> None:
27-
scheduler = args.scheduler
28+
filter = args.scheduler
2829
run_opts = get_runner().run_opts()
2930

30-
if not scheduler:
31-
for scheduler, opts in run_opts.items():
32-
logger.info(f"{scheduler}:\n{repr(opts)}")
33-
else:
34-
logger.info(repr(run_opts[scheduler]))
31+
for scheduler, opts in run_opts.items():
32+
if not filter or scheduler == filter:
33+
print(f"{GREEN}{scheduler}{ENDC}:\n{repr(opts)}\n")

torchx/cli/colors.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Facebook, Inc. and its affiliates.
3+
# All rights reserved.
4+
#
5+
# This source code is licensed under the BSD-style license found in the
6+
# LICENSE file in the root directory of this source tree.
7+
8+
import sys
9+
10+
# only print colors if outputting directly to a terminal
11+
if sys.stdout.isatty():
12+
GREEN = "\033[32m"
13+
ORANGE = "\033[38:2:238:76:44m"
14+
GRAY = "\033[2m"
15+
ENDC = "\033[0m"
16+
else:
17+
GREEN = ""
18+
ORANGE = ""
19+
GRAY = ""
20+
ENDC = ""

torchx/cli/main.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from torchx.cli.cmd_run import CmdBuiltins, CmdRun
1515
from torchx.cli.cmd_runopts import CmdRunopts
1616
from torchx.cli.cmd_status import CmdStatus
17+
from torchx.cli.colors import ORANGE, GRAY, ENDC
1718

1819
sub_parser_description = """Use the following commands to run operations, e.g.:
1920
torchx run ${JOB_NAME}
@@ -26,6 +27,12 @@ def create_parser() -> ArgumentParser:
2627
"""
2728

2829
parser = ArgumentParser(description="torchx CLI")
30+
parser.add_argument(
31+
"--log_level",
32+
type=int,
33+
help="Python logging log level",
34+
default=logging.INFO,
35+
)
2936
subparser = parser.add_subparsers(
3037
title="sub-commands",
3138
description=sub_parser_description,
@@ -49,13 +56,15 @@ def create_parser() -> ArgumentParser:
4956

5057

5158
def main(argv: List[str] = sys.argv[1:]) -> None:
59+
parser = create_parser()
60+
args = parser.parse_args(argv)
61+
5262
logging.basicConfig(
53-
level=logging.INFO,
54-
format="%(message)s",
63+
level=args.log_level,
64+
format=f"{ORANGE}torchx{ENDC} {GRAY}%(asctime)s %(levelname)-8s{ENDC} %(message)s",
65+
datefmt="%Y-%m-%d %H:%M:%S",
5566
)
5667

57-
parser = create_parser()
58-
args = parser.parse_args(argv)
5968
if "func" not in args:
6069
parser.print_help()
6170
sys.exit(1)

torchx/schedulers/local_scheduler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def fetch(self, image: str) -> str:
231231
try:
232232
subprocess.run(["docker", "pull", image], check=True)
233233
except Exception as e:
234-
print(f"failed to fetch image {image}, falling back to local: {e}")
234+
log.warning(f"failed to fetch image {image}, falling back to local: {e}")
235235
return ""
236236

237237
def get_replica_param(
@@ -438,7 +438,7 @@ def _fmt_io_filename(std_io: Optional[TextIO]) -> str:
438438
with open(os.path.join(self.log_dir, "SUCCESS"), "w") as fp:
439439
fp.write(info_str)
440440

441-
log.info(f"Successfully closed app_id: {self.id}.\n{info_str}")
441+
log.debug(f"Successfully closed app_id: {self.id}.\n{info_str}")
442442

443443
def __repr__(self) -> str:
444444
role_to_pid = {}
@@ -639,7 +639,7 @@ def _popen(
639639
env["PATH"] = os.pathsep.join([p for p in path if p]) # remove empty str
640640

641641
args_pfmt = pprint.pformat(asdict(replica_params), indent=2, width=80)
642-
log.info(f"Running {role_name} (replica {replica_id}):\n {args_pfmt}")
642+
log.debug(f"Running {role_name} (replica {replica_id}):\n {args_pfmt}")
643643

644644
proc = subprocess.Popen(
645645
args=replica_params.args,

torchx/specs/api.py

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -657,25 +657,32 @@ def resolve(self, config: RunConfig) -> RunConfig:
657657
return resolved_cfg
658658

659659
def __repr__(self) -> str:
660-
# make it a pretty printable dict
661-
pretty_opts = {}
662-
for cfg_key, runopt in self._opts.items():
663-
key = f"*{cfg_key}" if runopt.is_required else cfg_key
664-
opt = {"type": get_type_name(runopt.opt_type)}
665-
if runopt.is_required:
666-
opt["required"] = "True"
667-
else:
668-
opt["default"] = str(runopt.default)
669-
opt["help"] = runopt.help
670-
671-
pretty_opts[key] = opt
672-
import pprint
673-
674-
return pprint.pformat(
675-
pretty_opts,
676-
indent=2,
677-
width=80,
678-
)
660+
required = [(key, opt) for key, opt in self._opts.items() if opt.is_required]
661+
optional = [
662+
(key, opt) for key, opt in self._opts.items() if not opt.is_required
663+
]
664+
665+
out = " usage:\n "
666+
for i, (key, opt) in enumerate(required + optional):
667+
contents = f"{key}={key.upper()}"
668+
if not opt.is_required:
669+
contents = f"[{contents}]"
670+
if i > 0:
671+
contents = "," + contents
672+
out += contents
673+
674+
sections = [("required", required), ("optional", optional)]
675+
676+
for section, opts in sections:
677+
if len(opts) == 0:
678+
continue
679+
out += f"\n\n {section} arguments:"
680+
for key, opt in opts:
681+
default = "" if opt.is_required else f", {opt.default}"
682+
out += f"\n {key}={key.upper()} ({get_type_name(opt.opt_type)}{default})"
683+
out += f"\n {opt.help}"
684+
685+
return out
679686

680687

681688
class InvalidRunConfigException(Exception):

0 commit comments

Comments
 (0)