golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/greplogs/main.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Command greplogs searches Go builder logs.
     6  //
     7  //	greplogs [flags] (-e regexp|-E regexp) paths...
     8  //	greplogs [flags] (-e regexp|-E regexp) -dashboard
     9  //
    10  // greplogs finds builder logs matching a given set of regular
    11  // expressions in Go syntax (godoc.org/regexp/syntax) and extracts
    12  // failures from them.
    13  //
    14  // greplogs can search an arbitrary set of files just like grep.
    15  // Alternatively, the -dashboard flag causes it to search the logs
    16  // saved locally by fetchlogs (golang.org/x/build/cmd/fetchlogs).
    17  package main
    18  
    19  import (
    20  	"flag"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"regexp"
    25  	"runtime/debug"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/kballard/go-shellquote"
    31  	"golang.org/x/build/cmd/greplogs/internal/logparse"
    32  )
    33  
    34  // TODO: If searching dashboard logs, optionally print to builder URLs
    35  // instead of local file names.
    36  
    37  // TODO: Optionally extract failures and show only those.
    38  
    39  // TODO: Optionally classify matched logs by failure (and show either
    40  // file name or extracted failure).
    41  
    42  // TODO: Option to print failure summary versus full failure message.
    43  
    44  var (
    45  	fileRegexps regexpList
    46  	failRegexps regexpList
    47  	omit        regexpList
    48  	knownIssues regexpMap
    49  
    50  	flagDashboard = flag.Bool("dashboard", true, "search dashboard logs from fetchlogs")
    51  	flagMD        = flag.Bool("md", true, "output in Markdown")
    52  	flagTriage    = flag.Bool("triage", false, "adjust Markdown output for failure triage")
    53  	flagDetails   = flag.Bool("details", false, "surround Markdown results in a <details> tag")
    54  	flagFilesOnly = flag.Bool("l", false, "print only names of matching files")
    55  	flagColor     = flag.String("color", "auto", "highlight output in color: `mode` is never, always, or auto")
    56  
    57  	color         *colorizer
    58  	since, before timeFlag
    59  )
    60  
    61  const (
    62  	colorPath      = colorFgMagenta
    63  	colorPathColon = colorFgCyan
    64  	colorMatch     = colorBold | colorFgRed
    65  )
    66  
    67  var brokenBuilders []string
    68  
    69  func main() {
    70  	// XXX What I want right now is just to point it at a bunch of
    71  	// logs and have it extract the failures.
    72  	flag.Var(&knownIssues, "known-issue", "add an issue=regexp mapping; if a log matches regexp it will be categorized under issue. One mapping per flag.")
    73  	flag.Var(&fileRegexps, "e", "show files matching `regexp`; if provided multiple times, files must match all regexps")
    74  	flag.Var(&failRegexps, "E", "show only errors matching `regexp`; if provided multiple times, an error must match all regexps")
    75  	flag.Var(&omit, "omit", "omit results for builder names and/or revisions matching `regexp`; if provided multiple times, logs matching any regexp are omitted")
    76  	flag.Var(&since, "since", "list only failures on revisions since this date, as an RFC-3339 date or date-time")
    77  	flag.Var(&before, "before", "list only failures on revisions before this date, in the same format as -since")
    78  	flag.Parse()
    79  
    80  	// Validate flags.
    81  	if *flagDashboard && flag.NArg() > 0 {
    82  		fmt.Fprintf(os.Stderr, "-dashboard and paths are incompatible\n")
    83  		os.Exit(2)
    84  	}
    85  	switch *flagColor {
    86  	case "never":
    87  		color = newColorizer(false)
    88  	case "always":
    89  		color = newColorizer(true)
    90  	case "auto":
    91  		color = newColorizer(canColor())
    92  	default:
    93  		fmt.Fprintf(os.Stderr, "-color must be one of never, always, or auto")
    94  		os.Exit(2)
    95  	}
    96  
    97  	if *flagTriage {
    98  		*flagFilesOnly = true
    99  		if len(failRegexps) == 0 && len(fileRegexps) == 0 {
   100  			failRegexps.Set(".")
   101  		}
   102  
   103  		if before.Time.IsZero() {
   104  			year, month, day := time.Now().UTC().Date()
   105  			before = timeFlag{Time: time.Date(year, month, day, 0, 0, 0, 0, time.UTC)}
   106  		}
   107  
   108  		var err error
   109  		brokenBuilders, err = listBrokenBuilders()
   110  		if err != nil {
   111  			fmt.Fprintln(os.Stderr, err)
   112  			os.Exit(1)
   113  		}
   114  		if len(brokenBuilders) > 0 {
   115  			fmt.Fprintf(os.Stderr, "omitting builders with known issues:\n\t%s\n\n", strings.Join(brokenBuilders, "\n\t"))
   116  		}
   117  	}
   118  
   119  	status := 1
   120  	defer func() { os.Exit(status) }()
   121  
   122  	numMatching := 0
   123  	if *flagMD {
   124  		args := append([]string{filepath.Base(os.Args[0])}, os.Args[1:]...)
   125  		fmt.Printf("`%s`\n", shellquote.Join(args...))
   126  
   127  		defer func() {
   128  			if numMatching == 0 || *flagTriage || *flagDetails {
   129  				fmt.Printf("\n(%d matching logs)\n", numMatching)
   130  			}
   131  		}()
   132  		if *flagDetails {
   133  			os.Stdout.WriteString("<details>\n\n")
   134  			defer os.Stdout.WriteString("\n</details>\n")
   135  		}
   136  	}
   137  
   138  	// Gather paths.
   139  	var paths []string
   140  	var stripDir string
   141  	if *flagDashboard {
   142  		revDir := filepath.Join(xdgCacheDir(), "fetchlogs", "rev")
   143  		fis, err := os.ReadDir(revDir)
   144  		if err != nil {
   145  			fmt.Fprintf(os.Stderr, "%s: %s\n", revDir, err)
   146  			os.Exit(1)
   147  		}
   148  		for _, fi := range fis {
   149  			if !fi.IsDir() {
   150  				continue
   151  			}
   152  			paths = append(paths, filepath.Join(revDir, fi.Name()))
   153  		}
   154  		sort.Sort(sort.Reverse(sort.StringSlice(paths)))
   155  		stripDir = revDir + "/"
   156  	} else {
   157  		paths = flag.Args()
   158  	}
   159  
   160  	// Process files
   161  	for _, path := range paths {
   162  		filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
   163  			if err != nil {
   164  				status = 2
   165  				fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
   166  				return nil
   167  			}
   168  			if info.IsDir() || strings.HasPrefix(filepath.Base(path), ".") {
   169  				return nil
   170  			}
   171  
   172  			nicePath := path
   173  			if stripDir != "" && strings.HasPrefix(path, stripDir) {
   174  				nicePath = path[len(stripDir):]
   175  			}
   176  
   177  			found, err := process(path, nicePath)
   178  			if err != nil {
   179  				status = 2
   180  				fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
   181  			} else if found {
   182  				numMatching++
   183  				if status == 1 {
   184  					status = 0
   185  				}
   186  			}
   187  			return nil
   188  		})
   189  	}
   190  }
   191  
   192  var pathDateRE = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})-([0-9a-f]+(?:-[0-9a-f]+)?)$`)
   193  
   194  func process(path, nicePath string) (found bool, err error) {
   195  	// If this is from the dashboard, filter by builder and date and get the builder URL.
   196  	builder := filepath.Base(nicePath)
   197  	for _, b := range brokenBuilders {
   198  		if builder == b {
   199  			return false, nil
   200  		}
   201  	}
   202  	if omit.AnyMatchString(builder) {
   203  		return false, nil
   204  	}
   205  
   206  	if !since.Time.IsZero() || !before.Time.IsZero() {
   207  		revDir := filepath.Dir(nicePath)
   208  		revDirBase := filepath.Base(revDir)
   209  		match := pathDateRE.FindStringSubmatch(revDirBase)
   210  		if len(match) != 3 {
   211  			// Without a valid log date we can't filter by it.
   212  			return false, fmt.Errorf("timestamp not found in rev dir name: %q", revDirBase)
   213  		}
   214  		if omit.AnyMatchString(match[2]) {
   215  			return false, nil
   216  		}
   217  		revTime, err := time.Parse("2006-01-02T15:04:05", match[1])
   218  		if err != nil {
   219  			return false, err
   220  		}
   221  		if !since.Time.IsZero() && revTime.Before(since.Time) {
   222  			return false, nil
   223  		}
   224  		if !before.Time.IsZero() && !revTime.Before(before.Time) {
   225  			return false, nil
   226  		}
   227  	}
   228  
   229  	// TODO: Get the URL from the rev.json metadata
   230  	var logURL string
   231  	if link, err := os.Readlink(path); err == nil {
   232  		hash := filepath.Base(link)
   233  		logURL = "https://build.golang.org/log/" + hash
   234  	}
   235  
   236  	// TODO: Use streaming if possible.
   237  	data, err := os.ReadFile(path)
   238  	if err != nil {
   239  		return false, err
   240  	}
   241  
   242  	// Check regexp match.
   243  	if !fileRegexps.AllMatch(data) || !failRegexps.AllMatch(data) {
   244  		return false, nil
   245  	}
   246  
   247  	printPath := nicePath
   248  	kiMatches := 0
   249  	if *flagMD && logURL != "" {
   250  		prefix := ""
   251  		if *flagTriage {
   252  			matches := knownIssues.Matches(data)
   253  			if len(matches) == 0 {
   254  				prefix = "- [ ] "
   255  			} else {
   256  				kiMatches++
   257  				prefix = fmt.Sprintf("- [x] (%v) ", strings.Join(matches, ", "))
   258  			}
   259  		}
   260  		printPath = fmt.Sprintf("%s[%s](%s)", prefix, nicePath, logURL)
   261  	}
   262  
   263  	if *flagFilesOnly {
   264  		fmt.Printf("%s\n", color.color(printPath, colorPath))
   265  		return true, nil
   266  	}
   267  
   268  	timer := time.AfterFunc(30*time.Second, func() {
   269  		debug.SetTraceback("all")
   270  		panic("stuck in extracting " + path)
   271  	})
   272  
   273  	// Extract failures.
   274  	failures, err := logparse.Extract(string(data), "", "")
   275  	if err != nil {
   276  		return false, err
   277  	}
   278  
   279  	timer.Stop()
   280  
   281  	// Print failures.
   282  	for _, failure := range failures {
   283  		var msg []byte
   284  		if failure.FullMessage != "" {
   285  			msg = []byte(failure.FullMessage)
   286  		} else {
   287  			msg = []byte(failure.Message)
   288  		}
   289  
   290  		if len(failRegexps) > 0 && !failRegexps.AllMatch(msg) {
   291  			continue
   292  		}
   293  
   294  		fmt.Printf("%s%s\n", color.color(printPath, colorPath), color.color(":", colorPathColon))
   295  		if *flagMD {
   296  			fmt.Printf("```\n")
   297  		}
   298  		if !color.enabled {
   299  			fmt.Printf("%s", msg)
   300  		} else {
   301  			// Find specific matches and highlight them.
   302  			matches := mergeMatches(append(fileRegexps.Matches(msg),
   303  				failRegexps.Matches(msg)...))
   304  			printed := 0
   305  			for _, m := range matches {
   306  				fmt.Printf("%s%s", msg[printed:m[0]], color.color(string(msg[m[0]:m[1]]), colorMatch))
   307  				printed = m[1]
   308  			}
   309  			fmt.Printf("%s", msg[printed:])
   310  		}
   311  		if *flagMD {
   312  			fmt.Printf("\n```")
   313  		}
   314  		fmt.Printf("\n\n")
   315  	}
   316  	return true, nil
   317  }
   318  
   319  func mergeMatches(matches [][]int) [][]int {
   320  	sort.Slice(matches, func(i, j int) bool { return matches[i][0] < matches[j][0] })
   321  	for i := 0; i < len(matches); {
   322  		m := matches[i]
   323  
   324  		// Combine with later matches.
   325  		j := i + 1
   326  		for ; j < len(matches); j++ {
   327  			m2 := matches[j]
   328  			if m[1] <= m2[0] {
   329  				// Overlapping or exactly adjacent.
   330  				if m2[1] > m[1] {
   331  					m[1] = m2[1]
   332  				}
   333  				m2[0], m2[1] = 0, 0
   334  			} else {
   335  				break
   336  			}
   337  		}
   338  		i = j
   339  	}
   340  
   341  	// Clear out combined matches.
   342  	j := 0
   343  	for _, m := range matches {
   344  		if m[0] == 0 && m[1] == 0 {
   345  			continue
   346  		}
   347  		matches[j] = m
   348  		j++
   349  	}
   350  	return matches[:j]
   351  }