Skip to content
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

greplogs: refine output for issue triage #11

Closed
wants to merge 4 commits into from
Closed
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.17
require (
github.com/aclements/go-gg v0.0.0-20170323211221-abd1f791f5ee
github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
golang.org/x/build v0.0.0-20210804225706-d1bc548deb19
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand Down
26 changes: 26 additions & 0 deletions greplogs/_embed/broken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Command broken lists the current Go builders with known issues.
//
// To test this program, cd to its directory and run:
// go mod init
// go get golang.org/x/build/dashboard@HEAD
// go run .
// rm go.mod go.sum
package main

import (
"fmt"

"golang.org/x/build/dashboard"
)

func main() {
for _, b := range dashboard.Builders {
if len(b.KnownIssues) > 0 {
fmt.Println(b.Name)
}
}
}
110 changes: 110 additions & 0 deletions greplogs/broken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
_ "embed"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)

//go:embed _embed/broken.go
var brokenScript []byte

// listBrokenBuilders returns the builders that are marked
// as broken in golang.org/x/build/dashboard at HEAD.
func listBrokenBuilders() (broken []string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("identifying broken builders: %v", err)
}
}()

// Though this be madness, yet there is method in 't.
//
// Our goals here are:
//
// 1. Always use the most up-to-date information about broken builders, even
// if the user hasn't recently updated the greplogs binary.
//
// 2. Avoid the need to massively refactor the builder configuration right
// now. (Currently, the Go builders are configured programmatically in the
// x/build/dashboard package, not in external configuration files.)
//
// 3. Avoid the need to redeploy a production x/build/cmd/coordinator or
// x/build/devapp to pick up changes. (A user triaging test failures might
// not have access to deploy the coordinator, or might not want to disrupt
// running tests or active gomotes by pushing it.)
//
// Goals (2) and (3) imply that we must use x/build/dashboard, not fetch the
// list from build.golang.org or dev.golang.org. Since that is a Go package,
// we must run it as a Go program in order to evaluate it.
//
// Goal (1) implies that we must use x/build at HEAD, not (say) at whatever
// version of x/build this command was built with. We could perhaps relax that
// constraint if we move greplogs itself into x/build and consistently triage
// using 'go run golang.org/x/build/cmd/greplogs@HEAD' instead of an installed
// 'greplogs'.

if os.Getenv("GO111MODULE") == "off" {
return nil, errors.New("operation requires GO111MODULE=on or auto")
}

modDir, err := os.MkdirTemp("", "greplogs")
if err != nil {
return nil, err
}
defer func() {
removeErr := os.RemoveAll(modDir)
if err == nil {
err = removeErr
}
}()

runCommand := func(name string, args ...string) ([]byte, error) {
cmd := exec.Command(name, args...)
cmd.Dir = modDir
cmd.Env = append(os.Environ(),
"PWD="+modDir, // match cmd.Dir
"GOPRIVATE=golang.org/x/build", // avoid proxy cache; see https://go.dev/issue/38065
)
cmd.Stderr = new(strings.Builder)

out, err := cmd.Output()
if err != nil {
return out, fmt.Errorf("%s: %w\nstderr:\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr)
}
return out, nil
}

_, err = runCommand("go", "mod", "init", "github.com/aclements/go-misc/greplogs/_embed")
Copy link
Owner

Choose a reason for hiding this comment

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

This was certainly... unexpected. I assume the idea here is that we actually need to get whatever's at HEAD to get the current broken builders and there's not, like, an RPC API we can hit to get this?

If I'm understanding right, this definitely could use a comment explaining the rationale here. (Also, could we instead add a dashboard API to get this?)

The other thing I wonder is how well-defined this is at all. This will get the list of broken builders right now, but greplogs is all about digging back in history. Doesn't this mean it could miss real failures that happened before a builder was marked broken, and surface expected failures after a builder stops being marked broken?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I assume the idea here is that we actually need to get whatever's at HEAD to get the current broken builders and there's not, like, an RPC API we can hit to get this?

Correct. And even if there was an RPC API, because the configuration is encoded as a Go package we would have to push a new version of some server every time we wanted to update it.

If I'm understanding right, this definitely could use a comment explaining the rationale here.

Done!

Doesn't this mean it could miss real failures that happened before a builder was marked broken, and surface expected failures after a builder stops being marked broken?

Yep! That's why we only use this if the --triage flag is set — we don't have a log of which builders were broken at which commits (and that would be fairly tedious to maintain), so instead we bias toward over-suppressing failures for builders that are currently broken.

(If triage falls too far behind and a builder is already fixed, a user can explicitly notch out additional previously-broken builders using the --omit flag.)

Copy link
Owner

Choose a reason for hiding this comment

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

Correct. And even if there was an RPC API, because the configuration is encoded as a Go package we would have to push a new version of some server every time we wanted to update it.

Don't we have to do redeploy whenever the broken builder list changes anyway? As far as most people are concerned, the broken builders are whatever's grayed out on the dashboard. If the dashboard was also serving this list via an API, it would match up with whatever's grayed out.

(There's a bigger question here of whether it makes sense for this to be configured in a giant Go struct that can't track history and requires a redeploy on any changes, but that's for another time...)

if err != nil {
return nil, err
}

_, err = runCommand("go", "get", "golang.org/x/build/dashboard@HEAD")
if err != nil {
return nil, err
}

err = os.WriteFile(filepath.Join(modDir, "broken.go"), brokenScript, 0644)
if err != nil {
return nil, err
}

out, err := runCommand("go", "run", "broken.go")
if err != nil {
return nil, err
}

broken = strings.Split(strings.TrimSpace(string(out)), "\n")
sort.Strings(broken)
return broken, nil
}
79 changes: 69 additions & 10 deletions greplogs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"time"

"github.com/aclements/go-misc/internal/loganal"
"github.com/kballard/go-shellquote"
)

