Skip to content

Conversation

@houk-ms
Copy link
Contributor

@houk-ms houk-ms commented Dec 14, 2020

Description

This PR applies the new error output to support the feature requests #15693.

1. The UI changes of new error output includes

  • the new output UI designs
  • removing error types
  • multiple recommended commands (at most 3 commands + 1 reference link)
  • descriptions of the recommended commands
  • colorizations
  • command group level hyperlinks

An overview of the new error output.

image

2. The none-UI changes

  • Set core.disable_error_recommendation=true with az config to disable recommendations
  • The recommended commands are re-ordered by parameter matching with user's input
  • The recommended commands are eithe all from Aladdin service or all from examples, in avoid of duplicate commands
  • Contextual values are disabled to replace the placeholders as it will make command description not valid.

Testing Guide

Type any command that will triger the client side userfault. E.g, the command below, you shold see the output above.

az vm create -n test-vm

Disable the error recommendation and then test the same command, you could see the recommendations are disabled.

az config set core.error_recommendation=off
az vm create -n test-vm

image

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 14, 2020

Wait for service's readiness.

@yonzhan
Copy link
Collaborator

yonzhan commented Dec 14, 2020

Which parts are pending on Service?

@houk-ms houk-ms changed the title [Core] {Draft} New error output [Core] New error output Dec 15, 2020
@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 15, 2020

Which parts are pending on Service?

Aladdin service's new response :)

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 15, 2020

Ready for review.

Aladdin endpoint is for testing now and needs to be replaced.

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 15, 2020

@chenlomis for awareness and possible comments.

@christopher-o-toole

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 15, 2020

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@houk-ms houk-ms requested a review from yungezz December 15, 2020 09:13
@chenlomis
Copy link

@chenlomis for awareness and possible comments.

@christopher-o-toole

Very cool! Thank you for putting this together!

Quick questions:

What's the diff for the following 2 commands aside from the fact that one has optional tag and the other doesn't?
Is there an easy way to dedupe them by any chance?
image

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 16, 2020

Quick questions:

What's the diff for the following 2 commands aside from the fact that one has optional tag and the other doesn't?
Is there an easy way to dedupe them by any chance?
image

Thanks @chenlomis for the comments. These are the recommended commands from Aladdin service, @christopher-o-toole may want to say somthing here?

For me, I think they are not duplicate commands for users. Because for every command, it always has a minimum set of arguments that could make it work. And all other argument combinations need to include these necessary arguments. We just do more advanced customizations with more arguments.

Take vm create as an example, we have many help examples shown to users, while they all have different scenarios.

    Create a default Ubuntu VM with automatic SSH authentication.
        az vm create -n MyVm -g MyResourceGroup --image UbuntuLTS


    Create a default RedHat VM with automatic SSH authentication using an image URN.
        az vm create -n MyVm -g MyResourceGroup --image RedHat:RHEL:7-RAW:7.4.2018010506


    Create a default Windows Server VM with a private IP address.
        az vm create -n MyVm -g MyResourceGroup --public-ip-address "" --image Win2012R2Datacenter


    Create a VM from a custom managed image.
        az vm create -g MyResourceGroup -n MyVm --image MyImage


    Create a VM from a specialized image version.
        az vm create -g MyResourceGroup -n MyVm --image $id --specialized


    Create a VM by attaching to a managed operating system disk.
        az vm create -g MyResourceGroup -n MyVm --attach-os-disk MyOsDisk --os-type linux


    Create an Ubuntu Linux VM using a cloud-init script for configuration. See:
    https://docs.microsoft.com/azure/virtual-machines/linux/using-cloud-init.
        az vm create -g MyResourceGroup -n MyVm --image debian --custom-data MyCloudInitScript.yml

@chenlomis
Copy link

chenlomis commented Dec 16, 2020

Quick questions:
What's the diff for the following 2 commands aside from the fact that one has optional tag and the other doesn't?
Is there an easy way to dedupe them by any chance?
image

Thanks @chenlomis for the comments. These are the recommended commands from Aladdin service, @christopher-o-toole may want to say somthing here?

For me, I think they are not duplicate commands for users. Because for every command, it always has a minimum set of arguments that could make it work. And all other argument combinations need to include these necessary arguments. We just do more advanced customizations with more arguments.

Take vm create as an example, we have many help examples shown to users, while they all have different scenarios.

    Create a default Ubuntu VM with automatic SSH authentication.
        az vm create -n MyVm -g MyResourceGroup --image UbuntuLTS


    Create a default RedHat VM with automatic SSH authentication using an image URN.
        az vm create -n MyVm -g MyResourceGroup --image RedHat:RHEL:7-RAW:7.4.2018010506


    Create a default Windows Server VM with a private IP address.
        az vm create -n MyVm -g MyResourceGroup --public-ip-address "" --image Win2012R2Datacenter


    Create a VM from a custom managed image.
        az vm create -g MyResourceGroup -n MyVm --image MyImage


    Create a VM from a specialized image version.
        az vm create -g MyResourceGroup -n MyVm --image $id --specialized


    Create a VM by attaching to a managed operating system disk.
        az vm create -g MyResourceGroup -n MyVm --attach-os-disk MyOsDisk --os-type linux


    Create an Ubuntu Linux VM using a cloud-init script for configuration. See:
    https://docs.microsoft.com/azure/virtual-machines/linux/using-cloud-init.
        az vm create -g MyResourceGroup -n MyVm --image debian --custom-data MyCloudInitScript.yml

Thank you for the elaboration! @houk-ms
I like your vm examples. It's interesting though because all these vm variations have different descriptions... which do help me as the end user to understand that each command fulfills a different need and that each additional parameter would alter what the original core vm command does

In the case of the screenshot though, despite the 2nd command having an additional --tags param, the descriptions are the same

So imagine someone coming in genuinely seeking for next steps, how would (s)he know which one to pick....(?) And if (s)he doesn't know and can't follow through, then doesn't having both of them there cause additional confusion?


Am actually open to keeping both as long as they mean and do different things, and that the differences are reflected in the description

maybe that's a Q for @christopher-o-toole ?

print(recommendation, file=sys.stderr)

if self.aladdin_recommendations:
print('\nTRY THIS:', file=sys.stderr)
Copy link
Member

@jiasli jiasli Dec 16, 2020

Choose a reason for hiding this comment

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

We can use print_styled_text("\nTRY THIS:") or print_styled_text((Style.PRIMARY, "\nTRY THIS:")) once #16258 is merged.

if self.aladdin_recommendations:
print('\nTRY THIS:', file=sys.stderr)
for recommendation, description in self.aladdin_recommendations:
print_styled_text(recommendation, file=sys.stderr)
Copy link
Member

Choose a reason for hiding this comment

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

sys.stderr is now the default value for file.

return [candidate['recommendation'] for candidate in candidates]

return candidates
def decorate_command(raw_command):
Copy link
Member

Choose a reason for hiding this comment

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

The functionality for colorization can be extracted and put in style.py as a common util.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 17, 2020

@chenlomis If we simply replace the contextual values, there are cases that we will make users confused because the description are not changed accordingly.What do you think about this?

@christopher-o-toole and I tend to keep the original placeholders in the recommendations before we could find a proper method to solve it.

image

@chenlomis
Copy link

One more comment on why I insisted on "deduping the recommendations"

Below are some customer interview raw note, folks did have a reaction to seeing seemingly similar recommendations:

image
image
image

if the 2nd rec with --tags actually do offer something more unique, then we shall totally keep it
if it doesn't... it'd be absolutely amazing if there's a way to dedupe the top hand curated rec and Aladdin's rec

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 18, 2020

@houk-ms

