Fix error handling to log command output as one record per line (#754).
This commit is contained in:
parent
bac2aabe66
commit
5912769273
3 changed files with 34 additions and 17 deletions
2
NEWS
2
NEWS
|
@ -4,6 +4,8 @@
|
||||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook
|
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook
|
||||||
* #753: When "archive_name_format" is not set, filter archives using the default archive name
|
* #753: When "archive_name_format" is not set, filter archives using the default archive name
|
||||||
format.
|
format.
|
||||||
|
* #754: Fix error handling to log command output as one record per line instead of truncating
|
||||||
|
too-long output and swallowing the end of some Borg error messages.
|
||||||
* Update documentation to recommend installing/upgrading borgmatic with pipx instead of pip. See the
|
* Update documentation to recommend installing/upgrading borgmatic with pipx instead of pip. See the
|
||||||
documentation for more information:
|
documentation for more information:
|
||||||
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation
|
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation
|
||||||
|
|
|
@ -555,9 +555,6 @@ def log_record(suppress_log=False, **kwargs):
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
MAX_CAPTURED_OUTPUT_LENGTH = 1000
|
|
||||||
|
|
||||||
|
|
||||||
def log_error_records(
|
def log_error_records(
|
||||||
message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
|
message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
|
||||||
):
|
):
|
||||||
|
@ -579,20 +576,24 @@ def log_error_records(
|
||||||
raise error
|
raise error
|
||||||
except CalledProcessError as error:
|
except CalledProcessError as error:
|
||||||
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
||||||
|
|
||||||
if error.output:
|
if error.output:
|
||||||
try:
|
try:
|
||||||
output = error.output.decode('utf-8')
|
output = error.output.decode('utf-8')
|
||||||
except (UnicodeDecodeError, AttributeError):
|
except (UnicodeDecodeError, AttributeError):
|
||||||
output = error.output
|
output = error.output
|
||||||
|
|
||||||
# Suppress these logs for now and save full error output for the log summary at the end.
|
# Suppress these logs for now and save the error output for the log summary at the end.
|
||||||
yield log_record(
|
# Log a separate record per line, as some errors can be really verbose and overflow the
|
||||||
levelno=levelno,
|
# per-record size limits imposed by some logging backends.
|
||||||
levelname=level_name,
|
for output_line in output.splitlines():
|
||||||
msg=output[:MAX_CAPTURED_OUTPUT_LENGTH]
|
yield log_record(
|
||||||
+ ' ...' * (len(output) > MAX_CAPTURED_OUTPUT_LENGTH),
|
levelno=levelno,
|
||||||
suppress_log=True,
|
levelname=level_name,
|
||||||
)
|
msg=output_line,
|
||||||
|
suppress_log=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield log_record(levelno=levelno, levelname=level_name, msg=error)
|
yield log_record(levelno=levelno, levelname=level_name, msg=error)
|
||||||
except (ValueError, OSError) as error:
|
except (ValueError, OSError) as error:
|
||||||
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
||||||
|
|
|
@ -849,7 +849,7 @@ def test_log_record_with_suppress_does_not_raise():
|
||||||
|
|
||||||
|
|
||||||
def test_log_error_records_generates_output_logs_for_message_only():
|
def test_log_error_records_generates_output_logs_for_message_only():
|
||||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
flexmock(module).should_receive('log_record').replace_with(dict).once()
|
||||||
|
|
||||||
logs = tuple(module.log_error_records('Error'))
|
logs = tuple(module.log_error_records('Error'))
|
||||||
|
|
||||||
|
@ -857,7 +857,7 @@ def test_log_error_records_generates_output_logs_for_message_only():
|
||||||
|
|
||||||
|
|
||||||
def test_log_error_records_generates_output_logs_for_called_process_error_with_bytes_ouput():
|
def test_log_error_records_generates_output_logs_for_called_process_error_with_bytes_ouput():
|
||||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
flexmock(module).should_receive('log_record').replace_with(dict).times(3)
|
||||||
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||||
|
|
||||||
logs = tuple(
|
logs = tuple(
|
||||||
|
@ -869,7 +869,7 @@ def test_log_error_records_generates_output_logs_for_called_process_error_with_b
|
||||||
|
|
||||||
|
|
||||||
def test_log_error_records_generates_output_logs_for_called_process_error_with_string_ouput():
|
def test_log_error_records_generates_output_logs_for_called_process_error_with_string_ouput():
|
||||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
flexmock(module).should_receive('log_record').replace_with(dict).times(3)
|
||||||
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||||
|
|
||||||
logs = tuple(
|
logs = tuple(
|
||||||
|
@ -880,8 +880,22 @@ def test_log_error_records_generates_output_logs_for_called_process_error_with_s
|
||||||
assert any(log for log in logs if 'error output' in str(log))
|
assert any(log for log in logs if 'error output' in str(log))
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_error_records_splits_called_process_error_with_multiline_ouput_into_multiple_logs():
|
||||||
|
flexmock(module).should_receive('log_record').replace_with(dict).times(4)
|
||||||
|
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||||
|
|
||||||
|
logs = tuple(
|
||||||
|
module.log_error_records(
|
||||||
|
'Error', subprocess.CalledProcessError(1, 'ls', 'error output\nanother line')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
|
||||||
|
assert any(log for log in logs if 'error output' in str(log))
|
||||||
|
|
||||||
|
|
||||||
def test_log_error_records_generates_logs_for_value_error():
|
def test_log_error_records_generates_logs_for_value_error():
|
||||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
flexmock(module).should_receive('log_record').replace_with(dict).twice()
|
||||||
|
|
||||||
logs = tuple(module.log_error_records('Error', ValueError()))
|
logs = tuple(module.log_error_records('Error', ValueError()))
|
||||||
|
|
||||||
|
@ -889,7 +903,7 @@ def test_log_error_records_generates_logs_for_value_error():
|
||||||
|
|
||||||
|
|
||||||
def test_log_error_records_generates_logs_for_os_error():
|
def test_log_error_records_generates_logs_for_os_error():
|
||||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
flexmock(module).should_receive('log_record').replace_with(dict).twice()
|
||||||
|
|
||||||
logs = tuple(module.log_error_records('Error', OSError()))
|
logs = tuple(module.log_error_records('Error', OSError()))
|
||||||
|
|
||||||
|
@ -897,7 +911,7 @@ def test_log_error_records_generates_logs_for_os_error():
|
||||||
|
|
||||||
|
|
||||||
def test_log_error_records_generates_nothing_for_other_error():
|
def test_log_error_records_generates_nothing_for_other_error():
|
||||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
flexmock(module).should_receive('log_record').never()
|
||||||
|
|
||||||
logs = tuple(module.log_error_records('Error', KeyError()))
|
logs = tuple(module.log_error_records('Error', KeyError()))
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue