diff --git a/awx/api/generics.py b/awx/api/generics.py index 7556bbbc9f..3657505a46 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -44,6 +44,7 @@ from awx.main.views import ApiErrorView from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer from awx.api.versioning import URLPathVersioning from awx.api.metadata import SublistAttachDetatchMetadata, Metadata +from awx.conf import settings_registry __all__ = [ 'APIView', @@ -208,12 +209,20 @@ class APIView(views.APIView): return response if response.status_code >= 400: - status_msg = "status %s received by user %s attempting to access %s from %s" % ( - response.status_code, - request.user, - request.path, - request.META.get('REMOTE_ADDR', None), - ) + msg_data = { + 'status_code': response.status_code, + 'user_name': request.user, + 'url_path': request.path, + 'remote_addr': request.META.get('REMOTE_ADDR', None), + 'error': response.data.get('error', response.status_text), + } + try: + status_msg = getattr(settings, 'API_400_ERROR_LOG_FORMAT').format(**msg_data) + except Exception as e: + if getattr(settings, 'API_400_ERROR_LOG_FORMAT', None): + logger.error("Unable to format API_400_ERROR_LOG_FORMAT setting, defaulting log message: {}".format(e)) + status_msg = settings_registry.get_setting_field('API_400_ERROR_LOG_FORMAT').get_default().format(**msg_data) + if hasattr(self, '__init_request_error__'): response = self.handle_exception(self.__init_request_error__) if response.status_code == 401: @@ -221,6 +230,7 @@ class APIView(views.APIView): logger.info(status_msg) else: logger.warning(status_msg) + response = super(APIView, self).finalize_response(request, response, *args, **kwargs) time_started = getattr(self, 'time_started', None) response['X-API-Product-Version'] = get_awx_version() diff --git a/awx/main/conf.py b/awx/main/conf.py index 36bd121055..2fa477380f 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -674,6 +674,24 @@ register( category=_('Logging'), category_slug='logging', ) +register( + 'API_400_ERROR_LOG_FORMAT', + field_class=fields.CharField, + default='status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}', + label=_('Log Format For API 4XX Errors'), + help_text=_( + 'The format of logged messages when an API 4XX error occurs, ' + 'the following variables will be substituted: \n' + 'status_code - The HTTP status code of the error\n' + 'user_name - The user name attempting to use the API\n' + 'url_path - The URL path to the API endpoint called\n' + 'remote_addr - The remote address seen for the user\n' + 'error - The error set by the api endpoint\n' + 'Variables need to be in the format {}.' + ), + category=_('Logging'), + category_slug='logging', +) register( diff --git a/awx/main/tests/functional/test_api_generics.py b/awx/main/tests/functional/test_api_generics.py new file mode 100644 index 0000000000..4733c26768 --- /dev/null +++ b/awx/main/tests/functional/test_api_generics.py @@ -0,0 +1,26 @@ +import pytest + +from django.test.utils import override_settings +from awx.api.versioning import reverse + + +@pytest.mark.django_db +def test_change_400_error_log(caplog, post, admin_user): + with override_settings(API_400_ERROR_LOG_FORMAT='Test'): + post(url=reverse('api:setting_logging_test'), data={}, user=admin_user, expect=409) + assert 'Test' in caplog.text + + +@pytest.mark.django_db +def test_bad_400_error_log(caplog, post, admin_user): + with override_settings(API_400_ERROR_LOG_FORMAT="Not good {junk}"): + post(url=reverse('api:setting_logging_test'), data={}, user=admin_user, expect=409) + assert "Unable to format API_400_ERROR_LOG_FORMAT setting, defaulting log message: 'junk'" in caplog.text + assert 'status 409 received by user admin attempting to access /api/v2/settings/logging/test/ from 127.0.0.1' in caplog.text + + +@pytest.mark.django_db +def test_custom_400_error_log(caplog, post, admin_user): + with override_settings(API_400_ERROR_LOG_FORMAT="{status_code} {error}"): + post(url=reverse('api:setting_logging_test'), data={}, user=admin_user, expect=409) + assert '409 Logging not enabled' in caplog.text diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index feab6832fa..9f616017be 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -761,6 +761,7 @@ LOG_AGGREGATOR_MAX_DISK_USAGE_GB = 1 LOG_AGGREGATOR_MAX_DISK_USAGE_PATH = '/var/lib/awx' LOG_AGGREGATOR_RSYSLOGD_DEBUG = False LOG_AGGREGATOR_RSYSLOGD_ERROR_LOG_FILE = '/var/log/tower/rsyslog.err' +API_400_ERROR_LOG_FORMAT = 'status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}' # The number of retry attempts for websocket session establishment # If you're encountering issues establishing websockets in a cluster, diff --git a/awx/ui/src/screens/Setting/Logging/Logging.test.js b/awx/ui/src/screens/Setting/Logging/Logging.test.js index 0d7b35e4b1..d0c817de49 100644 --- a/awx/ui/src/screens/Setting/Logging/Logging.test.js +++ b/awx/ui/src/screens/Setting/Logging/Logging.test.js @@ -32,6 +32,8 @@ SettingsAPI.readCategory.mockResolvedValue({ LOG_AGGREGATOR_MAX_DISK_USAGE_GB: 1, LOG_AGGREGATOR_MAX_DISK_USAGE_PATH: '/var/lib/awx', LOG_AGGREGATOR_RSYSLOGD_DEBUG: false, + API_400_ERROR_LOG_FORMAT: + 'status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}', }, }); diff --git a/awx/ui/src/screens/Setting/Logging/LoggingDetail/LoggingDetail.js b/awx/ui/src/screens/Setting/Logging/LoggingDetail/LoggingDetail.js index 9224a43a7a..ac0e10373e 100644 --- a/awx/ui/src/screens/Setting/Logging/LoggingDetail/LoggingDetail.js +++ b/awx/ui/src/screens/Setting/Logging/LoggingDetail/LoggingDetail.js @@ -42,7 +42,8 @@ function LoggingDetail() { 'LOG_AGGREGATOR_TCP_TIMEOUT', 'LOG_AGGREGATOR_TYPE', 'LOG_AGGREGATOR_USERNAME', - 'LOG_AGGREGATOR_VERIFY_CERT' + 'LOG_AGGREGATOR_VERIFY_CERT', + 'API_400_ERROR_LOG_FORMAT' ); const mergedData = {}; diff --git a/awx/ui/src/screens/Setting/Logging/LoggingDetail/LoggingDetail.test.js b/awx/ui/src/screens/Setting/Logging/LoggingDetail/LoggingDetail.test.js index 32ddfe0e11..4b1132c09a 100644 --- a/awx/ui/src/screens/Setting/Logging/LoggingDetail/LoggingDetail.test.js +++ b/awx/ui/src/screens/Setting/Logging/LoggingDetail/LoggingDetail.test.js @@ -59,6 +59,7 @@ describe('', () => { assertDetail(wrapper, 'Logging Aggregator Protocol', 'https'); assertDetail(wrapper, 'TCP Connection Timeout', '5 seconds'); assertDetail(wrapper, 'Logging Aggregator Level Threshold', 'INFO'); + assertDetail(wrapper, 'Log Format For API 4XX Errors', 'Test Log Line'); assertDetail( wrapper, 'Enable/disable HTTPS certificate verification', diff --git a/awx/ui/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.js b/awx/ui/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.js index aa05ddd58f..5fd86d8837 100644 --- a/awx/ui/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.js +++ b/awx/ui/src/screens/Setting/Logging/LoggingEdit/LoggingEdit.js @@ -8,7 +8,7 @@ import { CardBody } from 'components/Card'; import ContentError from 'components/ContentError'; import ContentLoading from 'components/ContentLoading'; import { FormSubmitError } from 'components/FormField'; -import { FormColumnLayout } from 'components/FormLayout'; +import { FormColumnLayout, FormFullWidthLayout } from 'components/FormLayout'; import { useSettings } from 'contexts/Settings'; import useModal from 'hooks/useModal'; import useRequest from 'hooks/useRequest'; @@ -71,6 +71,7 @@ function LoggingEdit() { LOG_AGGREGATOR_LOGGERS: formatJson(form.LOG_AGGREGATOR_LOGGERS), LOG_AGGREGATOR_HOST: form.LOG_AGGREGATOR_HOST || null, LOG_AGGREGATOR_TYPE: form.LOG_AGGREGATOR_TYPE || null, + API_400_ERROR_LOG_FORMAT: form.API_400_ERROR_LOG_FORMAT || null, }); }; @@ -196,6 +197,12 @@ function LoggingEdit() { name="LOG_AGGREGATOR_LOGGERS" config={logging.LOG_AGGREGATOR_LOGGERS} /> + + + {submitError && } {revertError && } ', () => { diff --git a/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json b/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json index 9a50f8c73a..0c6a5b5ff7 100644 --- a/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json +++ b/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json @@ -564,6 +564,15 @@ "category_slug": "logging", "default": false }, + "API_400_ERROR_LOG_FORMAT": { + "type": "string", + "required": false, + "label": "Log Format For API 4XX Errors", + "help_text": "The format of logged messages when an API 4XX error occurs, the following variables will be substituted: \nstatus_code - The HTTP status code of the error\nuser_name - The user name attempting to use the API\nurl_path - The URL path to the API endpoint called\nremote_addr - The remote address seen for the user\nerror - The error set by the api endpoint\nVariables need to be in the format {}.", + "category": "Logging", + "category_slug": "logging", + "default": "status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}" + }, "AUTOMATION_ANALYTICS_LAST_GATHER": { "type": "datetime", "required": true, @@ -4221,6 +4230,15 @@ "category_slug": "logging", "defined_in_file": false }, + "API_400_ERROR_LOG_FORMAT": { + "type": "string", + "required": false, + "label": "Log Format For API 4XX Errors", + "help_text": "The format of logged messages when an API 4XX error occurs, the following variables will be substituted: \nstatus_code - The HTTP status code of the error\nuser_name - The user name attempting to use the API\nurl_path - The URL path to the API endpoint called\nremote_addr - The remote address seen for the user\nerror - The error set by the api endpoint\nVariables need to be in the format {}.", + "category": "Logging", + "category_slug": "logging", + "default": "status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}" + }, "AUTOMATION_ANALYTICS_LAST_GATHER": { "type": "datetime", "label": "Last gather date for Insights for Ansible Automation Platform.", @@ -6329,7 +6347,7 @@ }, "SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR": { "type": "nested object", - "label": "SAML User Flags Attribute Mapping", + "label": "SAML User Flags Attribute Mapping", "help_text": "Used to map super users and system auditors from SAML.", "category": "SAML", "category_slug": "saml", diff --git a/awx/ui/src/screens/Setting/shared/data.allSettings.json b/awx/ui/src/screens/Setting/shared/data.allSettings.json index d6cc08cd54..4715c4e03e 100644 --- a/awx/ui/src/screens/Setting/shared/data.allSettings.json +++ b/awx/ui/src/screens/Setting/shared/data.allSettings.json @@ -70,6 +70,7 @@ "LOG_AGGREGATOR_MAX_DISK_USAGE_GB":1, "LOG_AGGREGATOR_MAX_DISK_USAGE_PATH":"/var/lib/awx", "LOG_AGGREGATOR_RSYSLOGD_DEBUG":false, + "API_400_ERROR_LOG_FORMAT":"status {status_code} received by user {user_name} attempting to access {url_path} from {remote_addr}", "AUTOMATION_ANALYTICS_LAST_GATHER":null, "AUTOMATION_ANALYTICS_GATHER_INTERVAL":14400, "SESSION_COOKIE_AGE":1800, diff --git a/awx/ui/src/screens/Setting/shared/data.logSettings.json b/awx/ui/src/screens/Setting/shared/data.logSettings.json index d976bdc6e0..dca6c8d057 100644 --- a/awx/ui/src/screens/Setting/shared/data.logSettings.json +++ b/awx/ui/src/screens/Setting/shared/data.logSettings.json @@ -17,5 +17,6 @@ "LOG_AGGREGATOR_LEVEL": "INFO", "LOG_AGGREGATOR_MAX_DISK_USAGE_GB": 1, "LOG_AGGREGATOR_MAX_DISK_USAGE_PATH": "/var/lib/awx", - "LOG_AGGREGATOR_RSYSLOGD_DEBUG": false -} \ No newline at end of file + "LOG_AGGREGATOR_RSYSLOGD_DEBUG": false, + "API_400_ERROR_LOG_FORMAT": "Test Log Line" +}