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

[V2-UI] Move logging to Rust #6817

Merged
merged 32 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4092375
Move logging to rust
Nov 28, 2018
b058789
Fix up crate config
illicitonion Apr 2, 2019
9932a29
Remove bogus logging
illicitonion Apr 2, 2019
ddc5ef0
Some more bogus logging
Apr 2, 2019
4ff6b29
Update test expectation
illicitonion Apr 3, 2019
06317de
WIP
illicitonion Apr 3, 2019
555f3ca
Fix up bad rebase
illicitonion Apr 3, 2019
6a366c9
Fix tests
illicitonion Apr 4, 2019
7b4f5c0
SIGUSR2 outputs to pantsd log correctly
illicitonion Apr 4, 2019
85d93df
De-dupe log format, pull out test helper
illicitonion Apr 4, 2019
f1ac593
Fix rust fmt
illicitonion Apr 4, 2019
56ae211
Un-break test
illicitonion Apr 4, 2019
c4c8e74
Remove unused variable
illicitonion Apr 5, 2019
37631de
Fix py2 which doesn't have a kwarg here
illicitonion Apr 5, 2019
2b5b3ce
Review feedback
illicitonion Apr 5, 2019
77a92f1
Fix lint
illicitonion Apr 5, 2019
d315f97
Remove print that doesn't go anywhere
illicitonion Apr 5, 2019
7a03b51
Test debugging
illicitonion Apr 8, 2019
e09128e
More test debugging
Apr 8, 2019
796761b
Run test a few times
illicitonion Apr 8, 2019
75f00b2
Flush logs
illicitonion Apr 9, 2019
5cbb2f4
Restore travis yml
illicitonion Apr 9, 2019
695f169
Cleanup tempdirs
illicitonion Apr 9, 2019
bb86453
Misc comments
Apr 9, 2019
118840b
dup fd
illicitonion Apr 9, 2019
9d84495
Fix test expectations
illicitonion Apr 10, 2019
2a36fa0
Remove unused import
illicitonion Apr 10, 2019
3daa496
Fix rustfmt
illicitonion Apr 10, 2019
258b133
Fix test expectations
illicitonion Apr 10, 2019
12bc7cf
Fix one more test
illicitonion Apr 10, 2019
8b53076
Address last few comments
illicitonion Apr 10, 2019
db253a9
Fix typo
illicitonion Apr 10, 2019
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
13 changes: 9 additions & 4 deletions src/python/pants/base/exception_sink.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def reset_log_location(cls, new_log_location):
`cls._shared_error_fileobj`.
OS state:
- May create a new directory.
- Overwrites signal handlers for many fatal and non-fatal signals.
- Overwrites signal handlers for many fatal and non-fatal signals (but not SIGUSR2).

:raises: :class:`ExceptionSink.ExceptionSinkError` if the directory does not exist or is not
writable.
Expand Down Expand Up @@ -167,7 +167,11 @@ def reset_exiter(cls, exiter):
sys.excepthook = cls._log_unhandled_exception_and_exit

