...

Source file src/github.com/linkerd/linkerd2/pkg/healthcheck/healthcheck_output.go

Documentation: github.com/linkerd/linkerd2/pkg/healthcheck

     1  package healthcheck
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/briandowns/spinner"
    13  	"github.com/fatih/color"
    14  	"github.com/mattn/go-isatty"
    15  )
    16  
    17  const (
    18  	// JSONOutput is used to specify the json output format
    19  	JSONOutput = "json"
    20  	// TableOutput is used to specify the table output format
    21  	TableOutput = "table"
    22  	// WideOutput is used to specify the wide output format
    23  	WideOutput = "wide"
    24  	// ShortOutput is used to specify the short output format
    25  	ShortOutput = "short"
    26  
    27  	// DefaultHintBaseURL is the default base URL on the linkerd.io website
    28  	// that all check hints for the latest linkerd version point to. Each
    29  	// check adds its own `hintAnchor` to specify a location on the page.
    30  	DefaultHintBaseURL = "https://linkerd.io/2/checks/#"
    31  )
    32  
    33  var (
    34  	okStatus   = color.New(color.FgGreen, color.Bold).SprintFunc()("\u221A")  // √
    35  	warnStatus = color.New(color.FgYellow, color.Bold).SprintFunc()("\u203C") // ‼
    36  	failStatus = color.New(color.FgRed, color.Bold).SprintFunc()("\u00D7")    // ×
    37  
    38  	reStableVersion = regexp.MustCompile(`stable-(\d\.\d+)\.`)
    39  )
    40  
    41  // Checks describes the "checks" field on a CheckCLIOutput
    42  type Checks string
    43  
    44  const (
    45  	// ExtensionMetadataSubcommand is the subcommand name an extension must
    46  	// support in order to provide config metadata to the "linkerd" CLI.
    47  	ExtensionMetadataSubcommand = "_extension-metadata"
    48  
    49  	// Always run the check, regardless of cluster state
    50  	Always Checks = "always"
    51  	// // TODO:
    52  	// // Cluster informs "linkerd check" to only run this extension if there are
    53  	// // on-cluster resources.
    54  	// Cluster Checks = "cluster"
    55  	// // Never informs "linkerd check" to never run this extension.
    56  	// Never Checks = "never"
    57  )
    58  
    59  // ExtensionMetadataOutput contains the output of a _extension-metadata subcommand.
    60  type ExtensionMetadataOutput struct {
    61  	Name   string `json:"name"`
    62  	Checks Checks `json:"checks"`
    63  }
    64  
    65  // CheckResults contains a slice of CheckResult structs.
    66  type CheckResults struct {
    67  	Results []CheckResult
    68  }
    69  
    70  // CheckOutput groups the check results for all categories
    71  type CheckOutput struct {
    72  	Success    bool             `json:"success"`
    73  	Categories []*CheckCategory `json:"categories"`
    74  }
    75  
    76  // CheckCategory groups a series of check for a category
    77  type CheckCategory struct {
    78  	Name   CategoryID `json:"categoryName"`
    79  	Checks []*Check   `json:"checks"`
    80  }
    81  
    82  // Check is a user-facing version of `healthcheck.CheckResult`, for output via
    83  // `linkerd check -o json`.
    84  type Check struct {
    85  	Description string         `json:"description"`
    86  	Hint        string         `json:"hint,omitempty"`
    87  	Error       string         `json:"error,omitempty"`
    88  	Result      CheckResultStr `json:"result"`
    89  }
    90  
    91  // RunChecks submits each of the individual CheckResult structs to the given
    92  // observer.
    93  func (cr CheckResults) RunChecks(observer CheckObserver) (bool, bool) {
    94  	success := true
    95  	warning := false
    96  	for _, result := range cr.Results {
    97  		result := result // Copy loop variable to make lint happy.
    98  		if result.Err != nil {
    99  			if !result.Warning {
   100  				success = false
   101  			} else {
   102  				warning = true
   103  			}
   104  		}
   105  		observer(&result)
   106  	}
   107  	return success, warning
   108  }
   109  
   110  // PrintChecksResult writes the checks result.
   111  func PrintChecksResult(wout io.Writer, output string, success bool, warning bool) {
   112  	if output == JSONOutput {
   113  		return
   114  	}
   115  
   116  	switch success {
   117  	case true:
   118  		fmt.Fprintf(wout, "Status check results are %s\n", okStatus)
   119  	case false:
   120  		fmt.Fprintf(wout, "Status check results are %s\n", failStatus)
   121  	}
   122  }
   123  
   124  // RunChecks runs the checks that are part of hc
   125  func RunChecks(wout io.Writer, werr io.Writer, hc Runner, output string) (bool, bool) {
   126  	if output == JSONOutput {
   127  		return runChecksJSON(wout, werr, hc)
   128  	}
   129  
   130  	return runChecksTable(wout, hc, output)
   131  }
   132  
   133  func runChecksTable(wout io.Writer, hc Runner, output string) (bool, bool) {
   134  	var lastCategory CategoryID
   135  	spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
   136  	spin.Writer = wout
   137  
   138  	// We set up different printing functions because we need to handle
   139  	// 2 check formatting output use cases:
   140  	//  1. the default check output in `table` format
   141  	//  2. the summarized output in `short` format
   142  	prettyPrintResults := func(result *CheckResult) {
   143  		lastCategory = printCategory(wout, lastCategory, result)
   144  
   145  		spin.Stop()
   146  		if result.Retry {
   147  			restartSpinner(spin, result)
   148  			return
   149  		}
   150  
   151  		status := getResultStatus(result)
   152  
   153  		printResultDescription(wout, status, result)
   154  	}
   155  
   156  	prettyPrintResultsShort := func(result *CheckResult) {
   157  		// bail out early and skip printing if we've got an okStatus
   158  		if result.Err == nil {
   159  			return
   160  		}
   161  
   162  		lastCategory = printCategory(wout, lastCategory, result)
   163  
   164  		spin.Stop()
   165  		if result.Retry {
   166  			restartSpinner(spin, result)
   167  			return
   168  		}
   169  
   170  		status := getResultStatus(result)
   171  
   172  		printResultDescription(wout, status, result)
   173  	}
   174  
   175  	var (
   176  		success bool
   177  		warning bool
   178  	)
   179  	switch output {
   180  	case ShortOutput:
   181  		success, warning = hc.RunChecks(prettyPrintResultsShort)
   182  	default:
   183  		success, warning = hc.RunChecks(prettyPrintResults)
   184  	}
   185  
   186  	// This ensures there is a newline separating check categories from each
   187  	// other as well as the check result. When running in ShortOutput mode and
   188  	// there are no warnings, there is no newline printed.
   189  	if output != ShortOutput || !success || warning {
   190  		fmt.Fprintln(wout)
   191  	}
   192  
   193  	return success, warning
   194  }
   195  
   196  // CheckResultStr is a string describing the result of a check
   197  type CheckResultStr string
   198  
   199  const (
   200  	CheckSuccess CheckResultStr = "success"
   201  	CheckWarn    CheckResultStr = "warning"
   202  	CheckErr     CheckResultStr = "error"
   203  )
   204  
   205  func runChecksJSON(wout io.Writer, werr io.Writer, hc Runner) (bool, bool) {
   206  	var categories []*CheckCategory
   207  
   208  	collectJSONOutput := func(result *CheckResult) {
   209  		if categories == nil || categories[len(categories)-1].Name != result.Category {
   210  			categories = append(categories, &CheckCategory{
   211  				Name:   result.Category,
   212  				Checks: []*Check{},
   213  			})
   214  		}
   215  
   216  		if !result.Retry {
   217  			currentCategory := categories[len(categories)-1]
   218  			// ignore checks that are going to be retried, we want only final results
   219  			status := CheckSuccess
   220  			if result.Err != nil {
   221  				status = CheckErr
   222  				if result.Warning {
   223  					status = CheckWarn
   224  				}
   225  			}
   226  
   227  			currentCheck := &Check{
   228  				Description: result.Description,
   229  				Result:      status,
   230  			}
   231  
   232  			if result.Err != nil {
   233  				currentCheck.Error = result.Err.Error()
   234  
   235  				if result.HintURL != "" {
   236  					currentCheck.Hint = result.HintURL
   237  				}
   238  			}
   239  			currentCategory.Checks = append(currentCategory.Checks, currentCheck)
   240  		}
   241  	}
   242  
   243  	success, warning := hc.RunChecks(collectJSONOutput)
   244  
   245  	outputJSON := CheckOutput{
   246  		Success:    success,
   247  		Categories: categories,
   248  	}
   249  
   250  	resultJSON, err := json.MarshalIndent(outputJSON, "", "  ")
   251  	if err == nil {
   252  		fmt.Fprintf(wout, "%s\n", string(resultJSON))
   253  	} else {
   254  		fmt.Fprintf(werr, "JSON serialization of the check result failed with %s", err)
   255  	}
   256  	return success, warning
   257  }
   258  
   259  func printResultDescription(wout io.Writer, status string, result *CheckResult) {
   260  	fmt.Fprintf(wout, "%s %s\n", status, result.Description)
   261  
   262  	if result.Err == nil {
   263  		return
   264  	}
   265  
   266  	fmt.Fprintf(wout, "    %s\n", result.Err)
   267  	if result.HintURL != "" {
   268  		fmt.Fprintf(wout, "    see %s for hints\n", result.HintURL)
   269  	}
   270  }
   271  
   272  func getResultStatus(result *CheckResult) string {
   273  	status := okStatus
   274  	if result.Err != nil {
   275  		status = failStatus
   276  		if result.Warning {
   277  			status = warnStatus
   278  		}
   279  	}
   280  
   281  	return status
   282  }
   283  
   284  func restartSpinner(spin *spinner.Spinner, result *CheckResult) {
   285  	if isatty.IsTerminal(os.Stdout.Fd()) {
   286  		spin.Suffix = fmt.Sprintf(" %s", result.Err)
   287  		spin.Color("bold") // this calls spin.Restart()
   288  	}
   289  }
   290  
   291  func printCategory(wout io.Writer, lastCategory CategoryID, result *CheckResult) CategoryID {
   292  	if lastCategory == result.Category {
   293  		return lastCategory
   294  	}
   295  
   296  	if lastCategory != "" {
   297  		fmt.Fprintln(wout)
   298  	}
   299  
   300  	fmt.Fprintln(wout, result.Category)
   301  	fmt.Fprintln(wout, strings.Repeat("-", len(result.Category)))
   302  
   303  	return result.Category
   304  }
   305  
   306  // HintBaseURL returns the base URL on the linkerd.io website that check hints
   307  // point to, depending on the version
   308  func HintBaseURL(ver string) string {
   309  	stableVersion := reStableVersion.FindStringSubmatch(ver)
   310  	if stableVersion == nil {
   311  		return DefaultHintBaseURL
   312  	}
   313  	return fmt.Sprintf("https://linkerd.io/%s/checks/#", stableVersion[1])
   314  }
   315  

View as plain text