Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include error and traceback in log file #3247

Merged
merged 2 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion tmt/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ def run_cli() -> None:
raise SystemExit(2) from error

except Exception as nested_error:
print(f"Error: failed while importing tmt package: {nested_error}", file=sys.stderr)
import traceback

print(f"Error: failed while reporting exception: {nested_error}", file=sys.stderr)
traceback.print_exc()

raise SystemExit(2) from nested_error


Expand Down
16 changes: 16 additions & 0 deletions tmt/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

import click

from tmt._compat.pathlib import Path
from tmt._compat.warnings import deprecated

if TYPE_CHECKING:
Expand Down Expand Up @@ -280,9 +281,14 @@ class LogRecordDetails:


class LogfileHandler(logging.FileHandler):
#: Paths of all log files to which ``LogfileHandler`` was attached.
emitting_to: list[Path] = []

def __init__(self, filepath: 'tmt.utils.Path') -> None:
super().__init__(filepath, mode='a')

LogfileHandler.emitting_to.append(filepath)


# ignore[type-arg]: StreamHandler is a generic type, but such expression would be incompatible
# with older Python versions. Since it's not critical to mark the handler as "str only", we can
Expand Down Expand Up @@ -498,6 +504,16 @@ def __repr__(self) -> str:
f' apply_colors_logging={self.apply_colors_logging}'
f'>')

@property
def apply_colors_output(self) -> bool:
return self._apply_colors_output

@apply_colors_output.setter
def apply_colors_output(self, value: bool) -> None:
self._apply_colors_output = value

self._decolorize_output = create_decolorizer(self._apply_colors_output)

@property
def labels_span(self) -> int:
""" Length of rendered labels """
Expand Down
88 changes: 78 additions & 10 deletions tmt/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
Generic,
Literal,
Optional,
TextIO,
TypeVar,
Union,
cast,
Expand Down Expand Up @@ -2420,6 +2421,29 @@ class FinishError(GeneralError):
""" Finish step error """


class TracebackVerbosity(enum.Enum):
""" Levels of logged traveback verbosity """

#: Render only exception and its causes.
DEFAULT = '0'
#: Render also call stack for exception and each of its causes.
VERBOSE = '1'
#: Render also call stack and local variables for exception and each of its causes.
FULL = 'full'

@classmethod
def from_spec(cls, spec: str) -> 'TracebackVerbosity':
try:
return TracebackVerbosity(spec)

except ValueError:
raise SpecificationError(f"Invalid traceback verbosity '{spec}'.")

@classmethod
def from_env(cls) -> 'TracebackVerbosity':
return TracebackVerbosity.from_spec(os.getenv('TMT_SHOW_TRACEBACK', '0').lower())


def render_run_exception_streams(
stdout: Optional[str],
stderr: Optional[str],
Expand Down Expand Up @@ -2458,7 +2482,9 @@ def render_run_exception(exception: RunError) -> Iterator[str]:
yield from render_run_exception_streams(exception.stdout, exception.stderr, verbose=verbose)


def render_exception_stack(exception: BaseException) -> Iterator[str]:
def render_exception_stack(
exception: BaseException,
traceback_verbosity: TracebackVerbosity = TracebackVerbosity.DEFAULT) -> Iterator[str]:
""" Render traceback of the given exception """

exception_traceback = traceback.TracebackException(
Expand All @@ -2480,7 +2506,7 @@ def render_exception_stack(exception: BaseException) -> Iterator[str]:
yield f'File {Y(frame.filename)}, line {Y(str(frame.lineno))}, in {Y(frame.name)}'
yield f' {B(frame.line)}'

if os.getenv('TMT_SHOW_TRACEBACK', '0').lower() == 'full' and frame.locals:
if traceback_verbosity is TracebackVerbosity.FULL and frame.locals:
yield ''

for k, v in frame.locals.items():
Expand All @@ -2489,7 +2515,9 @@ def render_exception_stack(exception: BaseException) -> Iterator[str]:
yield ''


def render_exception(exception: BaseException) -> Iterator[str]:
def render_exception(
exception: BaseException,
traceback_verbosity: TracebackVerbosity = TracebackVerbosity.DEFAULT) -> Iterator[str]:
""" Render the exception and its causes for printing """

def _indent(iterable: Iterable[str]) -> Iterator[str]:
Expand All @@ -2507,16 +2535,17 @@ def _indent(iterable: Iterable[str]) -> Iterator[str]:
yield ''
yield from render_run_exception(exception)

if os.getenv('TMT_SHOW_TRACEBACK', '0') != '0':
if traceback_verbosity is not TracebackVerbosity.DEFAULT:
yield ''
yield from _indent(render_exception_stack(exception))
yield from _indent(render_exception_stack(
exception, traceback_verbosity=traceback_verbosity))

# Follow the chain and render all causes
def _render_cause(number: int, cause: BaseException) -> Iterator[str]:
yield ''
yield f'Cause number {number}:'
yield ''
yield from _indent(render_exception(cause))
yield from _indent(render_exception(cause, traceback_verbosity=traceback_verbosity))

def _render_causes(causes: list[BaseException]) -> Iterator[str]:
yield ''
Expand All @@ -2537,13 +2566,52 @@ def _render_causes(causes: list[BaseException]) -> Iterator[str]:
yield from _render_causes(causes)


def show_exception(exception: BaseException) -> None:
""" Display the exception and its causes """
def show_exception(
exception: BaseException,
include_logfiles: bool = True) -> None:
"""
Display the exception and its causes.

:param exception: exception to log.
:param include_logfiles: if set, exception will be logged into known
logfiles as well as to standard error output.
"""

from tmt.cli import EXCEPTION_LOGGER

EXCEPTION_LOGGER.print('', file=sys.stderr)
EXCEPTION_LOGGER.print('\n'.join(render_exception(exception)), file=sys.stderr)
traceback_verbosity = TracebackVerbosity.from_env()

def _render_exception(traceback_verbosity: TracebackVerbosity) -> Iterator[str]:
yield ''
yield from render_exception(exception, traceback_verbosity=traceback_verbosity)

for line in _render_exception(traceback_verbosity):
EXCEPTION_LOGGER.print(line, file=sys.stderr)

if include_logfiles:
logger = EXCEPTION_LOGGER.clone()
logger.apply_colors_output = False

logfile_streams: list[TextIO] = []

with contextlib.ExitStack() as stack:
for path in tmt.log.LogfileHandler.emitting_to:
try:
# SIM115: all opened files are added on exit stack, and they
# will get collected and closed properly.
stream: TextIO = open(path, 'a') # noqa: SIM115

logfile_streams.append(stream)
stack.enter_context(stream)

except Exception as exc:
show_exception(
GeneralError(f"Cannot log error into logfile '{path}'.", causes=[exc]),
include_logfiles=False)

for line in _render_exception(traceback_verbosity=TracebackVerbosity.FULL):
for stream in logfile_streams:
logger.print(line, file=stream)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
Loading