Skip to content

Commit 6583a5f

Browse files
Configure Python logging level when debug flag is specified; add debug logs to bake command (#749)
Co-authored-by: Ye Chen <[email protected]>
1 parent a939ff5 commit 6583a5f

File tree

8 files changed

+114772
-60
lines changed

8 files changed

+114772
-60
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: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
import sys
99
import time
10+
from logging import getLogger
1011
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional
1112

1213
import requests
@@ -26,6 +27,8 @@
2627
if TYPE_CHECKING:
2728
from linodecli.cli import CLI
2829

30+
logger = getLogger(__name__)
31+
2932

3033
def get_all_pages(
3134
ctx: "CLI", operation: OpenAPIOperation, args: List[str]
@@ -103,13 +106,18 @@ def do_request(
103106

104107
# Print response debug info is requested
105108
if ctx.debug_request:
106-
_print_request_debug_info(method, url, headers, body)
109+
# Multiline log entries aren't ideal, we should consider
110+
# using single-line structured logging in the future.
111+
logger.debug(
112+
"\n%s",
113+
"\n".join(_format_request_for_log(method, url, headers, body)),
114+
)
107115

108116
result = method(url, headers=headers, data=body, verify=API_CA_PATH)
109117

110118
# Print response debug info is requested
111119
if ctx.debug_request:
112-
_print_response_debug_info(result)
120+
logger.debug("\n%s", "\n".join(_format_response_for_log(result)))
113121

114122
# Retry the request if necessary
115123
while _check_retry(result) and not ctx.no_retry and ctx.retry_count < 3:
@@ -362,48 +370,61 @@ def _build_request_body(
362370
return json.dumps(_traverse_request_body(expanded_json))
363371

364372

365-
def _print_request_debug_info(
366-
method: Any, url: str, headers: Dict[str, Any], body: Optional[str]
367-
) -> None:
373+
def _format_request_for_log(
374+
method: Any,
375+
url: str,
376+
headers: Dict[str, str],
377+
body: str,
378+
) -> List[str]:
368379
"""
369-
Prints debug info for an HTTP request.
380+
Builds a debug output for the given request.
370381
371-
:param method: An object with a `__name__` attribute representing
372-
the HTTP method (e.g., "get", "post").
373-
:param url: The full request URL.
374-
:param headers: A dictionary of request headers.
375-
:param body: The request body as a string, or None if no body exists.
382+
:param method: The HTTP method of the request.
383+
:param url: The URL of the request.
384+
:param headers: The headers of the request.
385+
:param body: The body of the request.
386+
387+
:returns: The lines of the generated debug output.
376388
"""
377-
print(f"> {method.__name__.upper()} {url}", file=sys.stderr)
389+
result = [f"> {method.__name__.upper()} {url}"]
390+
378391
for k, v in headers.items():
379392
# If this is the Authorization header, sanitize the token
380393
if k.lower() == "authorization":
381394
v = "Bearer " + "*" * 64
382-
print(f"> {k}: {v}", file=sys.stderr)
383-
print("> Body:", file=sys.stderr)
384-
print("> ", body or "", file=sys.stderr)
385-
print("> ", file=sys.stderr)
395+
396+
result.append(f"> {k}: {v}")
397+
398+
result.extend(["> Body:", f"> {body or ''}", "> "])
399+
400+
return result
386401

387402

388-
def _print_response_debug_info(response: Any) -> None:
403+
def _format_response_for_log(
404+
response: requests.Response,
405+
):
389406
"""
390-
Prints debug info for a response from requests.
407+
Builds a debug output for the given response.
391408
392-
:param response: The response object returned by a `requests` call.
409+
:param response: The HTTP response to format.
410+
411+
:returns: The lines of the generated debug output.
393412
"""
413+
394414
# these come back as ints, convert to HTTP version
395415
http_version = response.raw.version / 10
396416
body = response.content.decode("utf-8", errors="replace")
397417

398-
print(
399-
f"< HTTP/{http_version:.1f} {response.status_code} {response.reason}",
400-
file=sys.stderr,
401-
)
418+
result = [
419+
f"< HTTP/{http_version:.1f} {response.status_code} {response.reason}"
420+
]
421+
402422
for k, v in response.headers.items():
403-
print(f"< {k}: {v}", file=sys.stderr)
404-
print("< Body:", file=sys.stderr)
405-
print("< ", body or "", file=sys.stderr)
406-
print("< ", file=sys.stderr)
423+
result.append(f"< {k}: {v}")
424+
425+
result.extend(["< Body:", f"< {body or ''}", "< "])
426+
427+
return result
407428

408429

409430
def _attempt_warn_old_version(ctx: "CLI", result: Any) -> None:

linodecli/baked/operation.py

Lines changed: 12 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
@@ -400,9 +401,10 @@ def __init__(
400401
self.docs_url = self._resolve_operation_docs_url(operation)
401402

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

408410
code_samples_ext = operation.extensions.get("code-samples")
@@ -426,6 +428,13 @@ def arg_routes(self) -> Dict[str, List[OpenAPIRequestArg]]:
426428
"""
427429
return self.request.attr_routes if self.request else []
428430

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

linodecli/cli.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pickle
99
import sys
1010
from json import JSONDecodeError
11+
from logging import getLogger
1112
from sys import version_info
1213
from typing import IO, Any, ContextManager, Dict
1314

@@ -23,6 +24,8 @@
2324

2425
METHODS = ("get", "post", "put", "delete")
2526

27+
logger = getLogger(__name__)
28+
2629

2730
class CLI: # pylint: disable=too-many-instance-attributes
2831
"""
@@ -54,6 +57,7 @@ def bake(self, spec_location: str):
5457
"""
5558

5659
try:
60+
logger.debug("Loading and parsing OpenAPI spec: %s", spec_location)
5761
spec = self._load_openapi_spec(spec_location)
5862
except Exception as e:
5963
print(f"Failed to load spec: {e}")
@@ -72,21 +76,60 @@ def bake(self, spec_location: str):
7276
command = path.extensions.get(ext["command"], "default")
7377
for m in METHODS:
7478
operation = getattr(path, m)
75-
if operation is None or ext["skip"] in operation.extensions:
79+
80+
if operation is None:
81+
continue
82+
83+
operation_log_fmt = f"{m.upper()} {path.path[-1]}"
84+
85+
logger.debug(
86+
"%s: Attempting to generate command for operation",
87+
operation_log_fmt,
88+
)
89+
90+
if ext["skip"] in operation.extensions:
91+
logger.debug(
92+
"%s: Skipping operation due to x-linode-cli-skip extension",
93+
operation_log_fmt,
94+
)
7695
continue
96+
7797
action = operation.extensions.get(
7898
ext["action"], operation.operationId
7999
)
80100
if not action:
101+
logger.warning(
102+
"%s: Skipping operation due to unresolvable action",
103+
operation_log_fmt,
104+
)
81105
continue
106+
82107
if isinstance(action, list):
83108
action = action[0]
109+
84110
if command not in self.ops:
85111
self.ops[command] = {}
86-
self.ops[command][action] = OpenAPIOperation(
112+
113+
operation = OpenAPIOperation(
87114
command, operation, m, path.parameters
88115
)
89116

117+
logger.debug(
118+
"%s %s: Successfully built command for operation: "
119+
"command='%s %s'; summary='%s'; paginated=%s; num_args=%s; num_attrs=%s",
120+
operation.method.upper(),
121+
operation.url_path,
122+
operation.command,
123+
operation.action,
124+
operation.summary.rstrip("."),
125+
operation.response_model
126+
and operation.response_model.is_paginated,
127+
len(operation.args),
128+
len(operation.attrs),
129+
)
130+
131+
self.ops[command][action] = operation
132+
90133
# hide the base_url from the spec away
91134
self.ops["_base_url"] = self.spec.servers[0].url
92135
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

0 commit comments

Comments
 (0)