Skip to content
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

{Log} Adapt az_command_data_logger to Knack 0.8.0 #17324

Merged
merged 3 commits into from
Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions src/azure-cli-core/azure/cli/core/azclierror.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,16 @@ def print_error(self):
if self.exception_trace:
logger.exception(self.exception_trace)

# print recommendations to action
if self.recommendations:
for recommendation in self.recommendations:
print(recommendation, file=sys.stderr)

if self.aladdin_recommendations:
print('\nTRY THIS:', file=sys.stderr)
for recommendation, description in self.aladdin_recommendations:
print_styled_text(recommendation, file=sys.stderr)
print_styled_text(description, file=sys.stderr)
# print recommendations to action
if self.recommendations:
for recommendation in self.recommendations:
print(recommendation, file=sys.stderr)

if self.aladdin_recommendations:
print('\nTRY THIS:', file=sys.stderr)
for recommendation, description in self.aladdin_recommendations:
print_styled_text(recommendation, file=sys.stderr)
print_styled_text(description, file=sys.stderr)
Comment on lines +73 to +82
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No log is written here. The scope of CommandLoggerContext should be as small as possible.


def send_telemetry(self):
telemetry.set_error_type(self.__class__.__name__)
Expand Down
34 changes: 21 additions & 13 deletions src/azure-cli-core/azure/cli/core/azlogging.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@


class AzCliLogging(CLILogging):
_COMMAND_METADATA_LOGGER = 'az_command_data_logger'
COMMAND_METADATA_LOGGER = 'az_command_data_logger'

def __init__(self, name, cli_ctx=None):
super(AzCliLogging, self).__init__(name, cli_ctx)
Expand Down Expand Up @@ -86,16 +86,13 @@ def _delete_old_logs(log_dir):
self = cli_ctx.logging
args = kwargs['args']

cmd_logger = logging.getLogger(AzCliLogging._COMMAND_METADATA_LOGGER)
metadata_logger = logging.getLogger(AzCliLogging.COMMAND_METADATA_LOGGER)

# overwrite CLILogging._is_file_log_enabled() from knack
# Overwrite the default of knack.log.CLILogging._is_file_log_enabled() to True
self.file_log_enabled = cli_ctx.config.getboolean('logging', 'enable_log_file', fallback=True)

if self.file_log_enabled:
self._init_command_logfile_handlers(cmd_logger, args) # pylint: disable=protected-access
get_logger(__name__).debug("metadata file logging enabled - writing logs to '%s'.",
self.command_log_dir)

self._init_command_logfile_handlers(metadata_logger, args) # pylint: disable=protected-access
_delete_old_logs(self.command_log_dir)

def _init_command_logfile_handlers(self, command_metadata_logger, args):
Expand All @@ -113,12 +110,15 @@ def _init_command_logfile_handlers(self, command_metadata_logger, args):
log_name = "{}.{}.{}.{}.{}".format(date_str, time_str, command_str, os.getpid(), "log")

log_file_path = os.path.join(self.command_log_dir, log_name)
get_logger(__name__).debug("metadata file logging enabled - writing logs to '%s'.", log_file_path)

logfile_handler = logging.FileHandler(log_file_path)

lfmt = logging.Formatter(_CMD_LOG_LINE_PREFIX + ' %(process)d | %(asctime)s | %(levelname)s | %(name)s | %(message)s') # pylint: disable=line-too-long
logfile_handler.setFormatter(lfmt)
logfile_handler.setLevel(logging.DEBUG)
# command_metadata_logger should always accept all levels regardless of the root logger level.
command_metadata_logger.setLevel(logging.DEBUG)
command_metadata_logger.addHandler(logfile_handler)

self.command_logger_handler = logfile_handler
Expand Down Expand Up @@ -201,15 +201,23 @@ def end_cmd_metadata_logging(self, exit_code): # leave it non '-' prefix to not


class CommandLoggerContext:
"""A context manager during which error logs are also written to az_command_data_logger for
`az feedback` usage.
"""
def __init__(self, module_logger):
self.logger = module_logger
self.hdlr = logging.getLogger(AzCliLogging._COMMAND_METADATA_LOGGER) # pylint: disable=protected-access
metadata_logger = logging.getLogger(AzCliLogging.COMMAND_METADATA_LOGGER)
original_error = module_logger.error

# Duplicate error logging to metadata logger
def error_duplicated(*args, **kwargs):
original_error(*args, **kwargs)
metadata_logger.error(*args, **kwargs)
from unittest import mock
self.mock_cm = mock.patch.object(module_logger, 'error', error_duplicated)

