瀏覽代碼

Initial work on putting logging and verbosity options into configuration.

Dan Helfman 4 月之前
父節點
當前提交
cfad4200a9
共有 5 個文件被更改,包括 249 次插入8 次删除
  1. 2 2
      borgmatic/commands/arguments.py
  2. 2 0
      borgmatic/commands/borgmatic.py
  3. 105 5
      borgmatic/config/schema.yaml
  4. 57 1
      borgmatic/logger.py
  5. 83 0
      tests/unit/test_logger.py

+ 2 - 2
borgmatic/commands/arguments.py

@@ -349,12 +349,12 @@ def make_parsers():
     global_group.add_argument(
     global_group.add_argument(
         '--log-file-format',
         '--log-file-format',
         type=str,
         type=str,
-        help='Log format string used for log messages written to the log file',
+        help='Python format string used for log messages written to the log file',
     )
     )
     global_group.add_argument(
     global_group.add_argument(
         '--log-json',
         '--log-json',
         action='store_true',
         action='store_true',
-        help='Write log messages and console output as one JSON object per log line instead of formatted text',
+        help='Write Borg log messages and console output as one JSON object per log line instead of formatted text',
     )
     )
     global_group.add_argument(
     global_group.add_argument(
         '--override',
         '--override',

+ 2 - 0
borgmatic/commands/borgmatic.py

@@ -43,6 +43,7 @@ from borgmatic.logger import (
     DISABLED,
     DISABLED,
     Log_prefix,
     Log_prefix,
     add_custom_log_levels,
     add_custom_log_levels,
+    configure_delayed_logging,
     configure_logging,
     configure_logging,
     should_do_markup,
     should_do_markup,
 )
 )
@@ -880,6 +881,7 @@ def exit_with_help_link():  # pragma: no cover
 
 
 def main(extra_summary_logs=[]):  # pragma: no cover
 def main(extra_summary_logs=[]):  # pragma: no cover
     configure_signals()
     configure_signals()
+    configure_delayed_logging()
 
 
     try:
     try:
         arguments = parse_arguments(*sys.argv[1:])
         arguments = parse_arguments(*sys.argv[1:])

+ 105 - 5
borgmatic/config/schema.yaml

@@ -751,12 +751,112 @@ properties:
             Restrict the number of checked archives to the last n. Applies only
             Restrict the number of checked archives to the last n. Applies only
             to the "archives" check. Defaults to checking all archives.
             to the "archives" check. Defaults to checking all archives.
         example: 3
         example: 3
-    color:
-        type: boolean
+    logging:
+        type: object
+        additionalProperties: false
+        properties:
+            console:
+                type: object
+                additionalProperties: false
+                properties:
+                    verbosity:
+                        type: integer
+                        enum:
+                            - -2
+                            - -1
+                            - 0
+                            - 1
+                            - 2
+                        description: |
+                            Display verbose progress to the console: -2
+                            (disabled), -1 (errors only), 0 (responses to
+                            actions, the default), 1 (info about steps borgmatic
+                            is taking), or 2 (debug).
+                        example: 2
+                    color:
+                        type: boolean
+                        description: |
+                            Apply color to console output. Can be overridden with --no-color
+                            command-line flag. Defaults to true.
+                        example: false
+            syslog:
+                type: object
+                additionalProperties: false
+                properties:
+                    verbosity:
+                        type: integer
+                        enum:
+                            - -2
+                            - -1
+                            - 0
+                            - 1
+                            - 2
+                        description: |
+                            Log verbose progress to syslog: -2 (disabled, the
+                            default), -1 (errors only), 0 (responses to
+                            actions), 1 (info about steps borgmatic is taking),
+                            or 2 (debug).
+                        example: 2
+            file:
+                type: object
+                required: ['path']
+                additionalProperties: false
+                properties:
+                    verbosity:
+                        type: integer
+                        enum:
+                            - -2
+                            - -1
+                            - 0
+                            - 1
+                            - 2
+                        description: |
+                            Log verbose progress to file: -2 (disabled), -1
+                            (errors only), 0 (responses to actions), 1 (info
+                            about steps borgmatic is taking, the default), or 2
+                            (debug).
+                        example: 2
+                    path:
+                        type: string
+                        description: |
+                            Write log messages to this file instead of syslog.
+                        example: /var/log/borgmatic/logfile.txt
+                    format:
+                        type: string
+                        description: |
+                            Python format string used for log messages written
+                            to the log file.
+                        example: "%(asctime)s %(message)s"
+                            
+            monitoring:
+                type: object
+                additionalProperties: false
+                properties:
+                    verbosity:
+                        type: integer
+                        enum:
+                            - -2
+                            - -1
+                            - 0
+                            - 1
+                            - 2
+                        description: |
+                            When a monitoring integration supporting logging is
+                            configured, log verbose progress to it: -2
+                            (disabled), -1 (errors only), responses to actions
+                            (0), 1 (info about steps borgmatic is taking, the
+                            default), or 2 (debug).
+                        example: 2
+            json:
+                type: boolean
+                description: |
+                    Write Borg log messages and console output as one JSON
+                    object per log line instead of formatted text. Defaults to
+                    false.
+                example: true
         description: |
         description: |
-            Apply color to console output. Can be overridden with --no-color
-            command-line flag. Defaults to true.
-        example: false
+            Settings for where to log borgmatic progress and how verbose those
+            logs should be.
     skip_actions:
     skip_actions:
         type: array
         type: array
         items:
         items:

+ 57 - 1
borgmatic/logger.py

@@ -252,6 +252,61 @@ class Log_prefix:
         set_log_prefix(self.original_prefix)
         set_log_prefix(self.original_prefix)
 
 
 
 
+class Delayed_logging_handler(logging.handlers.BufferingHandler):
+    '''
+    A logging handler that buffers logs and doesn't flush them until explicitly flushed (after target
+    handlers are actually set). It's useful for holding onto logged records from before logging is
+    configured to ensure those records eventually make their way to the relevant logging handlers.
+    '''
+
+    def __init__(self):
+        super(Delayed_logging_handler, self).__init__(capacity=0)
+
+        self.targets = None
+
+    def shouldFlush(self, record):
+        return self.targets is not None
+
+    def flush(self):
+        self.acquire()
+
+        try:
+            if not self.targets:
+                return
+
+            for record in self.buffer:
+                for target in self.targets:
+                    target.handle(record)
+
+            self.buffer.clear()
+        finally:
+            self.release()
+
+
+def configure_delayed_logging():  # pragma: no cover
+    '''
+    Configure a delayed logging handler to buffer anything that gets logged until we're ready to
+    deal with it.
+    '''
+    logging.basicConfig(
+        level=logging.DEBUG,
+        handlers=[Delayed_logging_handler()],
+    )
+
+
+def flush_delayed_logging(target_handlers):
+    '''
+    Flush any previously buffered logs to our "real" logging handlers.
+    '''
+    root_logger = logging.getLogger()
+
+    if root_logger.handlers and isinstance(root_logger.handlers[0], Delayed_logging_handler):
+        delayed_handler = root_logger.handlers[0]
+        delayed_handler.targets = target_handlers
+        delayed_handler.flush()
+        root_logger.removeHandler(delayed_handler)
+
+
 def configure_logging(
 def configure_logging(
     console_log_level,
     console_log_level,
     syslog_log_level=None,
     syslog_log_level=None,
@@ -299,7 +354,6 @@ def configure_logging(
         console_handler.setFormatter(Console_no_color_formatter())
         console_handler.setFormatter(Console_no_color_formatter())
 
 
     console_handler.setLevel(console_log_level)
     console_handler.setLevel(console_log_level)
-
     handlers = [console_handler]
     handlers = [console_handler]
 
 
     if syslog_log_level != logging.DISABLED:
     if syslog_log_level != logging.DISABLED:
@@ -336,6 +390,8 @@ def configure_logging(
         file_handler.setLevel(log_file_log_level)
         file_handler.setLevel(log_file_log_level)
         handlers.append(file_handler)
         handlers.append(file_handler)
 
 
+    flush_delayed_logging(handlers)
+
     logging.basicConfig(
     logging.basicConfig(
         level=min(handler.level for handler in handlers),
         level=min(handler.level for handler in handlers),
         handlers=handlers,
         handlers=handlers,

+ 83 - 0
tests/unit/test_logger.py

@@ -360,6 +360,78 @@ def test_log_prefix_sets_prefix_and_then_restores_original_prefix_after():
         pass
         pass
 
 
 
 
+def test_delayed_logging_handler_should_flush_without_targets_returns_false():
+    handler = module.Delayed_logging_handler()
+
+    assert handler.shouldFlush(flexmock()) is False
+
+
+def test_delayed_logging_handler_should_flush_with_targets_returns_true():
+    handler = module.Delayed_logging_handler()
+    handler.targets = [flexmock()]
+
+    assert handler.shouldFlush(flexmock()) is True
+
+
+def test_delayed_logging_handler_flush_without_targets_does_not_raise():
+    handler = module.Delayed_logging_handler()
+    flexmock(handler).should_receive('acquire')
+    flexmock(handler).should_receive('release')
+
+    handler.flush()
+
+
+def test_delayed_logging_handler_flush_with_empty_buffer_does_not_raise():
+    handler = module.Delayed_logging_handler()
+    flexmock(handler).should_receive('acquire')
+    flexmock(handler).should_receive('release')
+    handler.targets = [flexmock()]
+
+    handler.flush()
+
+
+def test_delayed_logging_handler_flush_forwards_each_record_to_each_target():
+    handler = module.Delayed_logging_handler()
+    flexmock(handler).should_receive('acquire')
+    flexmock(handler).should_receive('release')
+    handler.targets = [flexmock(), flexmock()]
+    handler.buffer = [flexmock(), flexmock()]
+    handler.targets[0].should_receive('handle').with_args(handler.buffer[0]).once()
+    handler.targets[1].should_receive('handle').with_args(handler.buffer[0]).once()
+    handler.targets[0].should_receive('handle').with_args(handler.buffer[1]).once()
+    handler.targets[1].should_receive('handle').with_args(handler.buffer[1]).once()
+
+    handler.flush()
+
+    assert handler.buffer == []
+
+
+def test_flush_delayed_logging_without_handlers_does_not_raise():
+    root_logger = flexmock(handlers=[])
+    root_logger.should_receive('removeHandler')
+    flexmock(module.logging).should_receive('getLogger').and_return(root_logger)
+
+    module.flush_delayed_logging([flexmock()])
+
+
+def test_flush_delayed_logging_without_delayed_logging_handler_does_not_raise():
+    root_logger = flexmock(handlers=[flexmock()])
+    root_logger.should_receive('removeHandler')
+    flexmock(module.logging).should_receive('getLogger').and_return(root_logger)
+
+    module.flush_delayed_logging([flexmock()])
+
+
+def test_flush_delayed_logging_flushes_delayed_logging_handler():
+    delayed_logging_handler = module.Delayed_logging_handler()
+    root_logger = flexmock(handlers=[delayed_logging_handler])
+    flexmock(module.logging).should_receive('getLogger').and_return(root_logger)
+    flexmock(delayed_logging_handler).should_receive('flush').once()
+    root_logger.should_receive('removeHandler')
+
+    module.flush_delayed_logging([flexmock()])
+
+
 def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux():
 def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module.logging).ANSWER = module.ANSWER
@@ -369,6 +441,7 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux(
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('interactive_console').and_return(False)
     flexmock(module).should_receive('interactive_console').and_return(False)
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.DEBUG, handlers=list
         level=logging.DEBUG, handlers=list
     )
     )
@@ -390,6 +463,7 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_macos(
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('interactive_console').and_return(False)
     flexmock(module).should_receive('interactive_console').and_return(False)
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.DEBUG, handlers=list
         level=logging.DEBUG, handlers=list
     )
     )
@@ -412,6 +486,7 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_freebs
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('interactive_console').and_return(False)
     flexmock(module).should_receive('interactive_console').and_return(False)
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.DEBUG, handlers=list
         level=logging.DEBUG, handlers=list
     )
     )
@@ -434,6 +509,7 @@ def test_configure_logging_without_syslog_log_level_skips_syslog():
     multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
     multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.INFO, handlers=list
         level=logging.INFO, handlers=list
     )
     )
