sourcegraph/dev/sg/sg_doctor.go
Bolaji Olajide 4d57eb1188
fix(sg): make sg gen output more readable (#64406)
Closes DINF-78

The output of `sg gen` is a bit hard to read when there's an error, this
is because the new line character `\n` isn't rendered as a new line. It
turns out the `%q` formatting directive used to quote a string doesn't
render the `\n` character as a new line.

| Before |
|---|
| ![CleanShot 2024-08-12 at 11 17
57@2x](https://github.com/user-attachments/assets/e03ec503-e437-4b68-80b3-fe34ac8848fb)
|

| After  |
|---|
| ![CleanShot 2024-08-12 at 10 53
35@2x](https://github.com/user-attachments/assets/5b7aac63-27b6-4de0-9c56-3b739f0ee0f9)
|

I also added a func to extract error messages from a bazel command to
avoid long output message when a bazel command fails and give the user
relevant messages related to the error.

| Before  |
|---|


https://github.com/user-attachments/assets/2d029ec1-5804-41bf-a675-8642e169ea80


| After  |
|---|
| ![CleanShot 2024-08-12 at 14 45
59@2x](https://github.com/user-attachments/assets/7d567fd6-de37-48aa-b2b5-03dc591fc77a)
|

## Test plan

<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->

* Manual testing

## Changelog

<!-- OPTIONAL; info at
https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c
-->
2024-08-13 06:22:21 -05:00

164 lines
4.2 KiB
Go

package main
import (
"bytes"
"context"
_ "embed"
"fmt"
"os"
"slices"
"strings"
"time"
"github.com/urfave/cli/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/category"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/run"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/std"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/output"
)
var doctorCommand = &cli.Command{
Name: "doctor",
Usage: "performs diagnostics of the local environment and prints out a report",
Description: `Runs a series of commands defined in sg-doctor.yaml.
The output of the commands are stored in a report, which can then be given to a dev-infra team memeber for
further diagnosis.
`,
Category: category.Util,
Action: runDoctorDiagnostics,
}
//go:embed sg.doctor.yaml
var doctorYaml []byte
type Diagnostic struct {
Name string `yaml:"name"`
Cmd string `yaml:"cmd"`
}
type Diagnostics struct {
Diagnostic map[string][]Diagnostic `yaml:"diagnostics"`
}
type diagnosticRunner struct {
diagnostics *Diagnostics
reporter *std.Output
}
type DiagnosticResult struct {
Diagnostic *Diagnostic
Output string
Err error
}
type DiagnosticReport map[string][]*DiagnosticResult
func (r DiagnosticReport) Add(group string, result *DiagnosticResult) {
if v, ok := r[group]; !ok {
r[group] = []*DiagnosticResult{result}
} else {
r[group] = append(v, result)
}
}
func runDoctorDiagnostics(cmd *cli.Context) error {
diagnostics, err := readDiagnosticDefinitions(doctorYaml)
if err != nil {
return errors.Newf("failed to load diagnostics from embedded yaml:", err)
}
// We do not want our progress messages to land on std out so we set output to os.Stderr
diagOut := std.NewOutput(os.Stderr, false)
runner := &diagnosticRunner{
diagnostics,
diagOut,
}
diagOut.WriteLine(output.Emoji("🥼", "Gathering diagnostics"))
report := runner.Run(cmd.Context)
diagOut.WriteLine(output.Emoji("💉", "Gathering of diagnostics complete!"))
markdown := buildMarkdownReport(report)
// check if we're rendering to the terminal or to another program
o, _ := os.Stdout.Stat()
if o.Mode()&os.ModeCharDevice != os.ModeCharDevice {
// our output has been redirected to another program, so lets just render it raw
fmt.Println(markdown)
return nil
}
// rendering to a terminal! so lets make it nice
return diagOut.WriteMarkdown(markdown)
}
func (d *diagnosticRunner) Run(ctx context.Context) DiagnosticReport {
env := os.Environ()
report := make(DiagnosticReport)
for group, diagnostics := range d.diagnostics.Diagnostic {
d.reporter.WriteLine(output.Emojif("💊", "Running %s diagnostics", group))
for _, diagnostic := range diagnostics {
out, err := run.BashInRoot(ctx, diagnostic.Cmd, run.BashInRootArgs{
Env: env,
})
diag := diagnostic
report.Add(group, &DiagnosticResult{
&diag,
out,
err,
})
}
}
return report
}
func buildMarkdownReport(report DiagnosticReport) string {
var sb strings.Builder
fmt.Fprintf(&sb, "# Diagnostic Report\n\n")
// General information
fmt.Fprintf(&sb, "sg commit: `%s`\n\n", BuildCommit)
fmt.Fprintf(&sb, "sg release: `%s`\n\n", ReleaseName)
fmt.Fprintf(&sb, "generated on: `%s`\n\n", time.Now())
titleCaser := cases.Title(language.English)
// map key order isn't stable so we extract them and sort them
groupKeys := []string{}
for k := range report {
groupKeys = append(groupKeys, k)
}
slices.Sort(groupKeys)
// Write out the report
for _, group := range groupKeys {
fmt.Fprintf(&sb, "## %s diagnostics\n\n", titleCaser.String(group))
result := report[group]
for _, item := range result {
cmdLine := fmt.Sprintf("Command: `%s`", item.Diagnostic.Cmd)
outputSection := fmt.Sprintf("Output: \n```\n%s\n```\n", item.Output)
errSection := fmt.Sprintf("Error: \n```\n%v\n```\n", item.Err)
fmt.Fprintf(&sb, "### %s\n\n%s\n\n%s\n%s", titleCaser.String(item.Diagnostic.Name), cmdLine, outputSection, errSection)
}
}
return sb.String()
}
func readDiagnosticDefinitions(content []byte) (*Diagnostics, error) {
var diags Diagnostics
dec := yaml.NewDecoder(bytes.NewReader(content))
err := dec.Decode(&diags)
if err != nil {
return nil, err
}
return &diags, nil
}