diff --git a/NEWS b/NEWS index 908256a3..dd0a6b65 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,9 @@ https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#archive-naming * #479, #588: The "prefix" options have been deprecated in favor of the new "archive_name_format" auto-matching behavior and the "match_archives" option. + * #658: Add "--log-file-format" flag for customizing the log message format. See the documentation + for more information: + https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/#logging-to-file * #662: Fix regression in which the "check_repositories" option failed to match repositories. * #663: Fix regression in which the "transfer" action produced a traceback. * Add spellchecking of source code during test runs. diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 44e11ebb..84ab79fc 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -178,10 +178,12 @@ def make_parsers(): help='Log verbose progress to monitoring integrations that support logging (from only errors to very verbose: -1, 0, 1, or 2)', ) global_group.add_argument( - '--log-file', + '--log-file', type=str, help='Write log messages to this file instead of syslog', + ) + global_group.add_argument( + '--log-file-format', type=str, - default=None, - help='Write log messages to this file instead of syslog', + help='Log format string used for log messages written to the log file', ) global_group.add_argument( '--override', diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index b4aadfca..22aba0b1 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -700,6 +700,7 @@ def main(): # pragma: no cover verbosity_to_log_level(global_arguments.log_file_verbosity), verbosity_to_log_level(global_arguments.monitoring_verbosity), global_arguments.log_file, + global_arguments.log_file_format, ) except (FileNotFoundError, PermissionError) as error: configure_logging(logging.CRITICAL) diff --git a/borgmatic/logger.py b/borgmatic/logger.py index 648500b0..52065928 100644 --- a/borgmatic/logger.py +++ b/borgmatic/logger.py @@ -156,6 +156,7 @@ def configure_logging( log_file_log_level=None, monitoring_log_level=None, log_file=None, + log_file_format=None, ): ''' Configure logging to go to both the console and (syslog or log file). Use the given log levels, @@ -200,12 +201,18 @@ def configure_logging( 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.setFormatter( + logging.Formatter('borgmatic: {levelname} {message}', style='{') # noqa: FS003 + ) syslog_handler.setLevel(syslog_log_level) handlers = (console_handler, syslog_handler) elif log_file: file_handler = logging.handlers.WatchedFileHandler(log_file) - file_handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s')) + file_handler.setFormatter( + logging.Formatter( + log_file_format or '[{asctime}] {levelname}: {message}', style='{' # noqa: FS003 + ) + ) file_handler.setLevel(log_file_log_level) handlers = (console_handler, file_handler) else: diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index 67d8cc23..b6238fe9 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -154,5 +154,39 @@ 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, for example with -[logrotate](https://wiki.archlinux.org/index.php/Logrotate). Also, there is a -`--log-file-verbosity` flag to customize the log file's log level. +[logrotate](https://wiki.archlinux.org/index.php/Logrotate). + +You can the `--log-file-verbosity` flag to customize the log file's log level: + +```bash +borgmatic --log-file /path/to/file.log --log-file-verbosity 2 +``` + +New in borgmatic version 1.7.11 +Use the `--log-file-format` flag to override the default log message format. +This format string can contain a series of named placeholders wrapped in curly +brackets. For instance, the default log format is: `[{asctime}] {levelname}: +{message}`. This means each log message is recorded as the log time (in square +brackets), a logging level name, a colon, and the actual log message. + +So if you just want each log message to get logged *without* a timestamp or a +logging level name: + +```bash +borgmatic --log-file /path/to/file.log --log-file-format "{message}" +``` + +Here is a list of available placeholders: + + * `{asctime}`: time the log message was created + * `{levelname}`: level of the log message (`INFO`, `DEBUG`, etc.) + * `{lineno}`: line number in the source file where the log message originated + * `{message}`: actual log message + * `{pathname}`: path of the source file where the log message originated + +See the [Python logging +documentation](https://docs.python.org/3/library/logging.html#logrecord-attributes) +for additional placeholders. + +Note that this `--log-file-format` flg only applies to the specified +`--log-file` and not to syslog or other logging. diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index 0fb284a3..dc9c748d 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -285,7 +285,7 @@ def test_configure_logging_skips_syslog_if_interactive_console(): module.configure_logging(console_log_level=logging.INFO) -def test_configure_logging_to_logfile_instead_of_syslog(): +def test_configure_logging_to_log_file_instead_of_syslog(): flexmock(module).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.ANSWER flexmock(module).should_receive('Multi_stream_handler').and_return( @@ -309,7 +309,36 @@ def test_configure_logging_to_logfile_instead_of_syslog(): ) -def test_configure_logging_skips_logfile_if_argument_is_none(): +def test_configure_logging_to_log_file_formats_with_custom_log_format(): + flexmock(module).should_receive('add_custom_log_levels') + flexmock(module.logging).ANSWER = module.ANSWER + flexmock(module.logging).should_receive('Formatter').with_args( + '{message}', style='{' # noqa: FS003 + ).once() + flexmock(module).should_receive('Multi_stream_handler').and_return( + flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None) + ) + + flexmock(module).should_receive('interactive_console').and_return(False) + flexmock(module.logging).should_receive('basicConfig').with_args( + 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() + file_handler = logging.handlers.WatchedFileHandler('/tmp/logfile') + flexmock(module.logging.handlers).should_receive('WatchedFileHandler').with_args( + '/tmp/logfile' + ).and_return(file_handler).once() + + module.configure_logging( + console_log_level=logging.INFO, + log_file_log_level=logging.DEBUG, + log_file='/tmp/logfile', + log_file_format='{message}', # noqa: FS003 + ) + + +def test_configure_logging_skips_log_file_if_argument_is_none(): flexmock(module).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.ANSWER flexmock(module).should_receive('Multi_stream_handler').and_return(