diff --git a/NEWS b/NEWS index 51861ef35..61443c097 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,6 @@ +1.4.5 + * Log to file instead of syslog via command-line "--log-file" flag. + 1.4.4 * #234: Support for Borg --keep-exclude-tags and --exclude-nodump options. diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 77fae7794..92b375235 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -147,13 +147,20 @@ def parse_arguments(*unparsed_arguments): type=int, choices=range(0, 3), default=0, - help='Display verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive', + help='Log verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive or --log-file is given', + ) + global_group.add_argument( + '--log-file-verbosity', + type=int, + choices=range(0, 3), + default=1, + help='Log verbose progress to log file (from none to lots: 0, 1, or 2). Only used when --log-file is given', ) global_group.add_argument( '--log-file', type=str, default=None, - help='Write log messages to this file instead of concole and syslog', + help='Write log messages to this file instead of syslog', ) global_group.add_argument( '--version', diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 5c7d4d321..43e343848 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -484,11 +484,17 @@ def main(): # pragma: no cover configs, parse_logs = load_configurations(config_filenames) colorama.init(autoreset=True, strip=not should_do_markup(global_arguments.no_color, configs)) - configure_logging( - verbosity_to_log_level(global_arguments.verbosity), - verbosity_to_log_level(global_arguments.syslog_verbosity), - global_arguments.log_file, - ) + try: + configure_logging( + verbosity_to_log_level(global_arguments.verbosity), + verbosity_to_log_level(global_arguments.syslog_verbosity), + verbosity_to_log_level(global_arguments.log_file_verbosity), + global_arguments.log_file, + ) + except (FileNotFoundError, PermissionError) as error: + configure_logging(logging.CRITICAL) + logger.critical('Error configuring logging: {}'.format(error)) + exit_with_help_link() logger.debug('Ensuring legacy configuration is upgraded') convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames) diff --git a/borgmatic/logger.py b/borgmatic/logger.py index 01b286f6f..515729740 100644 --- a/borgmatic/logger.py +++ b/borgmatic/logger.py @@ -73,12 +73,19 @@ def color_text(color, message): return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL) -def configure_logging(console_log_level, syslog_log_level=None, log_file=None): +def configure_logging( + console_log_level, syslog_log_level=None, log_file_log_level=None, log_file=None +): ''' - Configure logging to go to both the console and syslog. Use the given log levels, respectively. + Configure logging to go to both the console and (syslog or log file). Use the given log levels, + respectively. + + Raise FileNotFoundError or PermissionError if the log file could not be opened for writing. ''' if syslog_log_level is None: syslog_log_level = console_log_level + if log_file_log_level is None: + log_file_log_level = console_log_level console_handler = logging.StreamHandler() console_handler.setFormatter(Console_color_formatter()) @@ -97,18 +104,13 @@ def configure_logging(console_log_level, syslog_log_level=None, log_file=None): 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 = logging.FileHandler(log_file) file_handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s')) - file_handler.setLevel(syslog_log_level) + file_handler.setLevel(log_file_log_level) handlers = (console_handler, file_handler) else: handlers = (console_handler,) - logging.basicConfig(level=min(console_log_level, syslog_log_level), handlers=handlers) + logging.basicConfig( + level=min(console_log_level, syslog_log_level, log_file_log_level), handlers=handlers + ) diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index 50ce170ce..4930bae2b 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -70,6 +70,20 @@ Or to increase syslog logging to include debug spew: borgmatic --syslog-verbosity 2 ``` +### Logging to file + +If you don't want to use syslog, and you'd rather borgmatic log to a plain +file, use the `--log-file` flag: + +```bash +borgmatic --log-file /path/to/file.log +``` + +Note that if you use the `--log-file` flag, you are responsible for rotating +the log file so it doesn't grow too large. Also, there is also +`--log-file-verbosity` flag to customize the log file's log level. + + ### systemd journal If your local syslog daemon is systemd's journal, be aware that journald by diff --git a/setup.py b/setup.py index 81e391062..40627cd63 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '1.4.4' +VERSION = '1.4.5' setup( diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index c2955df42..91b25541b 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -195,11 +195,11 @@ def test_configure_logging_skips_syslog_if_interactive_console(): module.configure_logging(console_log_level=logging.INFO) -def test_configure_logging_to_logfile_instead_syslog(): +def test_configure_logging_to_logfile_instead_of_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 + level=logging.DEBUG, handlers=tuple ) flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True) flexmock(module.logging.handlers).should_receive('SysLogHandler').never() @@ -208,7 +208,9 @@ def test_configure_logging_to_logfile_instead_syslog(): file_handler ).once() - module.configure_logging(console_log_level=logging.INFO, log_file='/tmp/logfile') + module.configure_logging( + console_log_level=logging.INFO, log_file_log_level=logging.DEBUG, log_file='/tmp/logfile' + ) def test_configure_logging_skips_logfile_if_argument_is_none():