// JSON log grouping into per-request traces. // // traces reads slog JSON lines from stdin or files, groups them by their // "request" attribute, and prints one block per request. It renders the // same output cmd/dev shows live, for logs collected anywhere — a // production log file, kubectl logs, a local logs/server.log. // // Usage: // // traces [-r id] [-errors] [file...] // tail -f logs/server.log | traces -f package main import ( "bufio" "flag" "fmt" "io" "os" "strings" "time" "atlas9.dev/c/core/tracelog" ) func main() { idFilter := flag.String("r", "", "only show traces whose request ID contains this string") errsOnly := flag.Bool("errors", false, "only show traces containing WARN or ERROR lines") anomOnly := flag.Bool("anomalies", false, "only show traces flagged by anomaly detection") follow := flag.Bool("f", false, "render traces live as requests complete, e.g. tail -f server.log | traces -f") flag.Parse() col := &tracelog.Collector{ Out: os.Stdout, Color: tracelog.IsTerminal(os.Stdout), Filter: func(id string, entries []tracelog.Entry) bool { if *idFilter != "" && !strings.Contains(id, *idFilter) { return false } if *anomOnly && len(tracelog.Anomalies(entries)) == 0 { return false } return !*errsOnly || hasProblems(entries) }, } if *follow { // Flush hung traces even when the input goes quiet. sweeper := time.NewTicker(time.Second) defer sweeper.Stop() go func() { for range sweeper.C { col.Sweep(time.Now()) } }() } else { // Batch mode: never flush mid-stream, render leftovers at the end. col.MaxAge = 100 * 365 * 24 * time.Hour } scanner := bufio.NewScanner(input()) scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024) for scanner.Scan() { id, entry, ok := tracelog.ParseLine(scanner.Bytes()) if !ok || id == "" { continue } col.Add(id, entry) } col.Flush() if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func input() io.Reader { if flag.NArg() == 0 { return os.Stdin } var readers []io.Reader for _, path := range flag.Args() { f, err := os.Open(path) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } readers = append(readers, f) } return io.MultiReader(readers...) } func hasProblems(entries []tracelog.Entry) bool { for _, e := range entries { if e.Level == "WARN" || e.Level == "ERROR" { return true } } return false }