Truncate long command error output.

This commit is contained in:
Dan Helfman 2023-07-10 10:20:51 -07:00
parent a3f47a6418
commit bd9d592560
3 changed files with 15 additions and 30 deletions

2
NEWS
View file

@ -20,6 +20,8 @@
* All deprecated configuration option values now generate warning logs. * All deprecated configuration option values now generate warning logs.
* Remove the deprecated (and non-functional) "--excludes" flag in favor of excludes within * Remove the deprecated (and non-functional) "--excludes" flag in favor of excludes within
configuration. configuration.
* Fix an error when logging too-long command output during error handling. Now, long command output
is truncated before logging.
1.7.15 1.7.15
* #326: Add configuration options and command-line flags for backing up a database from one * #326: Add configuration options and command-line flags for backing up a database from one

View file

@ -141,7 +141,6 @@ def run_configuration(config_filename, config, arguments):
f'{repository.get("label", repository["path"])}: Error running actions for repository', f'{repository.get("label", repository["path"])}: Error running actions for repository',
error, error,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
) )
) )
logger.warning( logger.warning(
@ -531,26 +530,24 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
return (configs, logs) return (configs, logs)
def log_record(suppress_log=False, **kwargs): def log_record(**kwargs):
''' '''
Create a log record based on the given makeLogRecord() arguments, one of which must be Create a log record based on the given makeLogRecord() arguments, one of which must be
named "levelno". Log the record (unless suppress log is set) and return it. named "levelno". Log the record and return it.
''' '''
record = logging.makeLogRecord(kwargs) record = logging.makeLogRecord(kwargs)
if suppress_log:
return record
logger.handle(record) logger.handle(record)
return record return record
def log_error_records( MAX_CAPTURED_OUTPUT_LENGTH = 1000
message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
):
def log_error_records(message, error=None, levelno=logging.CRITICAL):
''' '''
Given error message text, an optional exception object, an optional log level, and whether to Given error message text, an optional exception object, and an optional log level, log error
log the error output of a CalledProcessError (if any), log error summary information and also summary information and also yield it as a series of logging.LogRecord instances.
yield it as a series of logging.LogRecord instances.
Note that because the logs are yielded as a generator, logs won't get logged unless you consume Note that because the logs are yielded as a generator, logs won't get logged unless you consume
the generator output. the generator output.
@ -566,12 +563,12 @@ def log_error_records(
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:
# Suppress these logs for now and save full error output for the log summary at the end. output = error.output.decode('utf-8')
yield log_record( yield log_record(
levelno=levelno, levelno=levelno,
levelname=level_name, levelname=level_name,
msg=error.output, msg=output[:MAX_CAPTURED_OUTPUT_LENGTH]
suppress_log=not log_command_error_output, + ' ...' * (len(output) > MAX_CAPTURED_OUTPUT_LENGTH),
) )
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:

View file

@ -238,7 +238,6 @@ def test_run_configuration_retries_hard_error():
'foo: Error running actions for repository', 'foo: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]) ).and_return([flexmock()])
error_logs = [flexmock()] error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args( flexmock(module).should_receive('log_error_records').with_args(
@ -278,13 +277,11 @@ def test_run_configuration_retries_round_robin():
'foo: Error running actions for repository', 'foo: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered() ).and_return([flexmock()]).ordered()
flexmock(module).should_receive('log_error_records').with_args( flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository', 'bar: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered() ).and_return([flexmock()]).ordered()
foo_error_logs = [flexmock()] foo_error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args( flexmock(module).should_receive('log_error_records').with_args(
@ -314,13 +311,11 @@ def test_run_configuration_retries_one_passes():
'foo: Error running actions for repository', 'foo: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered() ).and_return([flexmock()]).ordered()
flexmock(module).should_receive('log_error_records').with_args( flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository', 'bar: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return(flexmock()).ordered() ).and_return(flexmock()).ordered()
error_logs = [flexmock()] error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args( flexmock(module).should_receive('log_error_records').with_args(
@ -344,7 +339,6 @@ def test_run_configuration_retry_wait():
'foo: Error running actions for repository', 'foo: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered() ).and_return([flexmock()]).ordered()
flexmock(time).should_receive('sleep').with_args(10).and_return().ordered() flexmock(time).should_receive('sleep').with_args(10).and_return().ordered()
@ -352,7 +346,6 @@ def test_run_configuration_retry_wait():
'foo: Error running actions for repository', 'foo: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered() ).and_return([flexmock()]).ordered()
flexmock(time).should_receive('sleep').with_args(20).and_return().ordered() flexmock(time).should_receive('sleep').with_args(20).and_return().ordered()
@ -360,7 +353,6 @@ def test_run_configuration_retry_wait():
'foo: Error running actions for repository', 'foo: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered() ).and_return([flexmock()]).ordered()
flexmock(time).should_receive('sleep').with_args(30).and_return().ordered() flexmock(time).should_receive('sleep').with_args(30).and_return().ordered()
@ -389,13 +381,11 @@ def test_run_configuration_retries_timeout_multiple_repos():
'foo: Error running actions for repository', 'foo: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered() ).and_return([flexmock()]).ordered()
flexmock(module).should_receive('log_error_records').with_args( flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository', 'bar: Error running actions for repository',
OSError, OSError,
levelno=logging.WARNING, levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered() ).and_return([flexmock()]).ordered()
# Sleep before retrying foo (and passing) # Sleep before retrying foo (and passing)
@ -826,10 +816,6 @@ def test_log_record_does_not_raise():
module.log_record(levelno=1, foo='bar', baz='quux') module.log_record(levelno=1, foo='bar', baz='quux')
def test_log_record_with_suppress_does_not_raise():
module.log_record(levelno=1, foo='bar', baz='quux', suppress_log=True)
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)
@ -843,7 +829,7 @@ def test_log_error_records_generates_output_logs_for_called_process_error():
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING) flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
logs = tuple( logs = tuple(
module.log_error_records('Error', subprocess.CalledProcessError(1, 'ls', 'error output')) module.log_error_records('Error', subprocess.CalledProcessError(1, 'ls', b'error output'))
) )
assert {log['levelno'] for log in logs} == {logging.CRITICAL} assert {log['levelno'] for log in logs} == {logging.CRITICAL}