| // Copyright 2015 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 greplogs searches Go builder logs. |
| // |
| // greplogs [flags] (-e regexp|-E regexp) paths... |
| // greplogs [flags] (-e regexp|-E regexp) -dashboard |
| // |
| // greplogs finds builder logs matching a given set of regular |
| // expressions in Go syntax (godoc.org/regexp/syntax) and extracts |
| // failures from them. |
| // |
| // greplogs can search an arbitrary set of files just like grep. |
| // Alternatively, the -dashboard flag causes it to search the logs |
| // saved locally by fetchlogs (golang.org/x/build/cmd/fetchlogs). |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "regexp" |
| "runtime/debug" |
| "sort" |
| "strings" |
| "time" |
| |
| "github.com/kballard/go-shellquote" |
| "golang.org/x/build/cmd/greplogs/internal/logparse" |
| ) |
| |
| // TODO: If searching dashboard logs, optionally print to builder URLs |
| // instead of local file names. |
| |
| // TODO: Optionally extract failures and show only those. |
| |
| // TODO: Optionally classify matched logs by failure (and show either |
| // file name or extracted failure). |
| |
| // TODO: Option to print failure summary versus full failure message. |
| |
| var ( |
| fileRegexps regexpList |
| failRegexps regexpList |
| omit regexpList |
| |
| 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") |
| |
| color *colorizer |
| since, before timeFlag |
| ) |
| |
| const ( |
| colorPath = colorFgMagenta |
| colorPathColon = colorFgCyan |
| 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 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() |
| |
| // Validate flags. |
| if *flagDashboard && flag.NArg() > 0 { |
| fmt.Fprintf(os.Stderr, "-dashboard and paths are incompatible\n") |
| os.Exit(2) |
| } |
| switch *flagColor { |
| case "never": |
| color = newColorizer(false) |
| case "always": |
| color = newColorizer(true) |
| case "auto": |
| color = newColorizer(canColor()) |
| default: |
| fmt.Fprintf(os.Stderr, "-color must be one of never, always, or auto") |
| 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 |
| if *flagDashboard { |
| revDir := filepath.Join(xdgCacheDir(), "fetchlogs", "rev") |
| fis, err := ioutil.ReadDir(revDir) |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "%s: %s\n", revDir, err) |
| os.Exit(1) |
| } |
| for _, fi := range fis { |
| if !fi.IsDir() { |
| continue |
| } |
| paths = append(paths, filepath.Join(revDir, fi.Name())) |
| } |
| sort.Sort(sort.Reverse(sort.StringSlice(paths))) |
| stripDir = revDir + "/" |
| } else { |
| paths = flag.Args() |
| } |
| |
| // Process files |
| for _, path := range paths { |
| filepath.Walk(path, func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| status = 2 |
| fmt.Fprintf(os.Stderr, "%s: %v\n", path, err) |
| return nil |
| } |
| if info.IsDir() || strings.HasPrefix(filepath.Base(path), ".") { |
| return nil |
| } |
| |
| nicePath := path |
| if stripDir != "" && strings.HasPrefix(path, stripDir) { |
| nicePath = path[len(stripDir):] |
| } |
| |
| found, err := process(path, nicePath) |
| if err != nil { |
| status = 2 |
| fmt.Fprintf(os.Stderr, "%s: %v\n", path, err) |
| } else if found { |
| numMatching++ |
| if status == 1 { |
| status = 0 |
| } |
| } |
| return nil |
| }) |
| } |
| } |
| |
| 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 |
| } |
| |
| if !since.Time.IsZero() || !before.Time.IsZero() { |
| revDir := filepath.Dir(nicePath) |
| revDirBase := filepath.Base(revDir) |
| match := pathDateRE.FindStringSubmatch(revDirBase) |
| 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 |
| } |
| if !since.Time.IsZero() && revTime.Before(since.Time) { |
| return false, nil |
| } |
| if !before.Time.IsZero() && !revTime.Before(before.Time) { |
| return false, nil |
| } |
| } |
| |
| // TODO: Get the URL from the rev.json metadata |
| var logURL string |
| if link, err := os.Readlink(path); err == nil { |
| hash := filepath.Base(link) |
| logURL = "https://build.golang.org/log/" + hash |
| } |
| |
| // TODO: Use streaming if possible. |
| data, err := ioutil.ReadFile(path) |
| if err != nil { |
| return false, err |
| } |
| |
| // Check regexp match. |
| if !fileRegexps.AllMatch(data) || !failRegexps.AllMatch(data) { |
| return false, nil |
| } |
| |
| printPath := nicePath |
| if *flagMD && logURL != "" { |
| prefix := "" |
| if *flagTriage { |
| prefix = "- [ ] " |
| } |
| printPath = fmt.Sprintf("%s[%s](%s)", prefix, nicePath, logURL) |
| } |
| |
| if *flagFilesOnly { |
| fmt.Printf("%s\n", color.color(printPath, colorPath)) |
| return true, nil |
| } |
| |
| timer := time.AfterFunc(30*time.Second, func() { |
| debug.SetTraceback("all") |
| panic("stuck in extracting " + path) |
| }) |
| |
| // Extract failures. |
| failures, err := logparse.Extract(string(data), "", "") |
| if err != nil { |
| return false, err |
| } |
| |
| timer.Stop() |
| |
| // Print failures. |
| for _, failure := range failures { |
| var msg []byte |
| if failure.FullMessage != "" { |
| msg = []byte(failure.FullMessage) |
| } else { |
| msg = []byte(failure.Message) |
| } |
| |
| if len(failRegexps) > 0 && !failRegexps.AllMatch(msg) { |
| continue |
| } |
| |
| fmt.Printf("%s%s\n", color.color(printPath, colorPath), color.color(":", colorPathColon)) |
| if *flagMD { |
| fmt.Printf("```\n") |
| } |
| if !color.enabled { |
| fmt.Printf("%s", msg) |
| } else { |
| // Find specific matches and highlight them. |
| matches := mergeMatches(append(fileRegexps.Matches(msg), |
| failRegexps.Matches(msg)...)) |
| printed := 0 |
| for _, m := range matches { |
| fmt.Printf("%s%s", msg[printed:m[0]], color.color(string(msg[m[0]:m[1]]), colorMatch)) |
| printed = m[1] |
| } |
| fmt.Printf("%s", msg[printed:]) |
| } |
| if *flagMD { |
| fmt.Printf("\n```") |
| } |
| fmt.Printf("\n\n") |
| } |
| return true, nil |
| } |
| |
| func mergeMatches(matches [][]int) [][]int { |
| sort.Slice(matches, func(i, j int) bool { return matches[i][0] < matches[j][0] }) |
| for i := 0; i < len(matches); { |
| m := matches[i] |
| |
| // Combine with later matches. |
| j := i + 1 |
| for ; j < len(matches); j++ { |
| m2 := matches[j] |
| if m[1] <= m2[0] { |
| // Overlapping or exactly adjacent. |
| if m2[1] > m[1] { |
| m[1] = m2[1] |
| } |
| m2[0], m2[1] = 0, 0 |
| } else { |
| break |
| } |
| } |
| i = j |
| } |
| |
| // Clear out combined matches. |
| j := 0 |
| for _, m := range matches { |
| if m[0] == 0 && m[1] == 0 { |
| continue |
| } |
| matches[j] = m |
| j++ |
| } |
| return matches[:j] |
| } |