diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 3e78cc92e..f40b2b40f 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -351,6 +351,47 @@ + +
+
+ Action Log Rotation and Archiving +
+
+
+

+ All actions performed in are automatically logged. These logs are stored in a database table, which can become quite large. + Enabling log rotation and archiving will move all logs older than 30 days into storage. +

+
+
+ Enable Action Log Rotation +
+ + + + + + + + + + +
Storage location: + +
+ The storage location in which to place archived action logs. Logs will only be archived to this single location. +
+
Storage path: + +
+ The path under the configured storage engine in which to place the archived logs in JSON form. +
+
+
+
diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 763330ff6..bc208ef5a 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -80,6 +80,10 @@ angular.module("core-config-setup", ['angularFileUpload']) {'id': 'oidc-login', 'title': 'OIDC Login(s)', 'condition': function(config) { return $scope.getOIDCProviders(config).length > 0; }}, + + {'id': 'actionlogarchiving', 'title': 'Action Log Rotation', 'condition': function(config) { + return config.FEATURE_ACTION_LOG_ROTATION; + }}, ]; $scope.STORAGE_CONFIG_FIELDS = { diff --git a/util/config/validator.py b/util/config/validator.py index 4190caa34..dda3bd666 100644 --- a/util/config/validator.py +++ b/util/config/validator.py @@ -21,6 +21,7 @@ from util.config.validators.validate_github import GitHubLoginValidator, GitHubT from util.config.validators.validate_oidc import OIDCLoginValidator from util.config.validators.validate_timemachine import TimeMachineValidator from util.config.validators.validate_access import AccessSettingsValidator +from util.config.validators.validate_actionlog_archiving import ActionLogArchivingValidator logger = logging.getLogger(__name__) @@ -57,6 +58,7 @@ VALIDATORS = { OIDCLoginValidator.name: OIDCLoginValidator.validate, TimeMachineValidator.name: TimeMachineValidator.validate, AccessSettingsValidator.name: AccessSettingsValidator.validate, + ActionLogArchivingValidator.name: ActionLogArchivingValidator.validate, } def validate_service_for_config(service, config, password=None): diff --git a/util/config/validators/test/test_validate_actionlog_archiving.py b/util/config/validators/test/test_validate_actionlog_archiving.py new file mode 100644 index 000000000..c14555441 --- /dev/null +++ b/util/config/validators/test/test_validate_actionlog_archiving.py @@ -0,0 +1,51 @@ +import pytest + +from util.config.validators import ConfigValidationException +from util.config.validators.validate_actionlog_archiving import ActionLogArchivingValidator + +from test.fixtures import * + +@pytest.mark.parametrize('unvalidated_config', [ + ({}), + ({'ACTION_LOG_ARCHIVE_PATH': 'foo'}), + ({'ACTION_LOG_ARCHIVE_LOCATION': ''}), +]) +def test_skip_validate_actionlog(unvalidated_config, app): + validator = ActionLogArchivingValidator() + validator.validate(unvalidated_config, None, None) + + +@pytest.mark.parametrize('config, expected_error', [ + ({'FEATURE_ACTION_LOG_ROTATION': True}, 'Missing action log archive path'), + ({'FEATURE_ACTION_LOG_ROTATION': True, + 'ACTION_LOG_ARCHIVE_PATH': ''}, 'Missing action log archive path'), + ({'FEATURE_ACTION_LOG_ROTATION': True, + 'ACTION_LOG_ARCHIVE_PATH': 'foo'}, 'Missing action log archive storage location'), + ({'FEATURE_ACTION_LOG_ROTATION': True, + 'ACTION_LOG_ARCHIVE_PATH': 'foo', + 'ACTION_LOG_ARCHIVE_LOCATION': ''}, 'Missing action log archive storage location'), + ({'FEATURE_ACTION_LOG_ROTATION': True, + 'ACTION_LOG_ARCHIVE_PATH': 'foo', + 'ACTION_LOG_ARCHIVE_LOCATION': 'invalid'}, + 'Action log archive storage location `invalid` not found in storage config'), +]) +def test_invalid_config(config, expected_error, app): + validator = ActionLogArchivingValidator() + + with pytest.raises(ConfigValidationException) as ipe: + validator.validate(config, None, None) + + assert ipe.value.message == expected_error + +def test_valid_config(app): + config = { + 'FEATURE_ACTION_LOG_ROTATION': True, + 'ACTION_LOG_ARCHIVE_PATH': 'somepath', + 'ACTION_LOG_ARCHIVE_LOCATION': 'somelocation', + 'DISTRIBUTED_STORAGE_CONFIG': { + 'somelocation': {}, + }, + } + + validator = ActionLogArchivingValidator() + validator.validate(config, None, None) diff --git a/util/config/validators/validate_actionlog_archiving.py b/util/config/validators/validate_actionlog_archiving.py new file mode 100644 index 000000000..e8fb79a50 --- /dev/null +++ b/util/config/validators/validate_actionlog_archiving.py @@ -0,0 +1,22 @@ +from util.config.validators import BaseValidator, ConfigValidationException + +class ActionLogArchivingValidator(BaseValidator): + name = "actionlogarchiving" + + @classmethod + def validate(cls, config, user, user_password): + """ Validates the action log archiving configuration. """ + if not config.get('FEATURE_ACTION_LOG_ROTATION', False): + return + + if not config.get('ACTION_LOG_ARCHIVE_PATH'): + raise ConfigValidationException('Missing action log archive path') + + if not config.get('ACTION_LOG_ARCHIVE_LOCATION'): + raise ConfigValidationException('Missing action log archive storage location') + + location = config['ACTION_LOG_ARCHIVE_LOCATION'] + storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG') or {} + if location not in storage_config: + msg = 'Action log archive storage location `%s` not found in storage config' % location + raise ConfigValidationException(msg) diff --git a/workers/logrotateworker.py b/workers/logrotateworker.py index 54ff0f25d..06e93d23d 100644 --- a/workers/logrotateworker.py +++ b/workers/logrotateworker.py @@ -53,7 +53,13 @@ class LogRotateWorker(Worker): return def _perform_archiving(self, cutoff_id): - log_archive = DelegateUserfiles(app, storage, SAVE_LOCATION, SAVE_PATH) + save_location = SAVE_LOCATION + if not save_location: + # Pick the *same* save location for all instances. This is a fallback if + # a location was not configured. + save_location = storage.locations[0] + + log_archive = DelegateUserfiles(app, storage, save_location, SAVE_PATH) with UseThenDisconnect(app.config): start_id = get_stale_logs_start_id()