mirror of https://github.com/borgbackup/borg.git
Merge pull request #2527 from enkore/issue/2241.2
Fix remote logging and progress
This commit is contained in:
commit
781a41de50
|
@ -3815,15 +3815,15 @@ class Archiver:
|
||||||
""" turn on INFO level logging for args that imply that they will produce output """
|
""" turn on INFO level logging for args that imply that they will produce output """
|
||||||
# map of option name to name of logger for that option
|
# map of option name to name of logger for that option
|
||||||
option_logger = {
|
option_logger = {
|
||||||
'output_list': 'borg.output.list',
|
'output_list': 'borg.output.list',
|
||||||
'show_version': 'borg.output.show-version',
|
'show_version': 'borg.output.show-version',
|
||||||
'show_rc': 'borg.output.show-rc',
|
'show_rc': 'borg.output.show-rc',
|
||||||
'stats': 'borg.output.stats',
|
'stats': 'borg.output.stats',
|
||||||
'progress': 'borg.output.progress',
|
'progress': 'borg.output.progress',
|
||||||
}
|
}
|
||||||
for option, logger_name in option_logger.items():
|
for option, logger_name in option_logger.items():
|
||||||
if args.get(option, False):
|
option_set = args.get(option, False)
|
||||||
logging.getLogger(logger_name).setLevel('INFO')
|
logging.getLogger(logger_name).setLevel('INFO' if option_set else 'WARN')
|
||||||
|
|
||||||
def _setup_topic_debugging(self, args):
|
def _setup_topic_debugging(self, args):
|
||||||
"""Turn on DEBUG level logging for specified --debug-topics."""
|
"""Turn on DEBUG level logging for specified --debug-topics."""
|
||||||
|
@ -3839,8 +3839,10 @@ class Archiver:
|
||||||
# This works around http://bugs.python.org/issue9351
|
# This works around http://bugs.python.org/issue9351
|
||||||
func = getattr(args, 'func', None) or getattr(args, 'fallback_func')
|
func = getattr(args, 'func', None) or getattr(args, 'fallback_func')
|
||||||
# do not use loggers before this!
|
# do not use loggers before this!
|
||||||
setup_logging(level=args.log_level, is_serve=func == self.do_serve, json=args.log_json)
|
is_serve = func == self.do_serve
|
||||||
|
setup_logging(level=args.log_level, is_serve=is_serve, json=args.log_json)
|
||||||
self.log_json = args.log_json
|
self.log_json = args.log_json
|
||||||
|
args.progress |= is_serve
|
||||||
self._setup_implied_logging(vars(args))
|
self._setup_implied_logging(vars(args))
|
||||||
self._setup_topic_debugging(args)
|
self._setup_topic_debugging(args)
|
||||||
if args.show_version:
|
if args.show_version:
|
||||||
|
|
|
@ -1226,6 +1226,14 @@ class ProgressIndicatorBase:
|
||||||
if self.logger.level == logging.NOTSET:
|
if self.logger.level == logging.NOTSET:
|
||||||
self.logger.setLevel(logging.WARN)
|
self.logger.setLevel(logging.WARN)
|
||||||
self.logger.propagate = False
|
self.logger.propagate = False
|
||||||
|
|
||||||
|
# If --progress is not set then the progress logger level will be WARN
|
||||||
|
# due to setup_implied_logging (it may be NOTSET with a logging config file,
|
||||||
|
# but the interactions there are generally unclear), so self.emit becomes
|
||||||
|
# False, which is correct.
|
||||||
|
# If --progress is set then the level will be INFO as per setup_implied_logging;
|
||||||
|
# note that this is always the case for serve processes due to a "args.progress |= is_serve".
|
||||||
|
# In this case self.emit is True.
|
||||||
self.emit = self.logger.getEffectiveLevel() == logging.INFO
|
self.emit = self.logger.getEffectiveLevel() == logging.INFO
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
|
|
@ -88,15 +88,21 @@ def setup_logging(stream=None, conf_fname=None, env_var='BORG_LOGGING_CONF', lev
|
||||||
# if we did not / not successfully load a logging configuration, fallback to this:
|
# if we did not / not successfully load a logging configuration, fallback to this:
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
handler = logging.StreamHandler(stream)
|
handler = logging.StreamHandler(stream)
|
||||||
if is_serve:
|
if is_serve and not json:
|
||||||
fmt = '$LOG %(levelname)s %(name)s Remote: %(message)s'
|
fmt = '$LOG %(levelname)s %(name)s Remote: %(message)s'
|
||||||
else:
|
else:
|
||||||
fmt = '%(message)s'
|
fmt = '%(message)s'
|
||||||
formatter = JsonFormatter(fmt) if json and not is_serve else logging.Formatter(fmt)
|
formatter = JsonFormatter(fmt) if json else logging.Formatter(fmt)
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
borg_logger = logging.getLogger('borg')
|
borg_logger = logging.getLogger('borg')
|
||||||
borg_logger.formatter = formatter
|
borg_logger.formatter = formatter
|
||||||
borg_logger.json = json
|
borg_logger.json = json
|
||||||
|
if configured and logger.handlers:
|
||||||
|
# The RepositoryServer can call setup_logging a second time to adjust the output
|
||||||
|
# mode from text-ish is_serve to json is_serve.
|
||||||
|
# Thus, remove the previously installed handler, if any.
|
||||||
|
logger.handlers[0].close()
|
||||||
|
logger.handlers.clear()
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
logger.setLevel(level.upper())
|
logger.setLevel(level.upper())
|
||||||
configured = True
|
configured = True
|
||||||
|
@ -224,6 +230,8 @@ class JsonFormatter(logging.Formatter):
|
||||||
data = {
|
data = {
|
||||||
'type': 'log_message',
|
'type': 'log_message',
|
||||||
'time': record.created,
|
'time': record.created,
|
||||||
|
'message': '',
|
||||||
|
'levelname': 'CRITICAL',
|
||||||
}
|
}
|
||||||
for attr in self.RECORD_ATTRIBUTES:
|
for attr in self.RECORD_ATTRIBUTES:
|
||||||
value = getattr(record, attr, None)
|
value = getattr(record, attr, None)
|
||||||
|
|
|
@ -2,29 +2,30 @@ import errno
|
||||||
import fcntl
|
import fcntl
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
|
||||||
import textwrap
|
import textwrap
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
import msgpack
|
import msgpack
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .helpers import Error, IntegrityError
|
from .helpers import Error, IntegrityError
|
||||||
from .helpers import get_home_dir
|
|
||||||
from .helpers import sysinfo
|
|
||||||
from .helpers import bin_to_hex
|
from .helpers import bin_to_hex
|
||||||
from .helpers import replace_placeholders
|
from .helpers import get_home_dir
|
||||||
from .helpers import hostname_is_unique
|
from .helpers import hostname_is_unique
|
||||||
|
from .helpers import replace_placeholders
|
||||||
|
from .helpers import sysinfo
|
||||||
|
from .logger import create_logger, setup_logging
|
||||||
from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT
|
from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT
|
||||||
from .version import parse_version, format_version
|
from .version import parse_version, format_version
|
||||||
from .logger import create_logger
|
|
||||||
|
|
||||||
logger = create_logger(__name__)
|
logger = create_logger(__name__)
|
||||||
|
|
||||||
|
@ -312,6 +313,10 @@ class RepositoryServer: # pragma: no cover
|
||||||
# clients since 1.1.0b3 use a dict as client_data
|
# clients since 1.1.0b3 use a dict as client_data
|
||||||
if isinstance(client_data, dict):
|
if isinstance(client_data, dict):
|
||||||
self.client_version = client_data[b'client_version']
|
self.client_version = client_data[b'client_version']
|
||||||
|
if client_data.get(b'client_supports_log_v3', False):
|
||||||
|
level = logging.getLevelName(logging.getLogger('').level)
|
||||||
|
setup_logging(is_serve=True, json=True, level=level)
|
||||||
|
logger.debug('Initialized logging system for new (v3) protocol')
|
||||||
else:
|
else:
|
||||||
self.client_version = BORG_VERSION # seems to be newer than current version (no known old format)
|
self.client_version = BORG_VERSION # seems to be newer than current version (no known old format)
|
||||||
|
|
||||||
|
@ -555,7 +560,10 @@ class RemoteRepository:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
version = self.call('negotiate', {'client_data': {b'client_version': BORG_VERSION}})
|
version = self.call('negotiate', {'client_data': {
|
||||||
|
b'client_version': BORG_VERSION,
|
||||||
|
b'client_supports_log_v3': True,
|
||||||
|
}})
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
raise ConnectionClosedWithHint('Is borg working on the server?') from None
|
raise ConnectionClosedWithHint('Is borg working on the server?') from None
|
||||||
if version == RPC_PROTOCOL_VERSION:
|
if version == RPC_PROTOCOL_VERSION:
|
||||||
|
@ -646,12 +654,23 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
|
||||||
opts.append('--critical')
|
opts.append('--critical')
|
||||||
else:
|
else:
|
||||||
raise ValueError('log level missing, fix this code')
|
raise ValueError('log level missing, fix this code')
|
||||||
try:
|
|
||||||
borg_logger = logging.getLogger('borg')
|
# Tell the remote server about debug topics it may need to consider.
|
||||||
if borg_logger.json:
|
# Note that debug topics are usable for "spew" or "trace" logs which would
|
||||||
opts.append('--log-json')
|
# be too plentiful to transfer for normal use, so the server doesn't send
|
||||||
except AttributeError:
|
# them unless explicitly enabled.
|
||||||
pass
|
#
|
||||||
|
# Needless to say, if you do --debug-topic=repository.compaction, for example,
|
||||||
|
# with a 1.0.x server it won't work, because the server does not recognize the
|
||||||
|
# option.
|
||||||
|
#
|
||||||
|
# This is not considered a problem, since this is a debugging feature that
|
||||||
|
# should not be used for regular use.
|
||||||
|
for topic in args.debug_topics:
|
||||||
|
if '.' not in topic:
|
||||||
|
topic = 'borg.debug.' + topic
|
||||||
|
if 'repository' in topic:
|
||||||
|
opts.append('--debug-topic=%s' % topic)
|
||||||
env_vars = []
|
env_vars = []
|
||||||
if not hostname_is_unique():
|
if not hostname_is_unique():
|
||||||
env_vars.append('BORG_HOSTNAME_IS_UNIQUE=no')
|
env_vars.append('BORG_HOSTNAME_IS_UNIQUE=no')
|
||||||
|
@ -930,7 +949,63 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
|
||||||
|
|
||||||
|
|
||||||
def handle_remote_line(line):
|
def handle_remote_line(line):
|
||||||
if line.startswith('$LOG '):
|
"""
|
||||||
|
Handle a remote log line.
|
||||||
|
|
||||||
|
This function is remarkably complex because it handles multiple wire formats.
|
||||||
|
"""
|
||||||
|
if line.startswith('{'):
|
||||||
|
# This format is used by Borg since 1.1.0b6 for new-protocol clients.
|
||||||
|
# It is the same format that is exposed by --log-json.
|
||||||
|
msg = json.loads(line)
|
||||||
|
|
||||||
|
if msg['type'] not in ('progress_message', 'progress_percent', 'log_message'):
|
||||||
|
logger.warning('Dropped remote log message with unknown type %r: %s', msg['type'], line)
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg['type'] == 'log_message':
|
||||||
|
# Re-emit log messages on the same level as the remote to get correct log suppression and verbosity.
|
||||||
|
level = getattr(logging, msg['levelname'], logging.CRITICAL)
|
||||||
|
assert isinstance(level, int)
|
||||||
|
target_logger = logging.getLogger(msg['name'])
|
||||||
|
msg['message'] = 'Remote: ' + msg['message']
|
||||||
|
# In JSON mode, we manually check whether the log message should be propagated.
|
||||||
|
if logging.getLogger('borg').json and level >= target_logger.getEffectiveLevel():
|
||||||
|
sys.stderr.write(json.dumps(msg) + '\n')
|
||||||
|
else:
|
||||||
|
target_logger.log(level, '%s', msg['message'])
|
||||||
|
elif msg['type'].startswith('progress_'):
|
||||||
|
# Progress messages are a bit more complex.
|
||||||
|
# First of all, we check whether progress output is enabled. This is signalled
|
||||||
|
# through the effective level of the borg.output.progress logger
|
||||||
|
# (also see ProgressIndicatorBase in borg.helpers).
|
||||||
|
progress_logger = logging.getLogger('borg.output.progress')
|
||||||
|
if progress_logger.getEffectiveLevel() == logging.INFO:
|
||||||
|
# When progress output is enabled, we check whether the client is in
|
||||||
|
# --log-json mode, as signalled by the "json" attribute on the "borg" logger.
|
||||||
|
if logging.getLogger('borg').json:
|
||||||
|
# In --log-json mode we re-emit the progress JSON line as sent by the server,
|
||||||
|
# with the message, if any, prefixed with "Remote: ".
|
||||||
|
if 'message' in msg:
|
||||||
|
msg['message'] = 'Remote: ' + msg['message']
|
||||||
|
sys.stderr.write(json.dumps(msg) + '\n')
|
||||||
|
elif 'message' in msg:
|
||||||
|
# In text log mode we write only the message to stderr and terminate with \r
|
||||||
|
# (carriage return, i.e. move the write cursor back to the beginning of the line)
|
||||||
|
# so that the next message, progress or not, overwrites it. This mirrors the behaviour
|
||||||
|
# of local progress displays.
|
||||||
|
sys.stderr.write('Remote: ' + msg['message'] + '\r')
|
||||||
|
elif line.startswith('$LOG '):
|
||||||
|
# This format is used by Borg since 1.1.0b1.
|
||||||
|
# It prefixed log lines with $LOG as a marker, followed by the log level
|
||||||
|
# and optionally a logger name, then "Remote:" as a separator followed by the original
|
||||||
|
# message.
|
||||||
|
#
|
||||||
|
# It is the oldest format supported by these servers, so it was important to make
|
||||||
|
# it readable with older (1.0.x) clients.
|
||||||
|
#
|
||||||
|
# TODO: Remove this block (so it'll be handled by the "else:" below) with a Borg 1.1 RC.
|
||||||
|
# Also check whether client_supports_log_v3 should be removed.
|
||||||
_, level, msg = line.split(' ', 2)
|
_, level, msg = line.split(' ', 2)
|
||||||
level = getattr(logging, level, logging.CRITICAL) # str -> int
|
level = getattr(logging, level, logging.CRITICAL) # str -> int
|
||||||
if msg.startswith('Remote:'):
|
if msg.startswith('Remote:'):
|
||||||
|
@ -941,7 +1016,15 @@ def handle_remote_line(line):
|
||||||
logname, msg = msg.split(' ', 1)
|
logname, msg = msg.split(' ', 1)
|
||||||
logging.getLogger(logname).log(level, msg.rstrip())
|
logging.getLogger(logname).log(level, msg.rstrip())
|
||||||
else:
|
else:
|
||||||
sys.stderr.write('Remote: ' + line)
|
# Plain 1.0.x and older format - re-emit to stderr (mirroring what the 1.0.x
|
||||||
|
# client did) or as a generic log message.
|
||||||
|
# We don't know what priority the line had.
|
||||||
|
if logging.getLogger('borg').json:
|
||||||
|
logging.getLogger('').warning('Remote: ' + line.strip())
|
||||||
|
else:
|
||||||
|
# In non-JSON mode we circumvent logging to preserve carriage returns (\r)
|
||||||
|
# which are generated by remote progress displays.
|
||||||
|
sys.stderr.write('Remote: ' + line)
|
||||||
|
|
||||||
|
|
||||||
class RepositoryNoCache:
|
class RepositoryNoCache:
|
||||||
|
|
|
@ -714,6 +714,7 @@ class RemoteRepositoryTestCase(RepositoryTestCase):
|
||||||
class MockArgs:
|
class MockArgs:
|
||||||
remote_path = 'borg'
|
remote_path = 'borg'
|
||||||
umask = 0o077
|
umask = 0o077
|
||||||
|
debug_topics = []
|
||||||
|
|
||||||
assert self.repository.borg_cmd(None, testing=True) == [sys.executable, '-m', 'borg.archiver', 'serve']
|
assert self.repository.borg_cmd(None, testing=True) == [sys.executable, '-m', 'borg.archiver', 'serve']
|
||||||
args = MockArgs()
|
args = MockArgs()
|
||||||
|
@ -723,6 +724,9 @@ class RemoteRepositoryTestCase(RepositoryTestCase):
|
||||||
assert self.repository.borg_cmd(args, testing=False) == ['borg', 'serve', '--umask=077', '--info']
|
assert self.repository.borg_cmd(args, testing=False) == ['borg', 'serve', '--umask=077', '--info']
|
||||||
args.remote_path = 'borg-0.28.2'
|
args.remote_path = 'borg-0.28.2'
|
||||||
assert self.repository.borg_cmd(args, testing=False) == ['borg-0.28.2', 'serve', '--umask=077', '--info']
|
assert self.repository.borg_cmd(args, testing=False) == ['borg-0.28.2', 'serve', '--umask=077', '--info']
|
||||||
|
args.debug_topics = ['something_client_side', 'repository_compaction']
|
||||||
|
assert self.repository.borg_cmd(args, testing=False) == ['borg-0.28.2', 'serve', '--umask=077', '--info',
|
||||||
|
'--debug-topic=borg.debug.repository_compaction']
|
||||||
|
|
||||||
|
|
||||||
class RemoteLegacyFree(RepositoryTestCaseBase):
|
class RemoteLegacyFree(RepositoryTestCaseBase):
|
||||||
|
|
Loading…
Reference in New Issue