Skip to content

Commit 6151836

Browse files
authored
Boardwalk CLI: Use python logger; have debug logs (#53)
* start using a logger in place of click.echo * update format * use custom clickexception * update contrib doc * use individual loggers * add CLI debug mode * bump version * move log config for server
1 parent 81a13b1 commit 6151836

File tree

15 files changed

+204
-106
lines changed

15 files changed

+204
-106
lines changed

CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,12 @@ enforcement of test coverage.
5353

5454
The boardwalk pip module uses semantic versioning. Please make sure to update
5555
the VERSION file along with any changes to the package.
56+
57+
### Logging
58+
Most output should use a logger, with a few exceptions:
59+
- Raw output streamed from Ansible. Playbook runs should look familiar to
60+
Ansible users. Ansible output that has been processed by Boardwalk should be
61+
emitted by a logger.
62+
- Cases where the output of a command is intended to be consumed in a specific
63+
format, and the formatting features of a logger aren't useful. Examples
64+
include `boardwalk version` and `boardwalk workspace dump`.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.8.10
1+
0.8.11

src/boardwalk/ansible.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
from __future__ import annotations
55

66
import json
7+
import logging
78
import sys
89
from functools import partial
910
from pathlib import Path
1011
from typing import TYPE_CHECKING
1112

1213
import ansible_runner
13-
import click
14-
from click import ClickException
1514

1615
import boardwalk
16+
from boardwalk.app_exceptions import BoardwalkException
1717

1818
if TYPE_CHECKING:
1919
from typing import Any, Optional, TypedDict
@@ -61,6 +61,9 @@ class RunnerPlaybook(TypedDict, total=False):
6161
vars: dict[str, str | bool]
6262

6363

64+
logger = logging.getLogger(__name__)
65+
66+
6467
def ansible_runner_cancel_callback(ws: Workspace):
6568
"""
6669
ansible_runner needs a callback to tell it to stop execution. It returns
@@ -99,11 +102,11 @@ def ansible_runner_errors_to_output(runner: Runner, include_msg: bool = True) ->
99102
try:
100103
msg.append(event["event_data"]["res"]["msg"])
101104
except KeyError:
102-
click.echo("Warning: Event error did not contain msg")
105+
logger.warn("Event error did not contain msg")
103106
try:
104107
msg.append(event["stdout"])
105108
except KeyError:
106-
click.echo("Warning: Event error did not contain stdout")
109+
logger.warn("Event error did not contain stdout")
107110
output.append(": ".join(msg))
108111
return "\n".join(output)
109112

@@ -161,7 +164,7 @@ def ansible_runner_run_tasks(
161164
if limit:
162165
output_msg_prefix = f"{hosts}(limit: {limit}): ansible_runner invocation"
163166
output_msg = f"{output_msg_prefix}: {invocation_msg}"
164-
click.echo(output_msg)
167+
logger.info(output_msg)
165168
runner: Runner = ansible_runner.run(**runner_kwargs) # type: ignore
166169
runner_errors = ansible_runner_errors_to_output(runner)
167170
fail_msg = f"Error:\n{output_msg}\n{runner_errors}"
@@ -181,7 +184,7 @@ def ansible_runner_run_tasks(
181184

182185
def ansible_inventory() -> InventoryData:
183186
"""Uses ansible-inventory to fetch the inventory and returns it as a dict"""
184-
click.echo("Processing ansible-inventory")
187+
logger.info("Processing ansible-inventory")
185188
"""
186189
Note that for the moment we have --export set here. --export won't expand Jinja
187190
expressions and this is done for performance reasons. It's entirely possible
@@ -198,7 +201,7 @@ def ansible_inventory() -> InventoryData:
198201
suppress_env_files=True,
199202
)
200203
if rc != 0:
201-
ClickException(f"Failed to render inventory. {err}")
204+
BoardwalkException(f"Failed to render inventory. {err}")
202205

203206
return json.loads(out)
204207

src/boardwalk/app_exceptions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Common application exception classes
3+
"""
4+
import logging
5+
import typing
6+
7+
from click import ClickException
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class BoardwalkException(ClickException):
13+
"""
14+
click will handle subclasses of ClickException specially. We override the
15+
show method to make log formatting consistent
16+
"""
17+
18+
def show(self, file: typing.IO[str] | None = None) -> None:
19+
20+
logger.error(self.format_message())

src/boardwalk/cli.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
"""
44
from __future__ import annotations
55

6+
import logging
7+
import os
68
import signal
9+
import sys
10+
from distutils.util import strtobool
711
from importlib.metadata import version as lib_version
8-
from typing import TYPE_CHECKING
12+
from typing import Literal, TYPE_CHECKING
913

