Skip to content

Shouty errors - Highly verbose guidance when errors happen #146

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 3 commits into from
Feb 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ func main() {
Usage: "Disable all desktop notifications",
EnvVar: "RIG_NOTIFY_QUIET",
},
cli.BoolFlag{
Name: "power-user",
Usage: "Switch power-user mode on for quieter help output.",
EnvVar: "RIG_POWER_USER_MODE",
},
}

app.Before = func(c *cli.Context) error {
Expand Down
5 changes: 4 additions & 1 deletion commands/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ func (cmd *BaseCommand) Failure(message string, errorName string, exitCode int)
cmd.out.NoSpin()
// Handle error messaging.
util.NotifyError(cmd.context, message)

// Print expanded troubleshooting guidance.
if !cmd.context.GlobalBool("power-user") {
util.PrintDebugHelp(message, errorName, exitCode)
}
return cli.NewExitError(fmt.Sprintf("ERROR: %s [%s] (%d)", message, errorName, exitCode), exitCode)
}

Expand Down
112 changes: 112 additions & 0 deletions util/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package util

import (
"bytes"
"fmt"
"os"
"runtime"
"strings"

"github.com/fatih/color"
)

// PrintDebugHelp provides expanded troubleshooting help content for an error.
// It is primarily called by command.go:Failure().
// @todo consider switching this to a template.
func PrintDebugHelp(message, errorName string, exitCode int) {
header := color.New(color.FgYellow).Add(color.Underline).PrintlnFunc()
red := color.New(color.FgRed).PrintlnFunc()
code := color.New(color.BgHiBlack).PrintfFunc()

header(StringPad(fmt.Sprintf("Error [%s]", errorName), " ", 80))
fmt.Println()
red(color.RedString(message))

var codeMessage string
switch exitCode {
case 12:
codeMessage = "environmental"
case 13:
codeMessage = "external/upstream command"
default:
codeMessage = "general"
}
fmt.Println()
fmt.Printf("This is a %s error.\n", codeMessage)
fmt.Println()

header(StringPad("Debugging Help", " ", 80))
fmt.Println()
if !Logger().IsVerbose {
fmt.Println("Run again in verbose mode:")
fmt.Println()
line := fmt.Sprintf("%s --verbose %s", os.Args[0], strings.Join(os.Args[1:], " "))
code("\t %s", StringPad("", " ", len(line)+1))
fmt.Println()
code("\t %s", StringPad(line, " ", len(line)+1))
fmt.Println()
code("\t %s", StringPad("", " ", len(line)+1))
fmt.Println()
fmt.Println()
}
fmt.Println("Ask the doctor for a general health check:")
fmt.Println()
line := "rig doctor"
code("\t %s", StringPad("", " ", len(line)+1))
fmt.Println()
code("\t %s", StringPad(line, " ", len(line)+1))
fmt.Println()
code("\t %s", StringPad("", " ", len(line)+1))
fmt.Println()
fmt.Println()

header(StringPad("Get Support", " ", 80))
fmt.Println()
fmt.Printf("To search for related issues or documentation use the error ID '%s'.\n", errorName)
fmt.Println()
fmt.Println("\tDocs:\t\thttp://docs.outrigger.sh")
fmt.Println("\tIssues:\t\thttps://github.com/phase2/rig/issues")
fmt.Println("\tChat:\t\thttp://slack.outrigger.sh/")
fmt.Println()

header(StringPad("Your Environment Information", " ", 80))
fmt.Println()
// Verbose output is distracting in this help output.
Logger().SetVerbose(false)
fmt.Println("\tOperating System:\t\t", runtime.GOOS)
fmt.Println("\tdocker version:\t\t\t", GetCurrentDockerVersion())
fmt.Println("\tdocker client API version:\t", GetDockerClientAPIVersion())
if version, err := GetDockerServerAPIVersion(); err == nil {
fmt.Println("\tdocker server API version:\t", version)
} else {
fmt.Println("\tdocker server API version:\t", err.Error())
}
fmt.Println(color.CyanString("\nPlease include the 'Error' and 'Your Environment Information' sections in bug reports."))
fmt.Println()
fmt.Println("To disable the extended troubleshooting output, run with --power-user or RIG_POWER_USER_MODE=1")
fmt.Println()
}

// StringPad takes your string and returns it with the pad value repeatedly
// appended until it is the intended length. Note that if the delta between
// the initial string length and the intended size is not evenly divisible by
// the pad length, your final string could be slightly short -- partial padding
// is not applied. For guaranteed results, use a pad string of length 1.
func StringPad(s string, pad string, size int) string {
length := len(s)
if length < size {
var buffer bytes.Buffer
padLength := len(pad)
delta := size - length
iterations := delta / padLength

buffer.WriteString(s)
for i := 0; i <= iterations; i += padLength {
buffer.WriteString(pad)
}

return buffer.String()
}

return s
}
28 changes: 22 additions & 6 deletions util/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,13 @@ type RigSpinner struct {

// LoggerInit initializes the global logger
func LoggerInit(verbose bool) {
var verboseWriter = ioutil.Discard
if verbose {
verboseWriter = os.Stdout
}

s, _ := spun.NewSpinner(spun.Dots)
logger = &RigLogger{
Channel: logChannels{
Info: log.New(os.Stdout, color.BlueString("[INFO] "), 0),
Warning: log.New(os.Stdout, color.YellowString("[WARN] "), 0),
Error: log.New(os.Stderr, color.RedString("[ERROR] "), 0),
Verbose: log.New(verboseWriter, "[VERBOSE] ", 0),
Verbose: deriveVerboseLogChannel(verbose),
},
IsVerbose: verbose,
Progress: &RigSpinner{s},
Expand All @@ -68,6 +63,27 @@ func Logger() *RigLogger {
return logger
}

// deriveVerboseLogChannel determines if and how verbose logs are used by
// creating the log channel they are routed through. This must be attached to
// a RigLogger as the value for Channel.Verbose. It is extracted into a function
// to support SetVerbose().
func deriveVerboseLogChannel(verbose bool) *log.Logger {
verboseWriter := ioutil.Discard
if verbose {
verboseWriter = os.Stdout
}
return log.New(verboseWriter, "[VERBOSE] ", 0)
}

// SetVerbose allows toggling verbose mode mid-execution of the program.
func (log *RigLogger) SetVerbose(verbose bool) {
if log.IsVerbose == verbose {
return
}

log.Channel.Verbose = deriveVerboseLogChannel(verbose)
}

// Spin restarts the spinner for a new task.
func (log *RigLogger) Spin(message string) {
if !log.IsVerbose {
Expand Down