From d94d823aec4b7be0be83a7b9c2363ab16d46898f Mon Sep 17 00:00:00 2001 From: Szymon Basan <116343782+sbasan@users.noreply.github.com> Date: Fri, 3 Mar 2023 13:43:55 +0100 Subject: [PATCH] add process id and caller info to debug trace (#137) --- vmngclient/__init__.py | 40 +++++++++++++++++++++++++++++++++++++++- vmngclient/response.py | 20 ++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/vmngclient/__init__.py b/vmngclient/__init__.py index 0280eeefe..5a8fdaa04 100644 --- a/vmngclient/__init__.py +++ b/vmngclient/__init__.py @@ -1,14 +1,52 @@ import logging import logging.config +from functools import lru_cache from importlib import metadata +from importlib.machinery import PathFinder from os import environ from pathlib import Path -from typing import Final +from traceback import FrameSummary, StackSummary +from typing import Final, List import urllib3 + +def get_first_external_stack_frame(stack: StackSummary) -> FrameSummary: + """ + Get the first python frame + on the stack before entering vmngclient module + """ + for index, frame in enumerate(stack): + if is_file_in_package(frame.filename): + break + return stack[index - 1] + + +@lru_cache() +def is_file_in_package(fname: str) -> bool: + """ + Checks if filepath given by string + is part of vmngclient source code + """ + return Path(fname) in pkg_src_list + + +def list_package_sources() -> List[Path]: + """ + Creates a list containing paths to all python source files + for current package + """ + pkg_srcs: List[Path] = [] + if pkg_spec := PathFinder.find_spec(__package__): + if pkg_origin := pkg_spec.origin: + pkg_srcs = list(Path(pkg_origin).parent.glob("**/*.py")) + return pkg_srcs + + LOGGING_CONF_DIR: Final[str] = str(Path(__file__).parents[0] / "logging.conf") __version__ = metadata.version(__package__) +pkg_src_list = list_package_sources() + if environ.get("VMNGCLIENT_DEVEL") is not None: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) diff --git a/vmngclient/response.py b/vmngclient/response.py index 2d6699f21..4e7120338 100644 --- a/vmngclient/response.py +++ b/vmngclient/response.py @@ -1,11 +1,14 @@ +import multiprocessing from functools import wraps from pprint import pformat +from traceback import extract_stack from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union, cast from attr import define # type: ignore from requests import PreparedRequest, Request, Response from requests.exceptions import JSONDecodeError +from vmngclient import get_first_external_stack_frame from vmngclient.dataclasses import DataclassBase from vmngclient.typed_list import DataSequence from vmngclient.utils.creation_tools import create_dataclass @@ -20,6 +23,22 @@ class ErrorInfo(DataclassBase): code: str +def with_proc_info_header(method: Callable[..., str]) -> Callable[..., str]: + """ + Adds process ID and external caller information before first line of returned string + """ + + @wraps(method) + def wrapper(*args, **kwargs) -> str: + wrapped = method(*args, **kwargs) + fname, line_no, function, _ = get_first_external_stack_frame(extract_stack()) + external_caller_info = "%s:%d %s(...)" % (fname, line_no, function) + header = f"{multiprocessing.current_process()} {external_caller_info}\n" + return header + wrapped + + return wrapper + + def response_debug(response: Optional[Response], request: Union[Request, PreparedRequest, None]) -> str: """Returns human readable string containing Request-Response contents (helpful for debugging). @@ -67,6 +86,7 @@ def response_debug(response: Optional[Response], request: Union[Request, Prepare return pformat(debug_dict, width=80, sort_dicts=False) +@with_proc_info_header def response_history_debug(response: Optional[Response], request: Union[Request, PreparedRequest, None]) -> str: """Returns human readable string containing Request-Response history contents for given response.