1014
import click
11-
from click import ClickException
15+
16+
from boardwalk.app_exceptions import BoardwalkException
1217

1318
from boardwalk.cli_catch import catch, release
1419
from boardwalk.cli_init import init
@@ -25,6 +30,8 @@
2530
if TYPE_CHECKING:
2631
from typing import Any
2732

33+
logger = logging.getLogger(__name__)
34+
2835
terminating = False
2936

3037

@@ -36,36 +43,68 @@ def handle_signal(sig: int, frame: Any):
3643
signals sent to child processes (ansible-playbook)
3744
"""
3845
global terminating
39-
click.echo(f"Received signal {sig}")
46+
logger.warn(f"Received signal {sig}")
4047
if not terminating:
4148
terminating = True
4249
raise KeyboardInterrupt
4350
else:
44-
click.echo("Boardwalk is already terminating")
51+
logger.warn("Boardwalk is already terminating")
4552

4653

4754
@click.group()
55+
@click.option(
56+
"--debug/--no-debug",
57+
"-D/-nD",
58+
help=(
59+
"Whether or not output debug messages. Alternatively may be set with"
60+
" the BOARDWALK_DEBUG=1 environment variable"
61+
),
62+
default=False,
63+
show_default=True,
64+
)
4865
@click.pass_context
49-
def cli(ctx: click.Context):
66+
def cli(ctx: click.Context, debug: bool | Literal[0, 1]):
5067
"""
5168
Boardwalk is a linear remote execution workflow engine built on top of Ansible.
5269
See the README.md @ https://github.com/Backblaze/boardwalk for more info
5370
5471
To see more info about any subcommand, do `boardwalk <subcommand> --help`
5572
"""
56-
# There's not much we can do without a Boardwalkfile.py. Print help and
57-
# exit if it's missing
73+
try:
74+
debug = strtobool(os.environ["BOARDWALK_DEBUG"])
75+
except KeyError:
76+
pass
77+
except ValueError:
78+
raise BoardwalkException(
79+
"BOARDWALK_DEBUG env variable has an invalid boolean value"
80+
)
81+
82+
if debug:
83+
loglevel = logging.DEBUG
84+
logformat = "%(levelname)s:%(name)s:%(threadName)s:%(message)s"
85+
else:
86+
loglevel = logging.INFO
87+
logformat = "%(levelname)s:%(name)s:%(message)s"
88+
89+
logging.basicConfig(
90+
format=logformat,
91+
handlers=[logging.StreamHandler(sys.stdout)],
92+
level=loglevel,
93+
)
94+
5895
signal.signal(signal.SIGINT, handle_signal)
5996
signal.signal(signal.SIGHUP, handle_signal)
6097
signal.signal(signal.SIGTERM, handle_signal)
6198
try:
6299
get_ws()
63100
except ManifestNotFound:
64-
# The version subcommand is the only one that doesn't need a Boardwalkfile.py
101+
# There's not much we can do without a Boardwalkfile.py. Print help and
102+
# exit if it's missing. The version subcommand is the only one that
103+
# doesn't need a Boardwalkfile.py
65104
if ctx.invoked_subcommand == "version":
66105
return
67106
click.echo(cli.get_help(ctx))
68-
raise ClickException("No Boardwalkfile.py found")
107+
raise BoardwalkException("No Boardwalkfile.py found")
69108
except NoActiveWorkspace:
70109
return
71110
except WorkspaceNotFound:

src/boardwalk/cli_catch.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"""
22
catch and release CLI subcommands
33
"""
4+
import logging
5+
46
import click
5-
from click import ClickException
67

8+
from boardwalk.app_exceptions import BoardwalkException
79
from boardwalk.manifest import get_ws, NoActiveWorkspace
810

11+
logger = logging.getLogger(__name__)
12+
913

1014
@click.command(
1115
"catch",
@@ -16,8 +20,8 @@ def catch():
1620
try:
1721
ws = get_ws()
1822
except NoActiveWorkspace as e:
19-
raise ClickException(e.message)
20-
click.echo(f"Using workspace: {ws.name}")
23+
raise BoardwalkException(e.message)
24+
logger.info(f"Using workspace: {ws.name}")
2125
ws.catch()
2226

2327

@@ -30,6 +34,6 @@ def release():
3034
try:
3135
ws = get_ws()
3236
except NoActiveWorkspace as e:
33-
raise ClickException(e.message)
34-
click.echo(f"Using workspace: {ws.name}")
37+
raise BoardwalkException(e.message)
38+
logger.info(f"Using workspace: {ws.name}")
3539
ws.release()

src/boardwalk/cli_init.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
"""
44
from __future__ import annotations
55