@classmethod
def reset_interactive_output_stream(cls, interactive_output_stream):
def reset_interactive_output_stream(
cls,
interactive_output_stream,
override_faulthandler_destination=True
):
"""
Class state:
- Overwrites `cls._interactive_output_stream`.
Expand All @@ -180,8 +184,9 @@ def reset_interactive_output_stream(cls, interactive_output_stream):
try:
# NB: mutate process-global state!
# This permits a non-fatal `kill -31 <pants pid>` for stacktrace retrieval.
faulthandler.register(signal.SIGUSR2, interactive_output_stream,
all_threads=True, chain=False)
if override_faulthandler_destination:
faulthandler.register(signal.SIGUSR2, interactive_output_stream,
all_threads=True, chain=False)
# NB: mutate the class variables!
cls._interactive_output_stream = interactive_output_stream
except ValueError:
Expand Down
7 changes: 7 additions & 0 deletions src/python/pants/base/exiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import logging
import sys
import traceback
from builtins import object

from future.utils import PY3
Expand Down Expand Up @@ -70,6 +71,12 @@ def exit(self, result=PANTS_SUCCEEDED_EXIT_CODE, msg=None, out=None):
except Exception as e:
# If the file is already closed, or any other error occurs, just log it and continue to
# exit.
if msg:
logger.warning("Encountered error when trying to log this message: {}".format(msg))
# In pantsd, this won't go anywhere, because there's really nowhere for us to log if we
# can't log :(
# Not in pantsd, this will end up in sys.stderr.
traceback.print_stack()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure where this traceback prints, could you confirm that it's to a good place?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For pantsd, it will probably print nowhere. For nopantsd it will print to sys.stderr. Given this is only logged when we've failed to write to out, I don't think there's a better behaviour we can offer...

logger.exception(e)
self._exit(result)

Expand Down
4 changes: 2 additions & 2 deletions src/python/pants/bin/local_pants_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ def create(cls, exiter, args, env, target_roots=None, daemon_graph_session=None,
options, build_config, options_bootstrapper = cls.parse_options(
args,
env,
True,
options_bootstrapper
setup_logging=True,
options_bootstrapper=options_bootstrapper,
)
global_options = options.for_global_scope()

Expand Down
10 changes: 10 additions & 0 deletions src/python/pants/bin/pants_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from pants.base.exception_sink import ExceptionSink
from pants.bin.remote_pants_runner import RemotePantsRunner
from pants.init.logging import init_rust_logger, setup_logging_to_stderr
from pants.option.options_bootstrapper import OptionsBootstrapper


Expand All @@ -31,6 +32,11 @@ def __init__(self, exiter, args=None, env=None, start_time=None):
self._env = env or os.environ
self._start_time = start_time

def _enable_rust_logging(self, global_bootstrap_options):
levelname = global_bootstrap_options.level.upper()
init_rust_logger(levelname)
blorente marked this conversation as resolved.
Show resolved Hide resolved
setup_logging_to_stderr(logging.getLogger(None), levelname)

def run(self):
# Register our exiter at the beginning of the run() method so that any code in this process from
# this point onwards will use that exiter in the case of a fatal error.
Expand All @@ -40,6 +46,10 @@ def run(self):
bootstrap_options = options_bootstrapper.bootstrap_options
global_bootstrap_options = bootstrap_options.for_global_scope()

# We enable Rust logging here,
# and everything before it will be routed through regular Python logging.
self._enable_rust_logging(global_bootstrap_options)

ExceptionSink.reset_should_print_backtrace_to_terminal(global_bootstrap_options.print_exception_stacktrace)
ExceptionSink.reset_log_location(global_bootstrap_options.pants_workdir)

Expand Down
14 changes: 0 additions & 14 deletions src/python/pants/bin/remote_pants_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,6 @@ def _trapped_signals(self, client):
with ExceptionSink.trapped_signals(signal_handler):
yield

def _setup_logging(self):
"""Sets up basic stdio logging for the thin client."""
log_level = logging.getLevelName(self._bootstrap_options.for_global_scope().level.upper())

formatter = logging.Formatter('%(levelname)s] %(message)s')
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(log_level)
handler.setFormatter(formatter)

root = logging.getLogger()
root.setLevel(log_level)
root.addHandler(handler)

@staticmethod
def _backoff(attempt):
"""Minimal backoff strategy for daemon restarts."""
Expand Down Expand Up @@ -200,6 +187,5 @@ def _maybe_launch_pantsd(self):
return PantsDaemon.Factory.maybe_launch(options_bootstrapper=self._options_bootstrapper)

def run(self, args=None):
self._setup_logging()
pantsd_handle = self._maybe_launch_pantsd()
self._run_pants_with_retry(pantsd_handle)
26 changes: 19 additions & 7 deletions src/python/pants/engine/native.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,6 @@ def call(cls, c, func, args):

return PyResult(is_throw, c.to_value(val))

@_extern_decl('void', ['ExternContext*', 'uint8_t', 'uint8_t*', 'uint64_t'])
def extern_log(self, context_handle, level, msg_ptr, msg_len):
"""Given a log level and utf8 message string, log it."""
msg = bytes(self._ffi.buffer(msg_ptr, msg_len)).decode('utf-8')
logger.log(level, msg)

@_extern_decl('TypeId', ['ExternContext*', 'Handle*'])
def extern_get_type_for(self, context_handle, val):
"""Return a representation of the object's type."""
Expand Down Expand Up @@ -628,7 +622,6 @@ def context(self):
def init_externs():
context = ExternContext(self.ffi, self.lib)
self.lib.externs_set(context._handle,
self.ffi_lib.extern_log,
logger.getEffectiveLevel(),
self.ffi_lib.extern_call,
self.ffi_lib.extern_generator_send,
Expand Down Expand Up @@ -679,6 +672,25 @@ def decompress_tarball(self, tarfile_path, dest_dir):
result = self.lib.decompress_tarball(tarfile_path, dest_dir)
return self.context.raise_or_return(result)

def init_rust_logging(self, level):
blorente marked this conversation as resolved.
Show resolved Hide resolved
return self.lib.init_logging(level)

def setup_pantsd_logger(self, log_file_path, level):
log_file_path = log_file_path.encode("utf-8")
result = self.lib.setup_pantsd_logger(log_file_path, level)
return self.context.raise_or_return(result)

def setup_stderr_logger(self, level):
return self.lib.setup_stderr_logger(level)

def write_log(self, msg, level, target):
msg = msg.encode("utf-8")
target = target.encode("utf-8")
return self.lib.write_log(msg, level, target)

def flush_log(self):
return self.lib.flush_log()

def match_path_globs(self, path_globs, paths):
path_globs = self.context.to_value(path_globs)
paths_buf = self.context.utf8_buf_buf(tuple(paths))
Expand Down
1 change: 1 addition & 0 deletions src/python/pants/init/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ python_library(
'3rdparty/python/twitter/commons:twitter.common.collections',
'src/python/pants/backend/python/subsystems',
'src/python/pants/base:build_environment',
'src/python/pants/base:exception_sink',
'src/python/pants/base:exceptions',
'src/python/pants/base:target_roots',
'src/python/pants/binaries',
Expand Down
108 changes: 72 additions & 36 deletions src/python/pants/init/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import logging
import os
import sys
import time
from collections import namedtuple
from logging import FileHandler, Formatter, StreamHandler
from logging import StreamHandler

from future.moves.http import client

from pants.base.exception_sink import ExceptionSink
from pants.engine.native import Native
from pants.util.dirutil import safe_mkdir


Expand Down Expand Up @@ -42,13 +43,71 @@ def _maybe_configure_extended_logging(logger):
_configure_requests_debug_logging()


def init_rust_logger(level):
native = Native()
levelno = get_numeric_level(level)
native.init_rust_logging(levelno)


def setup_logging_to_stderr(python_logger, level):
"""
We setup logging as loose as possible from the Python side,
and let Rust do the filtering.
"""
native = Native()
levelno = get_numeric_level(level)
handler = create_native_stderr_log_handler(levelno, native, stream=sys.stderr)
blorente marked this conversation as resolved.
Show resolved Hide resolved
python_logger.addHandler(handler)
# Let the rust side filter levels; try to have the python side send everything to the rust logger.
python_logger.setLevel("TRACE")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an attempt to tell python "Log everything", and let the rust side have control over filtering? If so, may be worth adding a comment to that effect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO? Could also live without this.



def setup_logging_from_options(bootstrap_options):
# N.B. quiet help says 'Squelches all console output apart from errors'.
level = 'ERROR' if bootstrap_options.quiet else bootstrap_options.level.upper()
return setup_logging(level, console_stream=sys.stderr, log_dir=bootstrap_options.logdir)
native = Native()
return setup_logging(level, console_stream=sys.stderr, log_dir=bootstrap_options.logdir, native=native)


class NativeHandler(StreamHandler):

def __init__(self, level, native=None, stream=None, native_filename=None):
super(NativeHandler, self).__init__(stream)
self.native = native
self.native_filename = native_filename
self.setLevel(level)

def emit(self, record):
self.native.write_log(self.format(record), record.levelno, "{}:pid={}".format(record.name, os.getpid()))

def flush(self):
self.native.flush_log()


def setup_logging(level, console_stream=None, log_dir=None, scope=None, log_name=None):
def create_native_pantsd_file_log_handler(level, native, native_filename):
fd = native.setup_pantsd_logger(native_filename, get_numeric_level(level))
ExceptionSink.reset_interactive_output_stream(os.fdopen(os.dup(fd), 'a'))
return NativeHandler(level, native, native_filename=native_filename)


def create_native_stderr_log_handler(level, native, stream=None):
try:
native.setup_stderr_logger(get_numeric_level(level))
except Exception as e:
print("Error setting up pantsd logger: {}".format(e), file=sys.stderr)
raise e

return NativeHandler(level, native, stream)


# TODO This function relies on logging._checkLevel, which is private.
# There is currently no good way to convert string levels to numeric values,
# but if there is ever one, it may be worth changing this.
def get_numeric_level(level):
return logging._checkLevel(level)


def setup_logging(level, console_stream=None, log_dir=None, scope=None, log_name=None, native=None):
"""Configures logging for a given scope, by default the global scope.

:param str level: The logging level to enable, must be one of the level names listed here:
Expand All @@ -63,6 +122,7 @@ def setup_logging(level, console_stream=None, log_dir=None, scope=None, log_name
The '.' separator providing the scope hierarchy. By default the root logger is
configured.
:param str log_name: The base name of the log file (defaults to 'pants.log').
:param Native native: An instance of the Native FFI lib, to register rust logging.
:returns: The full path to the main log file if file logging is configured or else `None`.
:rtype: str
"""
Expand All @@ -88,43 +148,19 @@ def trace(self, message, *args, **kwargs):
for handler in logger.handlers:
logger.removeHandler(handler)


if console_stream:
console_handler = StreamHandler(stream=console_stream)
console_handler.setFormatter(Formatter(fmt='%(levelname)s] %(message)s'))
console_handler.setLevel(level)
logger.addHandler(console_handler)
native_handler = create_native_stderr_log_handler(level, native, stream=console_stream)
logger.addHandler(native_handler)

if log_dir:
safe_mkdir(log_dir)
log_filename = os.path.join(log_dir, log_name or 'pants.log')
file_handler = FileHandler(log_filename)

class GlogFormatter(Formatter):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsirois @benjyw This PR is going to change log formats - How stable should we be aiming to keep them?

LEVEL_MAP = {
logging.FATAL: 'F',
logging.ERROR: 'E',
logging.WARN: 'W',
logging.INFO: 'I',
logging.DEBUG: 'D',
TRACE: 'T'
}

def format(self, record):
datetime = time.strftime('%m%d %H:%M:%S', time.localtime(record.created))
micros = int((record.created - int(record.created)) * 1e6)
return '{levelchar}{datetime}.{micros:06d} {process} {filename}:{lineno}] {msg}'.format(
levelchar=self.LEVEL_MAP[record.levelno],
datetime=datetime,
micros=micros,
process=record.process,
filename=record.filename,
lineno=record.lineno,
msg=record.getMessage()
)

file_handler.setFormatter(GlogFormatter())
file_handler.setLevel(level)
logger.addHandler(file_handler)

native_handler = create_native_pantsd_file_log_handler(level, native, log_filename)
file_handler = native_handler
logger.addHandler(native_handler)


logger.setLevel(level)

Expand Down
27 changes: 18 additions & 9 deletions src/python/pants/pantsd/pants_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from pants.bin.daemon_pants_runner import DaemonPantsRunner
from pants.engine.native import Native
from pants.init.engine_initializer import EngineInitializer
from pants.init.logging import setup_logging
from pants.init.logging import init_rust_logger, setup_logging
from pants.init.options_initializer import BuildConfigInitializer
from pants.option.arg_splitter import GLOBAL_SCOPE
from pants.option.options_bootstrapper import OptionsBootstrapper
Expand Down Expand Up @@ -306,7 +306,8 @@ def _pantsd_logging(self):
# for further forks.
with stdio_as(stdin_fd=-1, stdout_fd=-1, stderr_fd=-1):
# Reinitialize logging for the daemon context.
result = setup_logging(self._log_level, log_dir=self._log_dir, log_name=self.LOG_NAME)
init_rust_logger(self._log_level)
result = setup_logging(self._log_level, log_dir=self._log_dir, log_name=self.LOG_NAME, native=self._native)

# Do a python-level redirect of stdout/stderr, which will not disturb `0,1,2`.
# TODO: Consider giving these pipes/actual fds, in order to make them "deep" replacements
Expand All @@ -315,7 +316,7 @@ def _pantsd_logging(self):
sys.stderr = _LoggerStream(logging.getLogger(), logging.WARN, result.log_handler)

self._logger.debug('logging initialized')
yield result.log_handler.stream
yield (result.log_handler.stream, result.log_handler.native_filename)

def _setup_services(self, pants_services):
for service in pants_services.services:
Expand Down Expand Up @@ -369,22 +370,30 @@ def run_sync(self):
"""Synchronously run pantsd."""
# Switch log output to the daemon's log stream from here forward.
self._close_stdio()
with self._pantsd_logging() as log_stream:
with self._pantsd_logging() as (log_stream, log_filename):

# Register an exiter using os._exit to ensure we only close stdio streams once.
ExceptionSink.reset_exiter(Exiter(exiter=os._exit))

# We don't have any stdio streams to log to anymore, but we can get tracebacks of the pantsd
# process by tailing the pantsd log and sending it SIGUSR2.
ExceptionSink.reset_interactive_output_stream(log_stream)
# We don't have any stdio streams to log to anymore, so we log to a file.
# We don't override the faulthandler destination because the stream we get will proxy things
# via the rust logging code, and faulthandler needs to be writing directly to a real file
# descriptor. When pantsd logging was originally initialised, we already set up faulthandler
# to log to the correct file descriptor, so don't override it.
#
# We can get tracebacks of the pantsd process by tailing the pantsd log and sending it
# SIGUSR2.
ExceptionSink.reset_interactive_output_stream(
log_stream,
override_faulthandler_destination=False,
)

# Reset the log location and the backtrace preference from the global bootstrap options.
global_bootstrap_options = self._bootstrap_options.for_global_scope()
ExceptionSink.reset_should_print_backtrace_to_terminal(
global_bootstrap_options.print_exception_stacktrace)
ExceptionSink.reset_log_location(global_bootstrap_options.pants_workdir)

self._logger.info('pantsd starting, log level is {}'.format(self._log_level))

self._native.set_panic_handler()

# Set the process name in ps output to 'pantsd' vs './pants compile src/etc:: -ldebug'.
Expand Down
Loading