瀏覽代碼

fix --progress and logging in general for remote

Marian Beermann 8 年之前
父節點
當前提交
fc105b49b1
共有 5 個文件被更改,包括 100 次插入24 次删除
  1. 11 9
      src/borg/archiver.py
  2. 8 0
      src/borg/helpers.py
  3. 10 2
      src/borg/logger.py
  4. 70 13
      src/borg/remote.py
  5. 1 0
      src/borg/testsuite/repository.py

+ 11 - 9
src/borg/archiver.py

@@ -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',
-                'show_version': 'borg.output.show-version',
-                'show_rc': 'borg.output.show-rc',
-                'stats': 'borg.output.stats',
-                'progress': 'borg.output.progress',
-                }
+            'output_list': 'borg.output.list',
+            'show_version': 'borg.output.show-version',
+            'show_rc': 'borg.output.show-rc',
+            'stats': 'borg.output.stats',
+            '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):
-                logging.getLogger(logger_name).setLevel('INFO')
+            option_set = args.get(option, False)
+            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:

+ 8 - 0
src/borg/helpers.py

@@ -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):

+ 10 - 2
src/borg/logger.py

@@ -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)

+ 70 - 13
src/borg/remote.py

@@ -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,9 @@ 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']
+            level = logging.getLevelName(logging.getLogger('').level)
+            setup_logging(is_serve=True, json=True, level=level)
+            logger.debug('Initialized loggin system for new 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)
 
 
@@ -646,12 +650,6 @@ 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')
-                if borg_logger.json:
-                    opts.append('--log-json')
-            except AttributeError:
-                pass
         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 +928,58 @@ 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 three different 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'])
+            # We manually check whether the log message should be propagated
+            if level >= target_logger.getEffectiveLevel() and logging.getLogger('borg').json:
+                sys.stderr.write(line)
+            else:
+                target_logger.log(level, '%s', msg['message'])
+        elif msg['type'].startswith('progress_') and not msg.get('finished'):
+            # 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, then 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 directly re-emit the progress line as sent by the server.
+                    sys.stderr.write(line)
+                else:
+                    # 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(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.
         _, 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 +990,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:

+ 1 - 0
src/borg/testsuite/repository.py

@@ -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()