6+
import logging
7+
68
from pathlib import Path
79
from typing import TYPE_CHECKING, TypedDict
810

911
import click
10-
from click import ClickException
1112

1213
from boardwalk.ansible import (
1314
ansible_runner_run_tasks,
@@ -16,6 +17,7 @@
1617
AnsibleRunnerGeneralError,
1718
AnsibleRunnerUnreachableHost,
1819
)
20+
from boardwalk.app_exceptions import BoardwalkException
1921
from boardwalk.host import Host
2022
from boardwalk.manifest import get_ws, NoActiveWorkspace, Workspace
2123

@@ -34,6 +36,9 @@ class runnerKwargs(TypedDict, total=False):
3436
timeout: int
3537

3638

39+
logger = logging.getLogger(__name__)
40+
41+
3742
@click.command(short_help="Inits local workspace state by getting host facts")
3843
@click.option(
3944
"--limit",
@@ -58,13 +63,13 @@ def init(ctx: click.Context, limit: str, retry: bool):
5863
"""
5964
if retry and limit not in ["all", ""]:
6065
# We don't allow limit and retry to be specified together at the moment
61-
raise ClickException("--limit and --retry cannot be supplied together")
66+
raise BoardwalkException("--limit and --retry cannot be supplied together")
6267

6368
try:
6469
ws = get_ws()
6570
except NoActiveWorkspace as e:
66-
raise ClickException(e.message)
67-
click.echo(f"Using workspace: {ws.name}")
71+
raise BoardwalkException(e.message)
72+
logger.info(f"Using workspace: {ws.name}")
6873

6974
ws.assert_host_pattern_unchanged()
7075

@@ -84,7 +89,7 @@ def init(ctx: click.Context, limit: str, retry: bool):
8489
}
8590
if retry:
8691
if not retry_file_path.exists():
87-
raise ClickException("No retry file exists")
92+
raise BoardwalkException("No retry file exists")
8893
runner_kwargs["limit"] = f"@{str(retry_file_path)}"
8994

9095
# Save the host pattern we are initializing with. If the pattern changes after
@@ -109,10 +114,10 @@ def init(ctx: click.Context, limit: str, retry: bool):
109114
# we try to print out some debug info and bail
110115
for event in e.runner.events:
111116
try:
112-
click.echo(event["stdout"])
117+
logger.error(event["stdout"])
113118
except KeyError:
114119
pass
115-
raise ClickException("Failed to start fact gathering")
120+
raise BoardwalkException("Failed to start fact gathering")
116121

117122
# Clear the retry file after we use it to start fresh before we build a new one
118123
retry_file_path.unlink(missing_ok=True)
@@ -131,11 +136,11 @@ def init(ctx: click.Context, limit: str, retry: bool):
131136

132137
# Note if any hosts were unreachable
133138
if hosts_were_unreachable:
134-
click.echo("Some hosts were unreachable. Consider running again with --retry")
139+
logger.warn("Some hosts were unreachable. Consider running again with --retry")
135140

136141
# If we didn't find any hosts, raise an exception
137142
if len(ws.state.hosts) == 0:
138-
raise ClickException("No hosts gathered")
143+
raise BoardwalkException("No hosts gathered")
139144

140145

141146
def add_gathered_facts_to_state(event: RunnerEvent, ws: Workspace):
@@ -164,9 +169,9 @@ def handle_failed_init_hosts(event: RunnerEvent, retry_file_path: Path):
164169
event["event"] == "runner_on_unreachable"
165170
or event["event"] == "runner_on_failed"
166171
):
167-
click.echo(event["stdout"])
172+
logger.warn(event["stdout"])
168173
with open(retry_file_path, "a") as file:
169174
file.write(f"{event['event_data']['host']}\n")
170175
# If no hosts matched or there are warnings, write them out
171176
if event["event"] == "warning" or event["event"] == "playbook_on_no_hosts_matched":
172-
click.echo(event["stdout"])
177+
logger.warn(event["stdout"])

src/boardwalk/cli_login.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import click
77
from boardwalkd.protocol import Client
88

9-
from click import ClickException
10-
9+
from boardwalk.app_exceptions import BoardwalkException
1110
from boardwalk.manifest import get_boardwalkd_url
1211

1312

@@ -19,4 +18,4 @@ def login():
1918
try:
2019
asyncio.run(client.api_login())
2120
except ConnectionRefusedError:
22-
raise ClickException(f"Unable to reach {url}")
21+
raise BoardwalkException(f"Unable to reach {url}")

0 commit comments

Comments
 (0)