-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Structured Error Output #9890
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/str-std-error
Are you sure you want to change the base?
Structured Error Output #9890
Changes from all commits
1afe452
c46cf3e
6842e4e
3fe5630
bcc33fc
829968b
181b94a
6958d85
9eef83f
329d167
5700443
073c28d
60b04ee
b82ef33
4b34b62
7f511a2
ae6987d
9db425e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "type": "feature", | ||
| "category": "Output", | ||
| "description": "AWS service errors containing modeled fields beyond Code and Message now write structured output to stdout in the configured output format. Set cli_error_format=LEGACY to disable or use --output off to suppress stdout." | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| import distro | ||
| from botocore import xform_name | ||
| from botocore.compat import OrderedDict, copy_kwargs | ||
| from botocore.exceptions import ClientError | ||
| from botocore.configprovider import ( | ||
| ChainProvider, | ||
| ConstantProvider, | ||
|
|
@@ -76,6 +77,7 @@ | |
| ) | ||
| from awscli.plugin import load_plugins | ||
| from awscli.telemetry import add_session_id_component_to_user_agent_extra | ||
| from awscli.structured_error import StructuredErrorHandler | ||
| from awscli.utils import ( | ||
| IMDSRegionProvider, | ||
| OutputStreamFactory, | ||
|
|
@@ -275,6 +277,9 @@ def _update_config_chain(self): | |
| config_store.set_config_provider( | ||
| 'cli_help_output', self._construct_cli_help_output_chain() | ||
| ) | ||
| config_store.set_config_provider( | ||
| 'cli_error_format', self._construct_cli_error_format_chain() | ||
| ) | ||
|
|
||
| def _construct_cli_region_chain(self): | ||
| providers = [ | ||
|
|
@@ -368,6 +373,20 @@ def _construct_cli_auto_prompt_chain(self): | |
| ] | ||
| return ChainProvider(providers=providers) | ||
|
|
||
| def _construct_cli_error_format_chain(self): | ||
| providers = [ | ||
| EnvironmentProvider( | ||
| name='AWS_CLI_ERROR_FORMAT', | ||
| env=os.environ, | ||
| ), | ||
| ScopedConfigProvider( | ||
| config_var_name='cli_error_format', | ||
| session=self.session, | ||
| ), | ||
| ConstantProvider(value='STANDARD'), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Few things on the enum values:
|
||
| ] | ||
| return ChainProvider(providers=providers) | ||
|
|
||
| @property | ||
| def subcommand_table(self): | ||
| return self._get_command_table() | ||
|
|
@@ -983,6 +1002,9 @@ class CLIOperationCaller: | |
| def __init__(self, session): | ||
| self._session = session | ||
| self._output_stream_factory = OutputStreamFactory(session) | ||
| self._structured_error_handler = StructuredErrorHandler( | ||
| session, self._output_stream_factory | ||
| ) | ||
|
|
||
| def invoke(self, service_name, operation_name, parameters, parsed_globals): | ||
| """Invoke an operation and format the response. | ||
|
|
@@ -1023,21 +1045,54 @@ def invoke(self, service_name, operation_name, parameters, parsed_globals): | |
| def _make_client_call( | ||
| self, client, operation_name, parameters, parsed_globals | ||
| ): | ||
| py_operation_name = xform_name(operation_name) | ||
| if client.can_paginate(py_operation_name) and parsed_globals.paginate: | ||
| paginator = client.get_paginator(py_operation_name) | ||
| response = paginator.paginate(**parameters) | ||
| else: | ||
| response = getattr(client, xform_name(operation_name))( | ||
| **parameters | ||
| try: | ||
| py_operation_name = xform_name(operation_name) | ||
| if ( | ||
| client.can_paginate(py_operation_name) | ||
| and parsed_globals.paginate | ||
| ): | ||
| paginator = client.get_paginator(py_operation_name) | ||
| response = paginator.paginate(**parameters) | ||
| else: | ||
| response = getattr(client, py_operation_name)(**parameters) | ||
| return response | ||
| except ClientError as e: | ||
| # Display structured error output before re-raising | ||
| self._display_structured_error_for_exception( | ||
| e, parsed_globals | ||
| ) | ||
| raise | ||
|
|
||
| def _display_structured_error_for_exception( | ||
| self, exception, parsed_globals | ||
| ): | ||
| try: | ||
| error_response = ( | ||
| self._structured_error_handler.extract_error_response( | ||
| exception | ||
| ) | ||
| ) | ||
| if error_response: | ||
| self._structured_error_handler.handle_error( | ||
| error_response, parsed_globals | ||
| ) | ||
| except Exception as e: | ||
| # Don't let structured error display break error handling | ||
| LOG.debug( | ||
| 'Failed to display structured error: %s', | ||
| e, | ||
| exc_info=True, | ||
| ) | ||
| return response | ||
|
|
||
| def _display_response(self, command_name, response, parsed_globals): | ||
| output = parsed_globals.output | ||
| if output is None: | ||
| output = self._session.get_config_variable('output') | ||
|
|
||
| formatter = get_formatter(output, parsed_globals) | ||
| with self._output_stream_factory.get_output_stream() as stream: | ||
| formatter(command_name, response, stream) | ||
| try: | ||
| with self._output_stream_factory.get_output_stream() as stream: | ||
| formatter(command_name, response, stream) | ||
| except ClientError as e: | ||
| self._display_structured_error_for_exception(e, parsed_globals) | ||
| raise | ||
|
Comment on lines
+1096
to
+1098
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need this block? Can the formatter raise a structured error that the actual API call wouldn't have? |
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -29,6 +29,8 @@ | |||||||||||
|
|
||||||||||||
| * yaml-stream | ||||||||||||
|
|
||||||||||||
| * off | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While we're here, can you catch up the section in the API reference too? aws-cli/awscli/topics/config-vars.rst Lines 92 to 96 in 33dbfd2
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also add new information there on the new error format setting. |
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| ``--query`` (string) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Throughout, either drop years, or use the short version: |
||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"). You | ||
| # may not use this file except in compliance with the License. A copy of | ||
| # the License is located at | ||
| # | ||
| # http://aws.amazon.com/apache2.0/ | ||
| # | ||
| # or in the "license" file accompanying this file. This file is | ||
| # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF | ||
| # ANY KIND, either express or implied. See the License for the specific | ||
| # language governing permissions and limitations under the License. | ||
| import logging | ||
|
|
||
| from botocore.exceptions import ClientError | ||
|
|
||
| from awscli.formatter import get_formatter | ||
|
|
||
|
|
||
| LOG = logging.getLogger('awscli.structured_error') | ||
|
|
||
|
|
||
| class StructuredErrorHandler: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be handled through the existing We'd need less new plumbing in the driver, and we already should have confidence that it is catching I also wonder if that'll help apply more of this to custom commands. |
||
| """Handles display of structured error information from AWS services. | ||
| This class is responsible for determining when to display structured | ||
| error output and formatting it appropriately based on user configuration | ||
| and output format settings. | ||
| """ | ||
|
|
||
| def __init__(self, session, output_stream_factory): | ||
| self._session = session | ||
| self._output_stream_factory = output_stream_factory | ||
|
|
||
| def handle_error(self, error_response, parsed_globals): | ||
| error_info = error_response.get('Error', {}) | ||
|
|
||
| if self.should_display(error_info, parsed_globals): | ||
| self.display(error_info, parsed_globals) | ||
|
|
||
| def should_display(self, error_info, parsed_globals): | ||
| if not self._has_additional_error_members(error_info): | ||
| return False | ||
|
|
||
| config_store = self._session.get_component('config_store') | ||
| error_format = config_store.get_config_variable('cli_error_format') | ||
| if error_format == 'LEGACY': | ||
| return False | ||
|
|
||
| output = self._get_output_format(parsed_globals) | ||
| if output == 'off': | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
| def display(self, error_response, parsed_globals): | ||
| output = self._get_output_format(parsed_globals) | ||
|
|
||
| try: | ||
| formatter = get_formatter(output, parsed_globals) | ||
|
|
||
| with self._output_stream_factory.get_output_stream() as stream: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| formatter('error', error_response, stream) | ||
| except Exception as e: | ||
| # Log the error but don't let it prevent the normal error handling | ||
| LOG.debug( | ||
| "Failed to display structured error output: %s", | ||
| e, | ||
| exc_info=True, | ||
| ) | ||
|
|
||
| def _get_output_format(self, parsed_globals): | ||
| output = parsed_globals.output | ||
| if output is None: | ||
| output = self._session.get_config_variable('output') | ||
| return output | ||
|
|
||
| def _has_additional_error_members(self, error_response): | ||
| if not error_response: | ||
| return False | ||
|
|
||
| standard_keys = {'Code', 'Message'} | ||
| error_keys = set(error_response.keys()) | ||
| return len(error_keys - standard_keys) > 0 | ||
|
|
||
| @staticmethod | ||
| def extract_error_response(exception): | ||
| if not isinstance(exception, ClientError): | ||
| return None | ||
|
|
||
| if hasattr(exception, 'response') and 'Error' in exception.response: | ||
| return {'Error': exception.response['Error']} | ||
|
|
||
| return None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--output offis substantial, please add its own entry in a separate file.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't need to see changes, but in hindsight I'd rather have done
--output offin a separate PR, these features aren't dependent on one another technically.