mirror of https://github.com/borgbackup/borg.git
Merge branch 'review-logging-levels'
This commit is contained in:
commit
5992d4be58
|
@ -37,8 +37,9 @@ has_lchflags = hasattr(os, 'lchflags')
|
||||||
|
|
||||||
class Archiver:
|
class Archiver:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, verbose=False):
|
||||||
self.exit_code = EXIT_SUCCESS
|
self.exit_code = EXIT_SUCCESS
|
||||||
|
self.verbose = verbose
|
||||||
|
|
||||||
def open_repository(self, location, create=False, exclusive=False):
|
def open_repository(self, location, create=False, exclusive=False):
|
||||||
if location.proto == 'ssh':
|
if location.proto == 'ssh':
|
||||||
|
@ -50,15 +51,21 @@ class Archiver:
|
||||||
|
|
||||||
def print_error(self, msg, *args):
|
def print_error(self, msg, *args):
|
||||||
msg = args and msg % args or msg
|
msg = args and msg % args or msg
|
||||||
self.exit_code = EXIT_WARNING # we do not terminate here, so it is a warning
|
self.exit_code = EXIT_ERROR
|
||||||
logger.error('borg: ' + msg)
|
logger.error(msg)
|
||||||
|
|
||||||
def print_verbose(self, msg, *args, **kw):
|
def print_warning(self, msg, *args):
|
||||||
|
msg = args and msg % args or msg
|
||||||
|
self.exit_code = EXIT_WARNING # we do not terminate here, so it is a warning
|
||||||
|
logger.warning(msg)
|
||||||
|
|
||||||
|
def print_info(self, msg, *args):
|
||||||
|
if self.verbose:
|
||||||
msg = args and msg % args or msg
|
msg = args and msg % args or msg
|
||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
|
|
||||||
def print_status(self, status, path):
|
def print_status(self, status, path):
|
||||||
logger.info("%1s %s", status, remove_surrogates(path))
|
self.print_info("%1s %s", status, remove_surrogates(path))
|
||||||
|
|
||||||
def do_serve(self, args):
|
def do_serve(self, args):
|
||||||
"""Start in server mode. This command is usually not used manually.
|
"""Start in server mode. This command is usually not used manually.
|
||||||
|
@ -82,14 +89,14 @@ class Archiver:
|
||||||
repository = self.open_repository(args.repository, exclusive=args.repair)
|
repository = self.open_repository(args.repository, exclusive=args.repair)
|
||||||
if args.repair:
|
if args.repair:
|
||||||
while not os.environ.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
|
while not os.environ.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
|
||||||
self.print_error("""Warning: 'check --repair' is an experimental feature that might result
|
self.print_warning("""'check --repair' is an experimental feature that might result
|
||||||
in data loss.
|
in data loss.
|
||||||
|
|
||||||
Type "Yes I am sure" if you understand this and want to continue.\n""")
|
Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
if input('Do you want to continue? ') == 'Yes I am sure':
|
if input('Do you want to continue? ') == 'Yes I am sure':
|
||||||
break
|
break
|
||||||
if not args.archives_only:
|
if not args.archives_only:
|
||||||
logger.warning('Starting repository check...')
|
logger.info('Starting repository check...')
|
||||||
if repository.check(repair=args.repair):
|
if repository.check(repair=args.repair):
|
||||||
logger.info('Repository check complete, no problems found.')
|
logger.info('Repository check complete, no problems found.')
|
||||||
else:
|
else:
|
||||||
|
@ -144,7 +151,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
try:
|
try:
|
||||||
status = archive.process_stdin(path, cache)
|
status = archive.process_stdin(path, cache)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
self.print_error('%s: %s', path, e)
|
status = 'E'
|
||||||
|
self.print_warning('%s: %s', path, e)
|
||||||
else:
|
else:
|
||||||
status = '-'
|
status = '-'
|
||||||
self.print_status(status, path)
|
self.print_status(status, path)
|
||||||
|
@ -154,7 +162,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
try:
|
try:
|
||||||
restrict_dev = os.lstat(path).st_dev
|
restrict_dev = os.lstat(path).st_dev
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.print_error('%s: %s', path, e)
|
self.print_warning('%s: %s', path, e)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
restrict_dev = None
|
restrict_dev = None
|
||||||
|
@ -181,7 +189,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
try:
|
try:
|
||||||
st = os.lstat(path)
|
st = os.lstat(path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.print_error('%s: %s', path, e)
|
self.print_warning('%s: %s', path, e)
|
||||||
return
|
return
|
||||||
if (st.st_ino, st.st_dev) in skip_inodes:
|
if (st.st_ino, st.st_dev) in skip_inodes:
|
||||||
return
|
return
|
||||||
|
@ -198,7 +206,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
try:
|
try:
|
||||||
status = archive.process_file(path, st, cache)
|
status = archive.process_file(path, st, cache)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
self.print_error('%s: %s', path, e)
|
status = 'E'
|
||||||
|
self.print_warning('%s: %s', path, e)
|
||||||
elif stat.S_ISDIR(st.st_mode):
|
elif stat.S_ISDIR(st.st_mode):
|
||||||
if exclude_caches and is_cachedir(path):
|
if exclude_caches and is_cachedir(path):
|
||||||
return
|
return
|
||||||
|
@ -207,7 +216,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
try:
|
try:
|
||||||
entries = os.listdir(path)
|
entries = os.listdir(path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.print_error('%s: %s', path, e)
|
status = 'E'
|
||||||
|
self.print_warning('%s: %s', path, e)
|
||||||
else:
|
else:
|
||||||
for filename in sorted(entries):
|
for filename in sorted(entries):
|
||||||
entry_path = os.path.normpath(os.path.join(path, filename))
|
entry_path = os.path.normpath(os.path.join(path, filename))
|
||||||
|
@ -227,7 +237,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
# Ignore unix sockets
|
# Ignore unix sockets
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self.print_error('Unknown file type: %s', path)
|
self.print_warning('Unknown file type: %s', path)
|
||||||
return
|
return
|
||||||
# Status output
|
# Status output
|
||||||
# A lowercase character means a file type other than a regular file,
|
# A lowercase character means a file type other than a regular file,
|
||||||
|
@ -270,7 +280,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
if not args.dry_run:
|
if not args.dry_run:
|
||||||
while dirs and not item[b'path'].startswith(dirs[-1][b'path']):
|
while dirs and not item[b'path'].startswith(dirs[-1][b'path']):
|
||||||
archive.extract_item(dirs.pop(-1), stdout=stdout)
|
archive.extract_item(dirs.pop(-1), stdout=stdout)
|
||||||
self.print_verbose(remove_surrogates(orig_path))
|
self.print_info(remove_surrogates(orig_path))
|
||||||
try:
|
try:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
archive.extract_item(item, dry_run=True)
|
archive.extract_item(item, dry_run=True)
|
||||||
|
@ -281,14 +291,14 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
else:
|
else:
|
||||||
archive.extract_item(item, stdout=stdout, sparse=sparse)
|
archive.extract_item(item, stdout=stdout, sparse=sparse)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
self.print_error('%s: %s', remove_surrogates(orig_path), e)
|
self.print_warning('%s: %s', remove_surrogates(orig_path), e)
|
||||||
|
|
||||||
if not args.dry_run:
|
if not args.dry_run:
|
||||||
while dirs:
|
while dirs:
|
||||||
archive.extract_item(dirs.pop(-1))
|
archive.extract_item(dirs.pop(-1))
|
||||||
for pattern in (patterns or []):
|
for pattern in (patterns or []):
|
||||||
if isinstance(pattern, IncludePattern) and pattern.match_count == 0:
|
if isinstance(pattern, IncludePattern) and pattern.match_count == 0:
|
||||||
self.print_error("Warning: Include pattern '%s' never matched.", pattern)
|
self.print_warning("Include pattern '%s' never matched.", pattern)
|
||||||
return self.exit_code
|
return self.exit_code
|
||||||
|
|
||||||
def do_rename(self, args):
|
def do_rename(self, args):
|
||||||
|
@ -340,7 +350,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
try:
|
try:
|
||||||
from .fuse import FuseOperations
|
from .fuse import FuseOperations
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
self.print_error('loading fuse support failed [ImportError: %s]' % str(e))
|
self.print_error('Loading fuse support failed [ImportError: %s]' % str(e))
|
||||||
return self.exit_code
|
return self.exit_code
|
||||||
|
|
||||||
if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
|
if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
|
||||||
|
@ -355,7 +365,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
else:
|
else:
|
||||||
archive = None
|
archive = None
|
||||||
operations = FuseOperations(key, repository, manifest, archive)
|
operations = FuseOperations(key, repository, manifest, archive)
|
||||||
self.print_verbose("Mounting filesystem")
|
self.print_info("Mounting filesystem")
|
||||||
try:
|
try:
|
||||||
operations.mount(args.mountpoint, args.options, args.foreground)
|
operations.mount(args.mountpoint, args.options, args.foreground)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
@ -435,7 +445,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
|
if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
|
||||||
self.print_error('At least one of the "within", "keep-hourly", "keep-daily", "keep-weekly", '
|
self.print_error('At least one of the "within", "keep-hourly", "keep-daily", "keep-weekly", '
|
||||||
'"keep-monthly" or "keep-yearly" settings must be specified')
|
'"keep-monthly" or "keep-yearly" settings must be specified')
|
||||||
return EXIT_ERROR
|
return self.exit_code
|
||||||
if args.prefix:
|
if args.prefix:
|
||||||
archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
|
archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
|
||||||
keep = []
|
keep = []
|
||||||
|
@ -456,12 +466,12 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
to_delete = [a for a in archives if a not in keep]
|
to_delete = [a for a in archives if a not in keep]
|
||||||
stats = Statistics()
|
stats = Statistics()
|
||||||
for archive in keep:
|
for archive in keep:
|
||||||
self.print_verbose('Keeping archive: %s' % format_archive(archive))
|
self.print_info('Keeping archive: %s' % format_archive(archive))
|
||||||
for archive in to_delete:
|
for archive in to_delete:
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
self.print_verbose('Would prune: %s' % format_archive(archive))
|
self.print_info('Would prune: %s' % format_archive(archive))
|
||||||
else:
|
else:
|
||||||
self.print_verbose('Pruning archive: %s' % format_archive(archive))
|
self.print_info('Pruning archive: %s' % format_archive(archive))
|
||||||
Archive(repository, key, manifest, archive.name, cache).delete(stats)
|
Archive(repository, key, manifest, archive.name, cache).delete(stats)
|
||||||
if to_delete and not args.dry_run:
|
if to_delete and not args.dry_run:
|
||||||
manifest.write()
|
manifest.write()
|
||||||
|
@ -559,8 +569,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
|
|
||||||
def build_parser(self, args=None, prog=None):
|
def build_parser(self, args=None, prog=None):
|
||||||
common_parser = argparse.ArgumentParser(add_help=False, prog=prog)
|
common_parser = argparse.ArgumentParser(add_help=False, prog=prog)
|
||||||
common_parser.add_argument('-v', '--verbose', dest='verbose', action='count',
|
common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', default=False,
|
||||||
help='verbose output, defaults to warnings only')
|
help='verbose output')
|
||||||
common_parser.add_argument('--no-files-cache', dest='cache_files', action='store_false',
|
common_parser.add_argument('--no-files-cache', dest='cache_files', action='store_false',
|
||||||
help='do not load/update the file metadata cache used to detect unchanged files')
|
help='do not load/update the file metadata cache used to detect unchanged files')
|
||||||
common_parser.add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=RemoteRepository.umask, metavar='M',
|
common_parser.add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=RemoteRepository.umask, metavar='M',
|
||||||
|
@ -999,7 +1009,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
||||||
parser = self.build_parser(args)
|
parser = self.build_parser(args)
|
||||||
|
|
||||||
args = parser.parse_args(args or ['-h'])
|
args = parser.parse_args(args or ['-h'])
|
||||||
setup_logging(args)
|
self.verbose = args.verbose
|
||||||
|
setup_logging()
|
||||||
os.umask(args.umask)
|
os.umask(args.umask)
|
||||||
RemoteRepository.remote_path = args.remote_path
|
RemoteRepository.remote_path = args.remote_path
|
||||||
RemoteRepository.umask = args.umask
|
RemoteRepository.umask = args.umask
|
||||||
|
@ -1018,7 +1029,7 @@ def sig_info_handler(signum, stack): # pragma: no cover
|
||||||
total = loc['st'].st_size
|
total = loc['st'].st_size
|
||||||
except Exception:
|
except Exception:
|
||||||
pos, total = 0, 0
|
pos, total = 0, 0
|
||||||
logger.warning("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total)))
|
logger.info("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total)))
|
||||||
break
|
break
|
||||||
if func in ('extract_item', ): # extract op
|
if func in ('extract_item', ): # extract op
|
||||||
path = loc['item'][b'path']
|
path = loc['item'][b'path']
|
||||||
|
@ -1026,7 +1037,7 @@ def sig_info_handler(signum, stack): # pragma: no cover
|
||||||
pos = loc['fd'].tell()
|
pos = loc['fd'].tell()
|
||||||
except Exception:
|
except Exception:
|
||||||
pos = 0
|
pos = 0
|
||||||
logger.warning("{0} {1}/???".format(path, format_file_size(pos)))
|
logger.info("{0} {1}/???".format(path, format_file_size(pos)))
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
@ -1072,7 +1083,8 @@ def main(): # pragma: no cover
|
||||||
elif exit_code == EXIT_ERROR:
|
elif exit_code == EXIT_ERROR:
|
||||||
logger.error(exit_msg % ('error', exit_code))
|
logger.error(exit_msg % ('error', exit_code))
|
||||||
else:
|
else:
|
||||||
logger.error(exit_msg % ('abnormal', exit_code))
|
# if you see 666 in output, it usually means exit_code was None
|
||||||
|
logger.error(exit_msg % ('abnormal', exit_code or 666))
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,9 @@ The way to use this is as follows:
|
||||||
* then each module uses logger.info/warning/debug/etc according to the
|
* then each module uses logger.info/warning/debug/etc according to the
|
||||||
level it believes is appropriate:
|
level it believes is appropriate:
|
||||||
|
|
||||||
logger.debug('some intricate details you usually do not care about')
|
logger.debug('debugging info for developers or power users')
|
||||||
logger.info('verbose progress information')
|
logger.info('normal, informational output')
|
||||||
logger.warning('some non-error condition that must always be reported')
|
logger.warning('warn about a non-fatal error or sth else')
|
||||||
logger.error('a fatal error')
|
logger.error('a fatal error')
|
||||||
|
|
||||||
... and so on. see the `logging documentation
|
... and so on. see the `logging documentation
|
||||||
|
@ -26,9 +26,8 @@ The way to use this is as follows:
|
||||||
stream it is using, unfortunately. we assume that it won't clutter
|
stream it is using, unfortunately. we assume that it won't clutter
|
||||||
stdout, because interaction would be broken then anyways
|
stdout, because interaction would be broken then anyways
|
||||||
|
|
||||||
* advanced verbosity filters, based on what i described in
|
* what is output on INFO level is additionally controlled by commandline
|
||||||
https://github.com/borgbackup/borg/pull/233#issuecomment-145100222
|
flags
|
||||||
may eventually be implemented
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -36,11 +35,11 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(args, stream=None):
|
def setup_logging(stream=None):
|
||||||
"""setup logging module according to the arguments provided
|
"""setup logging module according to the arguments provided
|
||||||
|
|
||||||
this sets up a stream handler logger on stderr (by default, if no
|
this sets up a stream handler logger on stderr (by default, if no
|
||||||
stream is provided) and verbosity levels.
|
stream is provided).
|
||||||
"""
|
"""
|
||||||
logging.raiseExceptions = False
|
logging.raiseExceptions = False
|
||||||
l = logging.getLogger('')
|
l = logging.getLogger('')
|
||||||
|
@ -50,13 +49,8 @@ def setup_logging(args, stream=None):
|
||||||
# example:
|
# example:
|
||||||
# sh.setFormatter(logging.Formatter('%(name)s: %(message)s'))
|
# sh.setFormatter(logging.Formatter('%(name)s: %(message)s'))
|
||||||
l.addHandler(sh)
|
l.addHandler(sh)
|
||||||
levels = {None: logging.WARNING,
|
l.setLevel(logging.INFO)
|
||||||
0: logging.WARNING,
|
return sh
|
||||||
1: logging.INFO,
|
|
||||||
2: logging.DEBUG}
|
|
||||||
# default to WARNING, -v goes to INFO and -vv to DEBUG
|
|
||||||
l.setLevel(levels[args.verbose])
|
|
||||||
return sh,
|
|
||||||
|
|
||||||
|
|
||||||
def find_parent_module():
|
def find_parent_module():
|
||||||
|
|
|
@ -282,9 +282,20 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
shutil.rmtree(self.cache_path)
|
shutil.rmtree(self.cache_path)
|
||||||
with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='1'):
|
with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='1'):
|
||||||
info_output2 = self.cmd('info', self.repository_location + '::test')
|
info_output2 = self.cmd('info', self.repository_location + '::test')
|
||||||
# info_output2 starts with some "initializing cache" text but should
|
|
||||||
# end the same way as info_output
|
def filter(output):
|
||||||
assert info_output2.endswith(info_output)
|
# filter for interesting "info" output, ignore cache rebuilding related stuff
|
||||||
|
prefixes = ['Name:', 'Fingerprint:', 'Number of files:', 'This archive:',
|
||||||
|
'All archives:', 'Chunk index:', ]
|
||||||
|
result = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
for prefix in prefixes:
|
||||||
|
if line.startswith(prefix):
|
||||||
|
result.append(line)
|
||||||
|
return '\n'.join(result)
|
||||||
|
|
||||||
|
# the interesting parts of info_output2 and info_output should be same
|
||||||
|
self.assert_equal(filter(info_output), filter(info_output2))
|
||||||
|
|
||||||
def test_atime(self):
|
def test_atime(self):
|
||||||
have_root = self.create_test_files()
|
have_root = self.create_test_files()
|
||||||
|
|
|
@ -11,10 +11,7 @@ logger = create_logger()
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def io_logger():
|
def io_logger():
|
||||||
io = StringIO()
|
io = StringIO()
|
||||||
args = Mock()
|
handler = setup_logging(io)
|
||||||
args.verbose = 2
|
|
||||||
assert args.verbose == 2
|
|
||||||
handler, = setup_logging(args, io)
|
|
||||||
handler.setFormatter(logging.Formatter('%(name)s: %(message)s'))
|
handler.setFormatter(logging.Formatter('%(name)s: %(message)s'))
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
return io
|
return io
|
||||||
|
|
Loading…
Reference in New Issue