def __enter__(self):
if self.hdlr:
self.logger.addHandler(self.hdlr) # add command metadata handler
self.mock_cm.__enter__()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if self.hdlr:
self.logger.removeHandler(self.hdlr)
self.mock_cm.__exit__(exc_type, exc_val, exc_tb)
157 changes: 78 additions & 79 deletions src/azure-cli-core/azure/cli/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,89 +69,88 @@ def handle_exception(ex): # pylint: disable=too-many-locals, too-many-statement
# Print the traceback and exception message
logger.debug(traceback.format_exc())

with CommandLoggerContext(logger):
Copy link
Member Author

@jiasli jiasli Mar 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommandLoggerContext will be called again at:

def print_error(self):
from azure.cli.core.azlogging import CommandLoggerContext
from azure.cli.core.style import print_styled_text
with CommandLoggerContext(logger):

Calling it here is redundant.

Besides, no log is written here, so these lines shouldn't be surrounded by CommandLoggerContext.

error_msg = getattr(ex, 'message', str(ex))
exit_code = 1

if isinstance(ex, azclierror.AzCLIError):
az_error = ex

elif isinstance(ex, JMESPathError):
error_msg = "Invalid jmespath query supplied for `--query`: {}".format(error_msg)
az_error = azclierror.InvalidArgumentValueError(error_msg)
az_error.set_recommendation(QUERY_REFERENCE)
error_msg = getattr(ex, 'message', str(ex))
exit_code = 1

if isinstance(ex, azclierror.AzCLIError):
az_error = ex

elif isinstance(ex, JMESPathError):
error_msg = "Invalid jmespath query supplied for `--query`: {}".format(error_msg)
az_error = azclierror.InvalidArgumentValueError(error_msg)
az_error.set_recommendation(QUERY_REFERENCE)

elif isinstance(ex, SSLError):
az_error = azclierror.AzureConnectionError(error_msg)
az_error.set_recommendation(SSLERROR_TEMPLATE)

elif isinstance(ex, CloudError):
if extract_common_error_message(ex):
error_msg = extract_common_error_message(ex)
status_code = str(getattr(ex, 'status_code', 'Unknown Code'))
AzCLIErrorType = get_error_type_by_status_code(status_code)
az_error = AzCLIErrorType(error_msg)

elif isinstance(ex, ValidationError):
az_error = azclierror.ValidationError(error_msg)

elif isinstance(ex, CLIError):
# TODO: Fine-grained analysis here
az_error = azclierror.UnclassifiedUserFault(error_msg)

elif isinstance(ex, AzureError):
if extract_common_error_message(ex):
error_msg = extract_common_error_message(ex)
AzCLIErrorType = get_error_type_by_azure_error(ex)
az_error = AzCLIErrorType(error_msg)

elif isinstance(ex, AzureException):
if is_azure_connection_error(error_msg):
az_error = azclierror.AzureConnectionError(error_msg)
else:
# TODO: Fine-grained analysis here for Unknown error
az_error = azclierror.UnknownError(error_msg)

elif isinstance(ex, SSLError):
elif isinstance(ex, ClientRequestError):
if is_azure_connection_error(error_msg):
az_error = azclierror.AzureConnectionError(error_msg)
elif isinstance(ex.inner_exception, SSLError):
# When msrest encounters SSLError, msrest wraps SSLError in ClientRequestError
az_error = azclierror.AzureConnectionError(error_msg)
az_error.set_recommendation(SSLERROR_TEMPLATE)

elif isinstance(ex, CloudError):
if extract_common_error_message(ex):
error_msg = extract_common_error_message(ex)
status_code = str(getattr(ex, 'status_code', 'Unknown Code'))
AzCLIErrorType = get_error_type_by_status_code(status_code)
az_error = AzCLIErrorType(error_msg)

elif isinstance(ex, ValidationError):
az_error = azclierror.ValidationError(error_msg)

elif isinstance(ex, CLIError):
# TODO: Fine-grained analysis here
az_error = azclierror.UnclassifiedUserFault(error_msg)

elif isinstance(ex, AzureError):
if extract_common_error_message(ex):
error_msg = extract_common_error_message(ex)
AzCLIErrorType = get_error_type_by_azure_error(ex)
az_error = AzCLIErrorType(error_msg)

elif isinstance(ex, AzureException):
if is_azure_connection_error(error_msg):
az_error = azclierror.AzureConnectionError(error_msg)
else:
# TODO: Fine-grained analysis here for Unknown error
az_error = azclierror.UnknownError(error_msg)

elif isinstance(ex, ClientRequestError):
if is_azure_connection_error(error_msg):
az_error = azclierror.AzureConnectionError(error_msg)
elif isinstance(ex.inner_exception, SSLError):
# When msrest encounters SSLError, msrest wraps SSLError in ClientRequestError
az_error = azclierror.AzureConnectionError(error_msg)
az_error.set_recommendation(SSLERROR_TEMPLATE)
else:
az_error = azclierror.ClientRequestError(error_msg)

