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
Fatal error logging followup fixes #6610
Changes from 8 commits
4dcc191
bd4be81
70b2bc8
fe62035
b2ea651
350c486
da762f1
6276262
fff15f0
da4eaca
d7f2d00
6056baa
640ec40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,9 @@ | |
import traceback | ||
from builtins import object, str | ||
|
||
import setproctitle | ||
|
||
from pants.base.build_environment import get_buildroot | ||
from pants.base.exiter import Exiter | ||
from pants.util.dirutil import safe_mkdir, safe_open | ||
from pants.util.meta import classproperty | ||
|
@@ -33,6 +36,10 @@ class ExceptionSink(object): | |
_exiter = None | ||
# Where to log stacktraces to in a SIGUSR2 handler. | ||
_interactive_output_stream = None | ||
# Copy of the applicable bootstrap option values for the current process. Used in error logs for | ||
# debuggability, and to determine whether to print the exception stacktrace to the terminal on | ||
# exit. | ||
_bootstrap_option_values = None | ||
|
||
# These persistent open file descriptors are kept so the signal handler can do almost no work | ||
# (and lets faulthandler figure out signal safety). | ||
|
@@ -70,6 +77,18 @@ def __new__(cls, *args, **kwargs): | |
|
||
class ExceptionSinkError(Exception): pass | ||
|
||
@classmethod | ||
def reset_bootstrap_options(cls, new_bootstrap_option_values): | ||
"""Set the bootstrap option values held by this singleton. | ||
|
||
The option values are used to provide more context in a fatal error log entry, and to determine | ||
whether to print the exception stacktrace to the terminal on exit. | ||
|
||
Class state: | ||
- Overwrites `cls._bootstrap_option_values`. | ||
""" | ||
cls._bootstrap_option_values = new_bootstrap_option_values | ||
|
||
# All reset_* methods are ~idempotent! | ||
@classmethod | ||
def reset_log_location(cls, new_log_location): | ||
|
@@ -237,16 +256,24 @@ def _iso_timestamp_for_now(cls): | |
# NB: This includes a trailing newline, but no leading newline. | ||
_EXCEPTION_LOG_FORMAT = """\ | ||
timestamp: {timestamp} | ||
args: {args} | ||
process title: {process_title} | ||
sys.argv: {args} | ||
bootstrap options: {bootstrap_options} | ||
pid: {pid} | ||
{message} | ||
""" | ||
|
||
@classmethod | ||
def _format_exception_message(cls, msg, pid): | ||
if cls._bootstrap_option_values: | ||
bootstrap_fmt = cls._bootstrap_option_values._debug_dump() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is going to be a bit too noisy probably? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It totally is, and this is why the testing has the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed! |
||
else: | ||
bootstrap_fmt = '<none>' | ||
return cls._EXCEPTION_LOG_FORMAT.format( | ||
timestamp=cls._iso_timestamp_for_now(), | ||
process_title=setproctitle.getproctitle(), | ||
args=sys.argv, | ||
bootstrap_options=bootstrap_fmt, | ||
pid=pid, | ||
message=msg) | ||
|
||
|
@@ -276,13 +303,28 @@ def _format_unhandled_exception_log(cls, exc, tb, add_newline, should_print_back | |
exception_message=exception_message, | ||
maybe_newline=maybe_newline) | ||
|
||
_EXIT_FAILURE_TERMINAL_MESSAGE_FORMAT = """\ | ||
timestamp: {timestamp} | ||
{terminal_msg} | ||
""" | ||
|
||
@classmethod | ||
def _exit_with_failure(cls, terminal_msg): | ||
formatted_terminal_msg = cls._EXIT_FAILURE_TERMINAL_MESSAGE_FORMAT.format( | ||
timestamp=cls._iso_timestamp_for_now(), | ||
terminal_msg=terminal_msg or '') | ||
# Exit with failure, printing a message to the terminal (or whatever the interactive stream is). | ||
cls._exiter.exit(result=cls.UNHANDLED_EXCEPTION_EXIT_CODE, | ||
msg=terminal_msg, | ||
msg=formatted_terminal_msg, | ||
out=cls._interactive_output_stream) | ||
|
||
@classproperty | ||
def _should_print_backtrace(cls): | ||
if cls._bootstrap_option_values: | ||
return cls._bootstrap_option_values.print_exception_stacktrace | ||
else: | ||
return True | ||
|
||
@classmethod | ||
def _log_unhandled_exception_and_exit(cls, exc_class=None, exc=None, tb=None, add_newline=False): | ||
"""A sys.excepthook implementation which logs the error and exits with failure.""" | ||
|
@@ -294,7 +336,7 @@ def _log_unhandled_exception_and_exit(cls, exc_class=None, exc=None, tb=None, ad | |
try: | ||
# Always output the unhandled exception details into a log file, including the traceback. | ||
exception_log_entry = cls._format_unhandled_exception_log(exc, tb, add_newline, | ||
should_print_backtrace=True) | ||
should_print_backtrace=True) | ||
cls.log_exception(exception_log_entry) | ||
except Exception as e: | ||
extra_err_msg = 'Additional error logging unhandled exception {}: {}'.format(exc, e) | ||
|
@@ -304,7 +346,7 @@ def _log_unhandled_exception_and_exit(cls, exc_class=None, exc=None, tb=None, ad | |
# Exiter's should_print_backtrace field). | ||
stderr_printed_error = cls._format_unhandled_exception_log( | ||
exc, tb, add_newline, | ||
should_print_backtrace=cls._exiter.should_print_backtrace) | ||
should_print_backtrace=cls._should_print_backtrace) | ||
if extra_err_msg: | ||
stderr_printed_error = '{}\n{}'.format(stderr_printed_error, extra_err_msg) | ||
cls._exit_with_failure(stderr_printed_error) | ||
|
@@ -339,19 +381,18 @@ def handle_signal_gracefully(cls, signum, frame): | |
|
||
# Format a message to be printed to the terminal or other interactive stream, if applicable. | ||
formatted_traceback_for_terminal = cls._format_traceback( | ||
tb, should_print_backtrace=cls._exiter.should_print_backtrace and bool(tb)) | ||
tb, should_print_backtrace=cls._should_print_backtrace and bool(tb)) | ||
terminal_log_entry = cls._CATCHABLE_SIGNAL_ERROR_LOG_FORMAT.format( | ||
signum=signum, | ||
formatted_traceback=formatted_traceback_for_terminal) | ||
cls._exit_with_failure(terminal_log_entry) | ||
|
||
|
||
|
||
# Setup global state such as signal handlers and sys.excepthook with probably-safe values at module | ||
# import time. | ||
# Sets fatal signal handlers with reasonable defaults to catch errors early in startup. | ||
ExceptionSink.reset_log_location(os.getcwd()) | ||
ExceptionSink.reset_log_location(os.path.join(get_buildroot(), '.pants.d')) | ||
# Sets except hook. | ||
ExceptionSink.reset_exiter(Exiter(print_backtraces=True)) | ||
ExceptionSink.reset_exiter(Exiter(exiter=sys.exit)) | ||
# Sets a SIGUSR2 handler. | ||
ExceptionSink.reset_interactive_output_stream(sys.stderr) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,9 @@ class OptionValueContainer(object): | |
def __init__(self): | ||
self._value_map = {} # key -> either raw value or RankedValue wrapping the raw value. | ||
|
||
def _debug_dump(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding this for a test is fine, but it shouldn't be called in other consuming code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be removed if we avoid keeping a copy of all the bootstrap values. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed! |
||
return '{}(<with value map = {!r}>)'.format(type(self).__name__, self._value_map) | ||
|
||
def get_explicit_keys(self): | ||
"""Returns the keys for any values that were set explicitly (via flag, config, or env var).""" | ||
ret = [] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than individually poking at fields of the bootstrap options, it would be good to capture the fields you need in set_bootstrap_options, and to have defaults for those fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Truly fantastic idea. Will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!