Hrmm that's some interesting call out...
May I ask how the descriptions are generated?
Would it be possible to replace "West US region" in the description to "a region"?
I prefer keeping the command specific over keeping the description specific...Also think the more we can contextualize the recommendations the better... but up for debate

And if the above is not possible... is it going to show --location {samplelocations} where the description still shows "West US region"? because that doesn't seem too accurate either...

It's difficult to adjust the description, since it's more about semantic annlysis than a simple replacement. Let me give a more complex case

The original recommendations
image

When we have replacement
image

Anyway, i think there are even more complicated cases ...
Ideas on this? @chenlomis

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 18, 2020

One more comment on why I insisted on "deduping the recommendations"

For the commands deduping, i think @christopher-o-toole will keep improving this

@chenlomis
Copy link

@houk-ms
Hrmm that's some interesting call out...
May I ask how the descriptions are generated?
Would it be possible to replace "West US region" in the description to "a region"?
I prefer keeping the command specific over keeping the description specific...Also think the more we can contextualize the recommendations the better... but up for debate
And if the above is not possible... is it going to show --location {samplelocations} where the description still shows "West US region"? because that doesn't seem too accurate either...

It's difficult to adjust the description, since it's more about semantic annlysis than a simple replacement. Let me give a more complex case

The original recommendations
image

When we have replacement
image

Anyway, i think there are even more complicated cases ...
Ideas on this? @chenlomis

Oh I see how the the description is "locked" with the original recommendation now...
and the variables in the descriptions aren't contextual like the command's...

Hmm I don't know how easy it is to implement, and it's probably a conversation for future releases, but is it possible to run the description through some form of syntactic analyzer and identifies the corresponding matching words with the command's values and set those as variables before displaying it here? That way when the values of the commands are replaced/updated, the descriptions will also get updated

Otherwise I'd upvote for keeping the original description more general, so that it doesn't create confusion even if we replace/update the command values
Similar to the description "Read more about the command in reference docs" for the reference link

If that's not possible either.. (since it also requires updates to the description), then perhaps we should go with what you suggested earlier which is to keep the values non contextual for this release, until we figure out a workaround to update the description as well (maybe next release?). Because otherwise there'd be a mismatch between the the commands and the description

@houk-ms @christopher-o-toole thoughts?

@houk-ms
Copy link
Contributor Author

houk-ms commented Dec 21, 2020

Oh I see how the the description is "locked" with the original recommendation now...
and the variables in the descriptions aren't contextual like the command's...

Hmm I don't know how easy it is to implement, and it's probably a conversation for future releases, but is it possible to run the description through some form of syntactic analyzer and identifies the corresponding matching words with the command's values and set those as variables before displaying it here? That way when the values of the commands are replaced/updated, the descriptions will also get updated

Otherwise I'd upvote for keeping the original description more general, so that it doesn't create confusion even if we replace/update the command values
Similar to the description "Read more about the command in reference docs" for the reference link

If that's not possible either.. (since it also requires updates to the description), then perhaps we should go with what you suggested earlier which is to keep the values non contextual for this release, until we figure out a workaround to update the description as well (maybe next release?). Because otherwise there'd be a mismatch between the the commands and the description

@houk-ms @christopher-o-toole thoughts?

Synched with @christopher-o-toole , seems both of the methods related with description adjustments are not easy. Let's keep the original values in this release and leave more time to figure out the better way.

@houk-ms houk-ms requested a review from jiasli December 21, 2020 09:27
error_type = AladdinUserFaultType.StorageAccountNotFound
elif 'resource_group' in error_msg or 'resource group' in error_msg:
error_type = AladdinUserFaultType.ResourceGroupNotFound
elif 'pattern' in error_msg or 'is not a valid value' in error_msg or 'invalid' in error_msg:
Copy link
Member

Choose a reason for hiding this comment

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

Is it enough to just match pattern instead of somethign like invalid pattern

