Skip to content

Commit e3569b0

Browse files
Configure logging level using --debug flag; add logging to bake command
1 parent 0ed9d73 commit e3569b0

File tree

7 files changed

+145
-53
lines changed

7 files changed

+145
-53
lines changed

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ VERSION_FILE := ./linodecli/version.py
1212
VERSION_MODULE_DOCSTRING ?= \"\"\"\nThe version of the Linode CLI.\n\"\"\"\n\n
1313
LINODE_CLI_VERSION ?= "0.0.0.dev"
1414

15+
BAKE_FLAGS := --debug
16+
1517
.PHONY: install
1618
install: check-prerequisites requirements build
1719
pip3 install --force dist/*.whl
@@ -21,7 +23,7 @@ bake: clean
2123
ifeq ($(SKIP_BAKE), 1)
2224
@echo Skipping bake stage
2325
else
24-
python3 -m linodecli bake ${SPEC} --skip-config
26+
python3 -m linodecli bake ${SPEC} --skip-config $(BAKE_FLAGS)
2527
cp data-3 linodecli/
2628
endif
2729

linodecli/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import argparse
7+
import logging
78
import os
89
import sys
910
from importlib.metadata import version
@@ -38,6 +39,11 @@
3839

3940
TEST_MODE = os.getenv("LINODE_CLI_TEST_MODE") == "1"
4041

42+
# Configure the `logging` package log level depending on the --debug flag.
43+
logging.basicConfig(
44+
level=logging.DEBUG if "--debug" in argv else logging.WARNING,
45+
)
46+
4147
# if any of these arguments are given, we don't need to prompt for configuration
4248
skip_config = (
4349
any(

linodecli/api_request.py

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import os
88
import sys
99
import time
10-
from typing import Any, Iterable, List, Optional
10+
from logging import getLogger
11+
from typing import Any, Dict, Iterable, List, Optional
1112

1213
import requests
1314
from packaging import version
@@ -23,6 +24,8 @@
2324
)
2425
from .helpers import handle_url_overrides
2526

27+
logger = getLogger(__name__)
28+
2629

2730
def get_all_pages(ctx, operation: OpenAPIOperation, args: List[str]):
2831
"""
@@ -87,13 +90,18 @@ def do_request(
8790

8891
# Print response debug info is requested
8992
if ctx.debug_request:
90-
_print_request_debug_info(method, url, headers, body)
93+
# Multiline log entries aren't ideal, we should consider
94+
# using single-line structured logging in the future.
95+
logger.debug(
96+
"\n"
97+
+ "\n".join(_format_request_for_log(method, url, headers, body))
98+
)
9199

92100
result = method(url, headers=headers, data=body, verify=API_CA_PATH)
93101

94102
# Print response debug info is requested
95103
if ctx.debug_request:
96-
_print_response_debug_info(result)
104+
logger.debug("\n" + "\n".join(_format_response_for_log(result)))
97105

98106
while _check_retry(result) and not ctx.no_retry and ctx.retry_count < 3:
99107
time.sleep(_get_retry_after(result.headers))
@@ -282,38 +290,61 @@ def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
282290
return json.dumps(_traverse_request_body(expanded_json))
283291

284292

285-
def _print_request_debug_info(method, url, headers, body):
293+
def _format_request_for_log(
294+
method: Any,
295+
url: str,
296+
headers: Dict[str, str],
297+
body: str,
298+
) -> List[str]:
286299
"""
287-
Prints debug info for an HTTP request
300+
Builds a debug output for the given request.
301+
302+
:param method: The HTTP method of the request.
303+
:param url: The URL of the request.
304+
:param headers: The headers of the request.
305+
:param body: The body of the request.
306+
307+
:returns: The lines of the generated debug output.
288308
"""
289-
print(f"> {method.__name__.upper()} {url}", file=sys.stderr)
309+
result = [f"> {method.__name__.upper()} {url}"]
310+
290311
for k, v in headers.items():
291312
# If this is the Authorization header, sanitize the token
292313
if k.lower() == "authorization":
293314
v = "Bearer " + "*" * 64
294-
print(f"> {k}: {v}", file=sys.stderr)
295-
print("> Body:", file=sys.stderr)
296-
print("> ", body or "", file=sys.stderr)
297-
print("> ", file=sys.stderr)
315+
316+
result.append(f"> {k}: {v}")
317+
318+
result.extend(["> Body:", f"> {body or ''}", f"> "])
319+
320+
return result
298321

299322

300-
def _print_response_debug_info(response):
323+
def _format_response_for_log(
324+
response: requests.Response,
325+
):
301326
"""
302-
Prints debug info for a response from requests
327+
Builds a debug output for the given request.
328+
329+
:param response: The HTTP response to format.
330+
331+
:returns: The lines of the generated debug output.
303332
"""
333+
304334
# these come back as ints, convert to HTTP version
305335
http_version = response.raw.version / 10
306336
body = response.content.decode("utf-8", errors="replace")
307337

308-
print(
309-
f"< HTTP/{http_version:.1f} {response.status_code} {response.reason}",
310-
file=sys.stderr,
311-
)
338+
result = [
339+
f"< HTTP/{http_version:.1f} {response.status_code} {response.reason}"
340+
]
341+
312342
for k, v in response.headers.items():
313-
print(f"< {k}: {v}", file=sys.stderr)
314-
print("< Body:", file=sys.stderr)
315-
print("< ", body or "", file=sys.stderr)
316-
print("< ", file=sys.stderr)
343+
result.append(f"< {k}: {v}")
344+
345+
result.extend(["< Body:", f"< {body or ''}", "< "])
346+
347+
return result
317348

318349

319350
def _attempt_warn_old_version(ctx, result):

linodecli/baked/operation.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import argparse
66
import glob
77
import json
8+
import logging
89
import platform
910
import re
1011
import sys
@@ -17,6 +18,7 @@
1718
import openapi3.paths
1819
from openapi3.paths import Operation, Parameter
1920

21+
from linodecli import logging
2022
from linodecli.baked.parsing import simplify_description
2123
from linodecli.baked.request import (
2224
OpenAPIFilteringRequest,
@@ -400,9 +402,10 @@ def __init__(
400402
self.docs_url = self._resolve_operation_docs_url(operation)
401403

402404
if self.docs_url is None:
403-
print(
404-
f"INFO: Could not resolve docs URL for {operation}",
405-
file=sys.stderr,
405+
logging.warning(
406+
"%s %s Could not resolve docs URL for operation",
407+
self.method.upper(),
408+
self.url_path,
406409
)
407410

408411
code_samples_ext = operation.extensions.get("code-samples")
@@ -426,6 +429,13 @@ def arg_routes(self) -> Dict[str, List[OpenAPIRequestArg]]:
426429
"""
427430
return self.request.attr_routes if self.request else []
428431

432+
@property
433+
def attrs(self):
434+
"""
435+
Return a list of attributes from the request schema
436+
"""
437+
return self.response_model.attrs if self.response_model else []
438+
429439
@staticmethod
430440
def _flatten_url_path(tag: str) -> str:
431441
"""

linodecli/cli.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323

2424
METHODS = ("get", "post", "put", "delete")
2525

26+
from logging import getLogger
27+
28+
logger = getLogger(__name__)
29+
2630

2731
class CLI: # pylint: disable=too-many-instance-attributes
2832
"""
@@ -54,6 +58,7 @@ def bake(self, spec_location: str):
5458
"""
5559

5660
try:
61+
logger.debug("Loading and parsing OpenAPI spec: %s", spec_location)
5762
spec = self._load_openapi_spec(spec_location)
5863
except Exception as e:
5964
print(f"Failed to load spec: {e}")
@@ -72,21 +77,60 @@ def bake(self, spec_location: str):
7277
command = path.extensions.get(ext["command"], "default")
7378
for m in METHODS:
7479
operation = getattr(path, m)
75-
if operation is None or ext["skip"] in operation.extensions:
80+
81+
if operation is None:
82+
continue
83+
84+
operation_log_fmt = f"{m.upper()} {path.path[-1]}"
85+
86+
logger.debug(
87+
"%s: Attempting to generate command for operation",
88+
operation_log_fmt,
89+
)
90+
91+
if ext["skip"] in operation.extensions:
92+
logger.debug(
93+
"%s: Skipping operation due to x-linode-cli-skip extension",
94+
operation_log_fmt,
95+
)
7696
continue
97+
7798
action = operation.extensions.get(
7899
ext["action"], operation.operationId
79100
)
80101
if not action:
102+
logger.warning(
103+
"%s: Skipping operation due to unresolvable action",
104+
operation_log_fmt,
105+
)
81106
continue
107+
82108
if isinstance(action, list):
83109
action = action[0]
110+
84111
if command not in self.ops:
85112
self.ops[command] = {}
86-
self.ops[command][action] = OpenAPIOperation(
113+
114+
operation = OpenAPIOperation(
87115
command, operation, m, path.parameters
88116
)
89117

118+
logger.debug(
119+
"%s %s: Successfully built command for operation: "
120+
"command='%s %s'; summary='%s'; paginated=%s; num_args=%s; num_attrs=%s",
121+
operation.method.upper(),
122+
operation.url_path,
123+
operation.command,
124+
operation.action,
125+
operation.summary.rstrip("."),
126+
operation.response_model
127+
and operation.response_model.is_paginated,
128+
len(operation.args),
129+
len(operation.attrs),
130+
)
131+
132+
self.ops[command][action] = operation
133+
90134
# hide the base_url from the spec away
91135
self.ops["_base_url"] = self.spec.servers[0].url
92136
self.ops["_spec_version"] = self.spec.info.version

linodecli/helpers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ def register_debug_arg(parser: ArgumentParser):
106106
ArgumentParser that may be shared across the CLI and plugins.
107107
"""
108108
parser.add_argument(
109-
"--debug", action="store_true", help="Enable verbose HTTP debug output."
109+
"--debug",
110+
action="store_true",
111+
help="Enable verbose debug logging, including displaying HTTP debug output and "
112+
"configuring the Python logging package level to DEBUG.",
110113
)
111114

112115

tests/unit/test_api_request.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ class TestAPIRequest:
2121
"""
2222

2323
def test_response_debug_info(self):
24-
stderr_buf = io.StringIO()
25-
2624
mock_response = SimpleNamespace(
2725
raw=SimpleNamespace(version=11.1),
2826
status_code=200,
@@ -31,34 +29,32 @@ def test_response_debug_info(self):
3129
content=b"cool body",
3230
)
3331

34-
with contextlib.redirect_stderr(stderr_buf):
35-
api_request._print_response_debug_info(mock_response)
32+
result = api_request._format_response_for_log(mock_response)
3633

37-
output = stderr_buf.getvalue()
38-
assert "< HTTP/1.1 200 OK" in output
39-
assert "< cool: test" in output
40-
assert "< Body:" in output
41-
assert "< cool body" in output
42-
assert "< " in output
34+
assert result == [
35+
"< HTTP/1.1 200 OK",
36+
"< cool: test",
37+
"< Body:",
38+
"< cool body",
39+
"< ",
40+
]
4341

4442
def test_request_debug_info(self):
45-
stderr_buf = io.StringIO()
46-
47-
with contextlib.redirect_stderr(stderr_buf):
48-
api_request._print_request_debug_info(
49-
SimpleNamespace(__name__="get"),
50-
"https://definitely.linode.com/",
51-
{"cool": "test", "Authorization": "sensitiveinfo"},
52-
"cool body",
53-
)
43+
result = api_request._format_request_for_log(
44+
SimpleNamespace(__name__="get"),
45+
"https://definitely.linode.com/",
46+
{"cool": "test", "Authorization": "sensitiveinfo"},
47+
"cool body",
48+
)
5449

55-
output = stderr_buf.getvalue()
56-
assert "> GET https://definitely.linode.com/" in output
57-
assert "> cool: test" in output
58-
assert f"> Authorization: Bearer {'*' * 64}" in output
59-
assert "> Body:" in output
60-
assert "> cool body" in output
61-
assert "> " in output
50+
assert result == [
51+
"> GET https://definitely.linode.com/",
52+
"> cool: test",
53+
f"> Authorization: Bearer {'*' * 64}",
54+
"> Body:",
55+
"> cool body",
56+
"> ",
57+
]
6258

6359
def test_build_request_body(self, mock_cli, create_operation):
6460
create_operation.allowed_defaults = ["region", "image"]

0 commit comments

Comments
 (0)