@@ -451,6 +527,7 @@ def test_configure_logging_skips_syslog_if_not_found():
     multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
     multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.INFO, handlers=list
         level=logging.INFO, handlers=list
     )
     )
@@ -469,6 +546,7 @@ def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled():
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
 
 
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.INFO, handlers=list
         level=logging.INFO, handlers=list
     )
     )
@@ -490,6 +568,7 @@ def test_configure_logging_to_log_file_instead_of_syslog():
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
 
 
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.DEBUG, handlers=list
         level=logging.DEBUG, handlers=list
     )
     )
@@ -517,6 +596,7 @@ def test_configure_logging_to_both_log_file_and_syslog():
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
 
 
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.DEBUG, handlers=list
         level=logging.DEBUG, handlers=list
     )
     )
@@ -553,6 +633,7 @@ def test_configure_logging_to_log_file_formats_with_custom_log_format():
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
 
 
     flexmock(module).should_receive('interactive_console').and_return(False)
     flexmock(module).should_receive('interactive_console').and_return(False)
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.DEBUG, handlers=list
         level=logging.DEBUG, handlers=list
     )
     )
@@ -580,6 +661,7 @@ def test_configure_logging_skips_log_file_if_argument_is_none():
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
 
 
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.INFO, handlers=list
         level=logging.INFO, handlers=list
     )
     )
@@ -599,6 +681,7 @@ def test_configure_logging_uses_console_no_color_formatter_if_color_disabled():
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
 
 
+    flexmock(module).should_receive('flush_delayed_logging')
     flexmock(module.logging).should_receive('basicConfig').with_args(
     flexmock(module.logging).should_receive('basicConfig').with_args(
         level=logging.INFO, handlers=list
         level=logging.INFO, handlers=list
     )
     )