diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 77ab7b20b..77fae7794 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -149,6 +149,12 @@ def parse_arguments(*unparsed_arguments): default=0, help='Display verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive', ) + global_group.add_argument( + '--log-file', + type=str, + default=None, + help='Write log messages to this file instead of concole and syslog', + ) global_group.add_argument( '--version', dest='version', diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 09413708e..5c7d4d321 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -487,6 +487,7 @@ def main(): # pragma: no cover configure_logging( verbosity_to_log_level(global_arguments.verbosity), verbosity_to_log_level(global_arguments.syslog_verbosity), + global_arguments.log_file, ) logger.debug('Ensuring legacy configuration is upgraded') diff --git a/borgmatic/logger.py b/borgmatic/logger.py index 7bbb912b1..01b286f6f 100644 --- a/borgmatic/logger.py +++ b/borgmatic/logger.py @@ -73,7 +73,7 @@ def color_text(color, message): return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL) -def configure_logging(console_log_level, syslog_log_level=None): +def configure_logging(console_log_level, syslog_log_level=None, log_file=None): ''' Configure logging to go to both the console and syslog. Use the given log levels, respectively. ''' @@ -85,16 +85,29 @@ def configure_logging(console_log_level, syslog_log_level=None): console_handler.setLevel(console_log_level) syslog_path = None - if os.path.exists('/dev/log'): - syslog_path = '/dev/log' - elif os.path.exists('/var/run/syslog'): - syslog_path = '/var/run/syslog' + if log_file is None: + if os.path.exists('/dev/log'): + syslog_path = '/dev/log' + elif os.path.exists('/var/run/syslog'): + syslog_path = '/var/run/syslog' if syslog_path and not interactive_console(): syslog_handler = logging.handlers.SysLogHandler(address=syslog_path) syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s %(message)s')) syslog_handler.setLevel(syslog_log_level) handlers = (console_handler, syslog_handler) + elif log_file: + try: + file_handler = logging.FileHandler(log_file) + except FileNotFoundError: + print("ERROR: Path to log-file doesn't exist: {}".format(log_file)) + sys.exit(1) + except PermissionError: + print("ERROR: No write access to log-file: {}".format(log_file)) + sys.exit(1) + file_handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s')) + file_handler.setLevel(syslog_log_level) + handlers = (console_handler, file_handler) else: handlers = (console_handler,) diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index 5cec8767a..c2955df42 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -193,3 +193,31 @@ def test_configure_logging_skips_syslog_if_interactive_console(): flexmock(module.logging.handlers).should_receive('SysLogHandler').never() module.configure_logging(console_log_level=logging.INFO) + + +def test_configure_logging_to_logfile_instead_syslog(): + # syslog skipped in non-interactive console if --log-file argument provided + flexmock(module).should_receive('interactive_console').and_return(False) + flexmock(module.logging).should_receive('basicConfig').with_args( + level=logging.INFO, handlers=tuple + ) + flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True) + flexmock(module.logging.handlers).should_receive('SysLogHandler').never() + file_handler = logging.FileHandler('/tmp/logfile') + flexmock(module.logging).should_receive('FileHandler').with_args('/tmp/logfile').and_return( + file_handler + ).once() + + module.configure_logging(console_log_level=logging.INFO, log_file='/tmp/logfile') + + +def test_configure_logging_skips_logfile_if_argument_is_none(): + # No FileHandler added if argument --log-file is None + flexmock(module).should_receive('interactive_console').and_return(False) + flexmock(module.logging).should_receive('basicConfig').with_args( + level=logging.INFO, handlers=tuple + ) + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.logging).should_receive('FileHandler').never() + + module.configure_logging(console_log_level=logging.INFO, log_file=None)