blob: 3df4261b8676b44af197a6f942f566c05a3e20d5 [file] [log] [blame]
// 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]
}