elif isinstance(ex, HttpOperationError):
message, _ = extract_http_operation_error(ex)
if message:
error_msg = message
status_code = str(getattr(ex.response, 'status_code', 'Unknown Code'))
AzCLIErrorType = get_error_type_by_status_code(status_code)
az_error = AzCLIErrorType(error_msg)

elif isinstance(ex, HTTPError):
status_code = str(getattr(ex.response, 'status_code', 'Unknown Code'))
AzCLIErrorType = get_error_type_by_status_code(status_code)
az_error = AzCLIErrorType(error_msg)

elif isinstance(ex, KeyboardInterrupt):
error_msg = 'Keyboard interrupt is captured.'
az_error = azclierror.ManualInterrupt(error_msg)

else:
error_msg = "The command failed with an unexpected error. Here is the traceback:"
az_error = azclierror.CLIInternalError(error_msg)
az_error.set_exception_trace(ex)
az_error.set_recommendation("To open an issue, please run: 'az feedback'")

if isinstance(az_error, azclierror.ResourceNotFoundError):
exit_code = 3

az_error.print_error()
az_error.send_telemetry()

return exit_code
az_error = azclierror.ClientRequestError(error_msg)

elif isinstance(ex, HttpOperationError):
message, _ = extract_http_operation_error(ex)
if message:
error_msg = message
status_code = str(getattr(ex.response, 'status_code', 'Unknown Code'))
AzCLIErrorType = get_error_type_by_status_code(status_code)
az_error = AzCLIErrorType(error_msg)

elif isinstance(ex, HTTPError):
status_code = str(getattr(ex.response, 'status_code', 'Unknown Code'))
AzCLIErrorType = get_error_type_by_status_code(status_code)
az_error = AzCLIErrorType(error_msg)

elif isinstance(ex, KeyboardInterrupt):
error_msg = 'Keyboard interrupt is captured.'
az_error = azclierror.ManualInterrupt(error_msg)

else:
error_msg = "The command failed with an unexpected error. Here is the traceback:"
az_error = azclierror.CLIInternalError(error_msg)
az_error.set_exception_trace(ex)
az_error.set_recommendation("To open an issue, please run: 'az feedback'")

if isinstance(az_error, azclierror.ResourceNotFoundError):
exit_code = 3

az_error.print_error()
az_error.send_telemetry()

return exit_code


def extract_common_error_message(ex):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import os
import time
import unittest

from unittest import mock
from knack.events import EVENT_CLI_POST_EXECUTE

from azure.cli.core.azlogging import CommandLoggerContext
Expand Down Expand Up @@ -138,7 +138,7 @@ def _helper_test_log_contents_correct(self):
# check failed cli command:
data_dict = command_log_files[0].command_data_dict
self.assertTrue(data_dict["success"] is False)
self.assertEqual("There was an error during execution.", data_dict["errors"][0].strip())
self.assertEqual("The extension alias is not installed.", data_dict["errors"][0].strip())
self.assertEqual(data_dict["command_args"], "extension remove -n {}")

# check successful cli command
Expand Down Expand Up @@ -192,22 +192,18 @@ def _run_cmd(self, command, checks=None, expect_failure=False):
cli_ctx = get_dummy_cli()
cli_ctx.logging.command_log_dir = self.temp_command_log_dir

# hotfix here for untouch feedback's code to avoid introducing possible break change.
# unregister the event for auto closing CLI's file logging after execute() is done
cli_ctx.unregister_event(EVENT_CLI_POST_EXECUTE, cli_ctx.logging.deinit_cmd_metadata_logging)

# manually handle error logging as azure.cli.core.util's handle_exception function is mocked out in testsdk / patches
# this logged error tests that we can properly parse errors from command log file.
with CommandLoggerContext(logger):
result = execute(cli_ctx, command, expect_failure=expect_failure).assert_with_checks(checks)
# azure.cli.core.util.handle_exception is mocked by azure.cli.testsdk.patches.patch_main_exception_handler
# Patch it again so that errors are properly written to command log file.
from azure.cli.core.util import handle_exception
original_handle_exception = handle_exception

if result.exit_code != 0:
logger.error("There was an error during execution.")

# close it manually because we unregister the deinit_cmd_metadata_logging
# callback from EVENT_CLI_POST_EXECUTE event
cli_ctx.logging.end_cmd_metadata_logging(result.exit_code)
def _handle_exception_with_log(ex, *args, **kwargs):
with CommandLoggerContext(logger):
logger.error(ex)
original_handle_exception(*args, **kwargs)

with mock.patch('azure.cli.core.util.handle_exception', _handle_exception_with_log):
result = execute(cli_ctx, command, expect_failure=expect_failure).assert_with_checks(checks)
Comment on lines +205 to +206
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old code registers Logger as a Handler. It works simply because both Logger and Handler expose handle method by coincidence. It is never guaranteed to work as such and may break any time the internal logic of logging module changes.

return result


Expand Down