Skip to content

Commit

Permalink
Include error and traceback in log file
Browse files Browse the repository at this point in the history
Always in full detail, traceback is added to log files tmt opened during
its run.
  • Loading branch information
happz committed Sep 30, 2024
1 parent 1e0cee2 commit 5023e4c
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 12 deletions.
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
17 changes: 16 additions & 1 deletion 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,13 @@ class LogRecordDetails:


class LogfileHandler(logging.FileHandler):
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 @@ -482,7 +487,7 @@ def __init__(
self.quiet = quiet
self.topics = topics or DEFAULT_TOPICS

self.apply_colors_output = apply_colors_output
self.apply_colors_output = self._apply_colors_output = apply_colors_output
self.apply_colors_logging = apply_colors_logging

self._decolorize_output = create_decolorizer(apply_colors_output)
Expand All @@ -498,6 +503,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 @@ -2399,6 +2400,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 @@ -2437,7 +2461,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 @@ -2459,7 +2485,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 @@ -2468,7 +2494,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 @@ -2486,16 +2514,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 @@ -2516,13 +2545,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

0 comments on commit 5023e4c

Please sign in to comment.