// TODO: If searching dashboard logs, optionally print to builder URLs
Expand All @@ -46,8 +47,10 @@ var (
failRegexps regexpList
omit regexpList

flagDashboard = flag.Bool("dashboard", false, "search dashboard logs from fetchlogs")
flagMD = flag.Bool("md", false, "output in Markdown")
flagDashboard = flag.Bool("dashboard", true, "search dashboard logs from fetchlogs")
flagMD = flag.Bool("md", true, "output in Markdown")
flagTriage = flag.Bool("triage", false, "adjust Markdown output for failure triage")
flagDetails = flag.Bool("details", false, "surround Markdown results in a <details> tag")
flagFilesOnly = flag.Bool("l", false, "print only names of matching files")
flagColor = flag.String("color", "auto", "highlight output in color: `mode` is never, always, or auto")

Expand All @@ -61,12 +64,14 @@ const (
colorMatch = colorBold | colorFgRed
)

var brokenBuilders []string

func main() {
// XXX What I want right now is just to point it at a bunch of
// logs and have it extract the failures.
flag.Var(&fileRegexps, "e", "show files matching `regexp`; if provided multiple times, files must match all regexps")
flag.Var(&failRegexps, "E", "show only errors matching `regexp`; if provided multiple times, an error must match all regexps")
flag.Var(&omit, "omit", "omit results for builder names matching `regexp`; if provided multiple times, builders matching any regexp are omitted")
flag.Var(&omit, "omit", "omit results for builder names and/or revisions matching `regexp`; if provided multiple times, logs matching any regexp are omitted")
flag.Var(&since, "since", "list only failures on revisions since this date, as an RFC-3339 date or date-time")
flag.Var(&before, "before", "list only failures on revisions before this date, in the same format as -since")
flag.Parse()
Expand All @@ -88,6 +93,47 @@ func main() {
os.Exit(2)
}

if *flagTriage {
*flagFilesOnly = true
if len(failRegexps) == 0 && len(fileRegexps) == 0 {
failRegexps.Set(".")
}

if before.Time.IsZero() {
year, month, day := time.Now().UTC().Date()
before = timeFlag{Time: time.Date(year, month, day, 0, 0, 0, 0, time.UTC)}
}

var err error
brokenBuilders, err = listBrokenBuilders()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if len(brokenBuilders) > 0 {
fmt.Fprintf(os.Stderr, "omitting builders with known issues:\n\t%s\n\n", strings.Join(brokenBuilders, "\n\t"))
}
}

status := 1
defer func() { os.Exit(status) }()

numMatching := 0
if *flagMD {
args := append([]string{filepath.Base(os.Args[0])}, os.Args[1:]...)
fmt.Printf("`%s`\n", shellquote.Join(args...))

defer func() {
if numMatching == 0 || *flagTriage || *flagDetails {
fmt.Printf("\n(%d matching logs)\n", numMatching)
}
}()
if *flagDetails {
os.Stdout.WriteString("<details>\n\n")
defer os.Stdout.WriteString("\n</details>\n")
}
}

// Gather paths.
var paths []string
var stripDir string
Expand All @@ -111,7 +157,6 @@ func main() {
}

// Process files
status := 1
for _, path := range paths {
filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
Expand All @@ -132,20 +177,27 @@ func main() {
if err != nil {
status = 2
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
} else if found && status == 1 {
status = 0
} else if found {
numMatching++
if status == 1 {
status = 0
}
}
return nil
})
}
os.Exit(status)
}

var pathDateRE = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})-[0-9a-f]+(?:-[0-9a-f]+)?$`)
var pathDateRE = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})-([0-9a-f]+(?:-[0-9a-f]+)?)$`)

func process(path, nicePath string) (found bool, err error) {
// If this is from the dashboard, filter by builder and date and get the builder URL.
builder := filepath.Base(nicePath)
for _, b := range brokenBuilders {
if builder == b {
return false, nil
}
}
if omit.AnyMatchString(builder) {
return false, nil
}
Expand All @@ -154,10 +206,13 @@ func process(path, nicePath string) (found bool, err error) {
revDir := filepath.Dir(nicePath)
revDirBase := filepath.Base(revDir)
match := pathDateRE.FindStringSubmatch(revDirBase)
if len(match) != 2 {
if len(match) != 3 {
// Without a valid log date we can't filter by it.
return false, fmt.Errorf("timestamp not found in rev dir name: %q", revDirBase)
}
if omit.AnyMatchString(match[2]) {
return false, nil
}
revTime, err := time.Parse("2006-01-02T15:04:05", match[1])
if err != nil {
return false, err
Expand Down Expand Up @@ -190,7 +245,11 @@ func process(path, nicePath string) (found bool, err error) {

printPath := nicePath
if *flagMD && logURL != "" {
printPath = fmt.Sprintf("[%s](%s)", nicePath, logURL)
prefix := ""
if *flagTriage {
prefix = "- [ ] "
}
printPath = fmt.Sprintf("%s[%s](%s)", prefix, nicePath, logURL)
}

if *flagFilesOnly {
Expand Down