Skip to content

Rewrite the output system #345

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

Merged
merged 22 commits into from
Aug 28, 2019
Merged

Rewrite the output system #345

merged 22 commits into from
Aug 28, 2019

Conversation

masci
Copy link
Contributor

@masci masci commented Aug 13, 2019

This PR rewrites the whole logging and feedback system of the CLI. It also fixes #34

Rationale

This PR implements a blueprint where the logging system is detached from the output produced in response to user actions.

Logging

The logging system is used by low level components to leave marks at any step of the code execution and it's intended to be mainly consumed by machines. Structured logging will be encouraged and a review of what we're logging right now, the logging levels and the structure of the messages will be eventually needed. To avoid passing around a logger object, the default logrus instance is used (this hasn't changed, it was already the case before this PR).

Feedback

We call "Feedback" the system used to report an output in response to an user action. For the time being, errors go to the standard error and any other information goes to standard output. To keep consistency across the source code, a new object called Feedback was added to provide a fmt-like interface the CLI commands can use to show messages to the user. The object interface is purposely minimal and there's almost no logic. To fill the gap with logging, when an error is sent back to the user through the Feedback API, the message is also forwarded to the default logger.

User interface

To put the user in control of the logging and feedback systems, CLI options changed as following:

  • -v, --verbose option was added. When passed, the output of the logging system is displayed in the standard output. The logger output has colors to ease human browsing.
  • --log-file option was added. When passed, the logger will write messages to the given file.
  • --log-leveloption was added to control which messages the log should forward. This applies to both the standard output when-vis passed, and the file on disk when--log-file` is passed.
  • --debug was removed. The closer option to that is now arduino-cli -v --log-level debug

Additional notes

Almost 1k lines of code were removed for different reasons, most notably:

  • ascii tables are now provided by https://github.com/cheynewallace/tabby that's just 70 lines of code and relies on the standard tabwriter
  • the formatter was only partially used and very often used inconsistently (different calls for the same goal), the Formatter is now used everywhere
  • some of the "shortcuts" in output where swapped with more explicit code

@gvarisco
Copy link
Contributor

I like the rationale behind this PR. +1 from my side.

if output.JSONOrElse(platforms) {
if globals.OutputFormat == "json" {
feedback.PrintJSON(platforms)
} else {
Copy link
Member

Choose a reason for hiding this comment

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

I'll add a comment here but it's valid for all the occurrence of this snippet of course :-)

Is it better to write something like:

	if feedback.PrintJSONOrElse(platforms) {
		outputInstalledCores(platforms)
	}

or

	if globals.OutputFormat == "json" {
		feedback.PrintJSON(platforms)
	} else {
		outputInstalledCores(platforms)
	}

This is boilerplate code that is duplicated everywhere (basically on each command that may potentially output json).
Every breaking change to the internals of the feedback/globals.OutputFormat, just to say, must be propagated on all occurrences of this snippet.

In this case, IMHO, less is more.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me see if I can find a simple way to remove the boilerplate before defending it 😄

Copy link
Contributor Author

@masci masci Aug 16, 2019

Choose a reason for hiding this comment

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

Here is my proposal: let's move the responsibility to evaluate the format away from the commands by introducing a "results container" that's very thin but generic enough to support different formats and that would be used only when highly customized outputs are required.

PROS:

  • The boilerplate is gone, cfr this example with no if-s at all. As a side effect, the format can be removed from the globals.
  • This approach is future proof, any new format we should add would have very little to zero impact on the existing commands
  • When a complex formatting logic is needed, that stays with the command that needs it

CONS:

  • When the output is complex, you have to write a struct to provide the custom logic
  • the feedback package gets more responsibilities but somebody has to decide which output to use

Copy link
Member

Choose a reason for hiding this comment

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

PrintResult is defined as:

func (fb *Feedback) PrintResult(res Result) {

but IIUC we need a function that accepts any type and do a runtime check to see if the argument is a Result (unless we want to always wrap the result in a Result but this will multiply the boilerplate by 10x), so something like this:

func (fb *Feedback) PrintResult(res interface{}) {
    if r, ok := res.(Result); ok {
        ...print using JSON(r.Data()) or r.String()...
    } else {
        ...print using JSON(res) or res.String()...
    }
}

Anyway, besides the above, the issue is another: there isn't a generic way to print complex outputs.

One thing is a simple primitive type like int or string another is a Library or a Board or a []PortList, they all have their custom print with tables and verbosity that most of the time shows a subset of the available fields, sorted in some random way, so you're basically in always in the worst case (where you need to wrap inside a Result).

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 type assertion, you always get a Result that can provide a string or an interface of some sort that's supposed to be marshalled (today in JSON, tomorrow we'll see). The "boilerplate" you add by always providing a Result object is just a struct definition plus a method definition (you always have an equivalent of the String method).

Anyway, besides the above, the issue is another: there isn't a generic way to print complex outputs.
I know, I tentatively drafted a solution to see if I could remove the pattern

	if globals.OutputFormat == "json" {
		feedback.PrintJSON(ports)
	} else {
		outputListResp(ports)
	}

but I'm fine with that. Instead I'm not fine with something like JSONOrElse which is non-obvious, has a vague name and doesn't solve the root problem.

Copy link
Member

Choose a reason for hiding this comment

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

but I'm fine with that. Instead I'm not fine with something like JSONOrElse which is non-obvious, has a vague name and doesn't solve the root problem.

Ok, let's keep the bigger if then, there is no point in discussing this further, we could always go back if we change our mind ;-)

@cmaglie
Copy link
Member

cmaglie commented Aug 13, 2019

I've added two comments, BTW it's great to see the output module finally eradicated!
Will give a try to this branch ASAP!

Copy link
Contributor

@rsora rsora left a comment

Choose a reason for hiding this comment

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

Excellent work!

@masci masci merged commit 68e82ba into master Aug 28, 2019
@masci masci deleted the massi/output branch August 28, 2019 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GPIO-based DTR reset needed for upload on Raspberry Pi/UP2 board etc.
4 participants