Copy link
Contributor Author

@houk-ms houk-ms Dec 22, 2020

Choose a reason for hiding this comment

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

This are the rules provided by Aladdin Team for their internal error categories used to train model. I think it's ok as long as it makes sense to them.

return ''.join(formatted_parts)


def get_styled_command(raw_command):
Copy link
Member

Choose a reason for hiding this comment

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

We can call this function highlight_command, just as pygments does in Knack:

https://github.com/microsoft/knack/blob/4cb4c2e0516f760b08defa8b9afacc004f7e4fac/knack/output.py#L44-L46

def format_json_color(obj):
    from pygments import highlight, lexers, formatters
    return highlight(format_json(obj), lexers.JsonLexer(), formatters.TerminalFormatter())  # pylint: disable=no-member

Copy link
Contributor

@zhoxing-ms zhoxing-ms Dec 24, 2020

Choose a reason for hiding this comment

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

I agree with jiasli.

Moreover, as far as I know that the UX design from Lomis adds the <> to wrap the parameter values. Such as

az webapp log config --name <WebappName> --resource-group <ResourceGroup> --detailed-error-messages <True or False>

Because only the color of each part is returned here, and the style content of text is not processed, I think the function of the style content of text processing also needs another separate and general method. (That method might call this method to get the color and then modify the content style of text)

So the naming of the method should be more accurate to distinguish the two methods with different processing degrees.

Copy link
Member

Choose a reason for hiding this comment

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

Let's move this discussion to #16349 which is solely for the highlighting functionality.

styled_command = []
argument_begins = False

for index, arg in enumerate(raw_command.split()):
Copy link
Member

Choose a reason for hiding this comment

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

While str.split works here, shlex.split is safer which doesn't splits spaces within quoted text, like "some quoted text".

self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

str.split works fine here, except for the cases where we have space in the command name. shlex.split will remove the quotation marks in the command thus bring more complexity when we join the command args.

Let's use str.split here @zhoxing-ms for awareness

spaced_arg = ' {}'.format(arg) if index > 0 else arg
style = Style.PRIMARY

if arg.startswith('-') and '=' not in arg:
Copy link
Member

Choose a reason for hiding this comment

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

The logic of = handling is a little bit tricky. It works for az config set a=b, but it doesn't work for az config unset output. It is the nature of positional argument that we can NOT decide whether an arg is

  • part of a command, or
  • a positional argument

until semantic analysis is involved.

Let's keep it simple and treat it as part of the command by now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree. @zhoxing-ms for awareness.

return ''.join(formatted_parts)


def get_styled_command(raw_command):
Copy link
Member

Choose a reason for hiding this comment

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

How about we add some function doc and give some examples about the behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, moved to #16349

Comment on lines +48 to +67
if 'unrecognized' in error_msg:
error_type = AladdinUserFaultType.UnrecognizedArguments
elif 'expected one argument' in error_msg or 'expected at least one argument' in error_msg \
or 'value required' in error_msg:
error_type = AladdinUserFaultType.ExpectedArgument
elif 'misspelled' in error_msg:
error_type = AladdinUserFaultType.UnknownSubcommand
elif 'arguments are required' in error_msg or 'argument required' in error_msg:
error_type = AladdinUserFaultType.MissingRequiredParameters
if '_subcommand' in error_msg:
error_type = AladdinUserFaultType.MissingRequiredSubcommand
elif '_command_package' in error_msg:
error_type = AladdinUserFaultType.UnableToParseCommandInput
elif 'not found' in error_msg or 'could not be found' in error_msg \
or 'resource not found' in error_msg:
error_type = AladdinUserFaultType.AzureResourceNotFound
if 'storage_account' in error_msg or 'storage account' in error_msg:
error_type = AladdinUserFaultType.StorageAccountNotFound
elif 'resource_group' in error_msg or 'resource group' in error_msg:
error_type = AladdinUserFaultType.ResourceGroupNotFound
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we make the mapping relationship between these error messages and error codes into a dict, and then match the error types directly through the dict?
So we don't need to write so many judgment statements, and the code may be more readable
Such as:

