diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 07d1f6d7..6039e4fa 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -152,30 +152,30 @@ def make_parsers(): '-v', '--verbosity', type=int, - choices=range(-1, 3), + choices=range(-2, 3), default=0, - help='Display verbose progress to the console (from only errors to very verbose: -1, 0, 1, or 2)', + help='Display verbose progress to the console (disabled, errors only, default, some, or lots: -2, -1, 0, 1, or 2)', ) global_group.add_argument( '--syslog-verbosity', type=int, - choices=range(-1, 3), + choices=range(-2, 3), default=0, - help='Log verbose progress to syslog (from only errors to very verbose: -1, 0, 1, or 2). Ignored when console is interactive or --log-file is given', + help='Log verbose progress to syslog (disabled, errors only, default, some, or lots: -2, -1, 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(-1, 3), + choices=range(-2, 3), default=0, - help='Log verbose progress to log file (from only errors to very verbose: -1, 0, 1, or 2). Only used when --log-file is given', + help='Log verbose progress to log file (disabled, errors only, default, some, or lots: -2, -1, 0, 1, or 2). Only used when --log-file is given', ) global_group.add_argument( '--monitoring-verbosity', type=int, - choices=range(-1, 3), + choices=range(-2, 3), default=0, - help='Log verbose progress to monitoring integrations that support logging (from only errors to very verbose: -1, 0, 1, or 2)', + help='Log verbose progress to monitoring integrations that support logging (from disabled, errors only, default, some, or lots: -2, -1, 0, 1, or 2)', ) global_group.add_argument( '--log-file', diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 965f3931..597868f6 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -36,7 +36,7 @@ from borgmatic.borg import version as borg_version from borgmatic.commands.arguments import parse_arguments from borgmatic.config import checks, collect, convert, validate from borgmatic.hooks import command, dispatch, monitor -from borgmatic.logger import add_custom_log_levels, configure_logging, should_do_markup +from borgmatic.logger import add_custom_log_levels, configure_logging, should_do_markup, DISABLED from borgmatic.signals import configure_signals from borgmatic.verbosity import verbosity_to_log_level @@ -70,6 +70,7 @@ def run_configuration(config_filename, config, arguments): error_repository = '' using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments) monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity) + monitoring_hooks_are_activated = using_primary_action and monitoring_log_level != DISABLED try: local_borg_version = borg_version.local_borg_version(storage, local_path) @@ -78,7 +79,7 @@ def run_configuration(config_filename, config, arguments): return try: - if using_primary_action: + if monitoring_hooks_are_activated: dispatch.call_hooks( 'initialize_monitor', hooks, @@ -87,7 +88,7 @@ def run_configuration(config_filename, config, arguments): monitoring_log_level, global_arguments.dry_run, ) - if using_primary_action: + dispatch.call_hooks( 'ping_monitor', hooks, @@ -165,7 +166,7 @@ def run_configuration(config_filename, config, arguments): error_repository = repository['path'] try: - if using_primary_action: + if monitoring_hooks_are_activated: # send logs irrespective of error dispatch.call_hooks( 'ping_monitor', @@ -185,7 +186,7 @@ def run_configuration(config_filename, config, arguments): if not encountered_error: try: - if using_primary_action: + if monitoring_hooks_are_activated: dispatch.call_hooks( 'ping_monitor', hooks, diff --git a/borgmatic/logger.py b/borgmatic/logger.py index 52065928..cda5151d 100644 --- a/borgmatic/logger.py +++ b/borgmatic/logger.py @@ -141,6 +141,7 @@ def add_logging_level(level_name, level_number): ANSWER = logging.WARN - 5 +DISABLED = logging.CRITICAL + 10 def add_custom_log_levels(): # pragma: no cover @@ -148,6 +149,7 @@ def add_custom_log_levels(): # pragma: no cover Add a custom log level between WARN and INFO for user-requested answers. ''' add_logging_level('ANSWER', ANSWER) + add_logging_level('DISABLED', DISABLED) def configure_logging( @@ -175,10 +177,12 @@ def configure_logging( # Log certain log levels to console stderr and others to stdout. This supports use cases like # grepping (non-error) output. + console_disabled = logging.NullHandler() console_error_handler = logging.StreamHandler(sys.stderr) console_standard_handler = logging.StreamHandler(sys.stdout) console_handler = Multi_stream_handler( { + logging.DISABLED: console_disabled, logging.CRITICAL: console_error_handler, logging.ERROR: console_error_handler, logging.WARN: console_error_handler, @@ -191,7 +195,7 @@ def configure_logging( console_handler.setLevel(console_log_level) syslog_path = None - if log_file is None: + if log_file is None and syslog_log_level != logging.DISABLED: if os.path.exists('/dev/log'): syslog_path = '/dev/log' elif os.path.exists('/var/run/syslog'): @@ -206,7 +210,7 @@ def configure_logging( ) syslog_handler.setLevel(syslog_log_level) handlers = (console_handler, syslog_handler) - elif log_file: + elif log_file and log_file_log_level != logging.DISABLED: file_handler = logging.handlers.WatchedFileHandler(log_file) file_handler.setFormatter( logging.Formatter( diff --git a/borgmatic/verbosity.py b/borgmatic/verbosity.py index cdadd61f..8cba009d 100644 --- a/borgmatic/verbosity.py +++ b/borgmatic/verbosity.py @@ -2,6 +2,7 @@ import logging import borgmatic.logger +VERBOSITY_DISABLED = -2 VERBOSITY_ERROR = -1 VERBOSITY_ANSWER = 0 VERBOSITY_SOME = 1 @@ -15,6 +16,7 @@ def verbosity_to_log_level(verbosity): borgmatic.logger.add_custom_log_levels() return { + VERBOSITY_DISABLED: logging.DISABLED, VERBOSITY_ERROR: logging.ERROR, VERBOSITY_ANSWER: logging.ANSWER, VERBOSITY_SOME: logging.INFO, diff --git a/sample/systemd/borgmatic.service b/sample/systemd/borgmatic.service index 3b2dff65..885c435f 100644 --- a/sample/systemd/borgmatic.service +++ b/sample/systemd/borgmatic.service @@ -61,4 +61,4 @@ LogRateLimitIntervalSec=0 # Delay start to prevent backups running during boot. Note that systemd-inhibit requires dbus and # dbus-user-session to be installed. ExecStartPre=sleep 1m -ExecStart=systemd-inhibit --who="borgmatic" --what="sleep:shutdown" --why="Prevent interrupting scheduled backup" /root/.local/bin/borgmatic --verbosity -1 --syslog-verbosity 1 +ExecStart=systemd-inhibit --who="borgmatic" --what="sleep:shutdown" --why="Prevent interrupting scheduled backup" /root/.local/bin/borgmatic --verbosity -2 --syslog-verbosity 1 diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index bd98c01f..9b8bf582 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -169,6 +169,19 @@ def test_run_configuration_bails_for_monitor_finish_soft_failure(): assert results == [] +def test_run_configuration_does_not_call_monitoring_hooks_if_monitoring_hooks_are_disabled(): + flexmock(module).should_receive('verbosity_to_log_level').and_return(module.DISABLED) + flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) + + flexmock(module.dispatch).should_receive('call_hooks').never() + flexmock(module).should_receive('run_actions').and_return([]) + + config = {'location': {'repositories': [{'path': 'foo'}]}} + arguments = {'global': flexmock(monitoring_verbosity=-2, dry_run=False), 'create': flexmock()} + results = list(module.run_configuration('test.yaml', config, arguments)) + assert results == [] + + def test_run_configuration_logs_on_error_hook_error(): flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO) flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock()) @@ -229,8 +242,7 @@ def test_run_configuration_retries_hard_error(): ).and_return([flexmock()]) error_logs = [flexmock()] flexmock(module).should_receive('log_error_records').with_args( - 'foo: Error running actions for repository', - OSError, + 'foo: Error running actions for repository', OSError, ).and_return(error_logs) config = {'location': {'repositories': [{'path': 'foo'}]}, 'storage': {'retries': 1}} arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()} diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index dc9c748d..f86aacd7 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -285,6 +285,44 @@ def test_configure_logging_skips_syslog_if_interactive_console(): module.configure_logging(console_log_level=logging.INFO) +def test_configure_logging_skips_syslog_if_syslog_logging_is_disabled(): + flexmock(module).should_receive('add_custom_log_levels') + flexmock(module.logging).DISABLED = module.DISABLED + flexmock(module).should_receive('Multi_stream_handler').and_return( + flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None) + ) + flexmock(module).should_receive('Console_color_formatter') + flexmock(module).should_receive('interactive_console').never() + 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() + + module.configure_logging(console_log_level=logging.INFO, syslog_log_level=logging.DISABLED) + + +def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled(): + flexmock(module).should_receive('add_custom_log_levels') + flexmock(module.logging).DISABLED = module.DISABLED + flexmock(module).should_receive('Multi_stream_handler').and_return( + flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None) + ) + + # 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').never() + flexmock(module.logging.handlers).should_receive('SysLogHandler').never() + flexmock(module.logging.handlers).should_receive('WatchedFileHandler').never() + + module.configure_logging( + console_log_level=logging.INFO, log_file_log_level=logging.DISABLED, log_file='/tmp/logfile' + ) + + def test_configure_logging_to_log_file_instead_of_syslog(): flexmock(module).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.ANSWER @@ -297,7 +335,7 @@ def test_configure_logging_to_log_file_instead_of_syslog(): 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.os.path).should_receive('exists').never() flexmock(module.logging.handlers).should_receive('SysLogHandler').never() file_handler = logging.handlers.WatchedFileHandler('/tmp/logfile') flexmock(module.logging.handlers).should_receive('WatchedFileHandler').with_args( diff --git a/tests/unit/test_verbosity.py b/tests/unit/test_verbosity.py index 572ad192..a53e4edb 100644 --- a/tests/unit/test_verbosity.py +++ b/tests/unit/test_verbosity.py @@ -17,11 +17,13 @@ def insert_logging_mock(log_level): def test_verbosity_to_log_level_maps_known_verbosity_to_log_level(): flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER + flexmock(module.logging).DISABLED = module.borgmatic.logger.DISABLED assert module.verbosity_to_log_level(module.VERBOSITY_ERROR) == logging.ERROR assert module.verbosity_to_log_level(module.VERBOSITY_ANSWER) == module.borgmatic.logger.ANSWER assert module.verbosity_to_log_level(module.VERBOSITY_SOME) == logging.INFO assert module.verbosity_to_log_level(module.VERBOSITY_LOTS) == logging.DEBUG + assert module.verbosity_to_log_level(module.VERBOSITY_DISABLED) == logging.DISABLED def test_verbosity_to_log_level_maps_unknown_verbosity_to_warning_level():