|
@@ -2,29 +2,30 @@ import errno
|
|
|
import fcntl
|
|
|
import functools
|
|
|
import inspect
|
|
|
+import json
|
|
|
import logging
|
|
|
import os
|
|
|
import select
|
|
|
import shlex
|
|
|
import sys
|
|
|
import tempfile
|
|
|
-import traceback
|
|
|
import textwrap
|
|
|
import time
|
|
|
+import traceback
|
|
|
from subprocess import Popen, PIPE
|
|
|
|
|
|
import msgpack
|
|
|
|
|
|
from . import __version__
|
|
|
from .helpers import Error, IntegrityError
|
|
|
-from .helpers import get_home_dir
|
|
|
-from .helpers import sysinfo
|
|
|
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 replace_placeholders
|
|
|
+from .helpers import sysinfo
|
|
|
+from .logger import create_logger, setup_logging
|
|
|
from .repository import Repository, MAX_OBJECT_SIZE, LIST_SCAN_LIMIT
|
|
|
from .version import parse_version, format_version
|
|
|
-from .logger import create_logger
|
|
|
|
|
|
logger = create_logger(__name__)
|
|
|
|
|
@@ -312,6 +313,10 @@ class RepositoryServer: # pragma: no cover
|
|
|
# clients since 1.1.0b3 use a dict as client_data
|
|
|
if isinstance(client_data, dict):
|
|
|
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:
|
|
|
self.client_version = BORG_VERSION # seems to be newer than current version (no known old format)
|
|
|
|
|
@@ -555,7 +560,10 @@ class RemoteRepository:
|
|
|
|
|
|
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:
|
|
|
raise ConnectionClosedWithHint('Is borg working on the server?') from None
|
|
|
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')
|
|
|
else:
|
|
|
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
|
|
|
+
|
|
|
+ # Tell the remote server about debug topics it may need to consider.
|
|
|
+ # Note that debug topics are usable for "spew" or "trace" logs which would
|
|
|
+ # be too plentiful to transfer for normal use, so the server doesn't send
|
|
|
+ # them unless explicitly enabled.
|
|
|
+ #
|
|
|
+ # 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 = []
|
|
|
if not hostname_is_unique():
|
|
|
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):
|
|
|
- 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 = getattr(logging, level, logging.CRITICAL) # str -> int
|
|
|
if msg.startswith('Remote:'):
|
|
@@ -941,7 +1016,15 @@ def handle_remote_line(line):
|
|
|
logname, msg = msg.split(' ', 1)
|
|
|
logging.getLogger(logname).log(level, msg.rstrip())
|
|
|
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:
|