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 }