error_type_dict = {
    'not found': AladdinUserFaultType.UnableToParseCommandInput,
    'misspelled': AladdinUserFaultType.UnknownSubcommand,
    'arguments are required': {
        '_subcommand': AladdinUserFaultType.MissingRequiredSubcommand,
        '_command_package': AladdinUserFaultType.UnableToParseCommandInput
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a good idea!

But i am afraid it may bring extra complexity. For example, the statements here need 15 key-value pairs?

elif 'not found' in error_msg or 'could not be found' in error_msg \
            or 'resource not found' in error_msg:
        error_type = AladdinUserFaultType.AzureResourceNotFound
        if 'storage_account' in error_msg or 'storage account' in error_msg:
            error_type = AladdinUserFaultType.StorageAccountNotFound
        elif 'resource_group' in error_msg or 'resource group' in error_msg:
            error_type = AladdinUserFaultType.ResourceGroupNotFound

Besides, can we deal with the default logic? E.g, the error_type = AladdinUserFaultType.AzureResourceNotFound above?

Copy link
Contributor

@zhoxing-ms zhoxing-ms Dec 28, 2020

Choose a reason for hiding this comment

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

For example, the statements here need 15 key-value pairs?

Yes, this makes it easier to find and modify the error type corresponding to an error message

error_type_dict = {
    'not found': {
        "default error": AladdinUserFaultType.UnableToParseCommandInput,   // default error type
        "storage_account": AladdinUserFaultType.StorageAccountNotFound,
        "storage account": AladdinUserFaultType.StorageAccountNotFound,
        "resource_group": AladdinUserFaultType.ResourceGroupNotFound,
        "resource group": AladdinUserFaultType.ResourceGroupNotFound
    },
    ...
}

In fact, the essential difference between the two ways is that one is aggregated according to the error message and the other is aggregated according to the error type returned.
I personally think that we are more concerned about how to get the error type according to error_msg rather than what error_msg corresponds to an error type.

Comment on lines +214 to +217
The sorting rules below are applied in order:
1. Commands starting with the user's input command name are ahead of those don't
2. Commands having more matched arguments are ahead of those having less
3. Commands having less arguments are ahead of those having more
Copy link
Contributor

@zhoxing-ms zhoxing-ms Dec 24, 2020

Choose a reason for hiding this comment

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

Does Aladdin recommendation service have its own ranking rules?
I personally think that the content of relevance ranking might be better implemented by the recommendation server side.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, we discussed with Aladdin team about this. We agreed that before the service could return better ordered recommendtions, we would control the orders in client side.

What's more, the rules will also be applied to example recommendations.

Comment on lines 412 to 421
param_mappings = {
'-h': '--help',
'-o': '--output',
'--only-show-errors': None,
'--help': None,
'--output': None,
'--query': None,
'--debug': None,
'--verbose': None
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What kind of parameters need to be put here?
How about --yes, --no-wait?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a good catch, let me confirm. I think we need to add all the global parameters here.

if self.aladdin_recommendations:
telemetry.set_debug_info('AladdinRecommendCommand', ';'.join(raw_commands))
else:
telemetry.set_debug_info('ExampleRecommendCommand', ';'.join(raw_commands))
Copy link
Member

Choose a reason for hiding this comment

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

will this contain user input value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, these are the raw recommendations.

3. In testing environments
:return: whether Aladdin service need to be disabled or not
:type: bool
Copy link
Member

Choose a reason for hiding this comment

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

in 2,3, we could still provide recommendation from help right? Seems need to separate it in command_recommender

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They have already been separated, we could provide recommendations from help examples even though Aladdin service is disabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants