package parser import ( "bufio" "compress/gzip" "fmt" "io" "os" "path/filepath" "sort" "strings" ) // ScanDirectory scans dir for nginx access and error log files. // It matches filenames like access*.log* and error*.log*. // If includeGzip is false, .gz files are skipped. // Returns (accessFiles, errorFiles, error) sorted newest-first. func ScanDirectory(dir string, includeGzip bool) ([]string, []string, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, nil, fmt.Errorf("scandir %q: %w", dir, err) } var accessFiles, errorFiles []string for _, e := range entries { if e.IsDir() { continue } name := e.Name() if !includeGzip && strings.HasSuffix(name, ".gz") { continue } if matchesLogPattern(name, "access") { accessFiles = append(accessFiles, filepath.Join(dir, name)) } else if matchesLogPattern(name, "error") { errorFiles = append(errorFiles, filepath.Join(dir, name)) } } sortLogFiles(accessFiles) sortLogFiles(errorFiles) return accessFiles, errorFiles, nil } // matchesLogPattern returns true if filename starts with prefix and contains ".log". func matchesLogPattern(name, prefix string) bool { if !strings.HasPrefix(name, prefix) { return false } return strings.Contains(name, ".log") } // logSortKey returns a sort key such that: // - "access.log" → (0, 0) — most recent // - "access.log.1" → (1, 0) // - "access.log.1.gz" → (1, 1) // - "access.log.2.gz" → (2, 1) func logSortKey(path string) (int, int) { name := filepath.Base(path) // strip prefix up to and including ".log" idx := strings.Index(name, ".log") if idx < 0 { return 999, 0 } suffix := name[idx+4:] // e.g. "", ".1", ".1.gz", ".2.gz" isGz := 0 if strings.HasSuffix(suffix, ".gz") { isGz = 1 suffix = strings.TrimSuffix(suffix, ".gz") } suffix = strings.TrimPrefix(suffix, ".") num := 0 if suffix != "" { fmt.Sscanf(suffix, "%d", &num) } return num, isGz } func sortLogFiles(files []string) { sort.SliceStable(files, func(i, j int) bool { ni, gi := logSortKey(files[i]) nj, gj := logSortKey(files[j]) if ni != nj { return ni < nj } return gi < gj }) } // openLogFile opens a log file for reading. // If the path ends in ".gz", it transparently decompresses. func openLogFile(path string) (io.ReadCloser, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open %q: %w", path, err) } if strings.HasSuffix(path, ".gz") { gz, err := gzip.NewReader(f) if err != nil { f.Close() return nil, fmt.Errorf("gzip %q: %w", path, err) } return &gzipReadCloser{gz: gz, f: f}, nil } return f, nil } // gzipReadCloser closes both the gzip reader and underlying file. type gzipReadCloser struct { gz *gzip.Reader f *os.File } func (g *gzipReadCloser) Read(p []byte) (int, error) { return g.gz.Read(p) } func (g *gzipReadCloser) Close() error { err := g.gz.Close() g.f.Close() return err } // ReadAccessEntries reads all access log entries from the given files. // Malformed lines are silently skipped. func ReadAccessEntries(files []string) ([]AccessEntry, error) { var entries []AccessEntry for _, path := range files { rc, err := openLogFile(path) if err != nil { return nil, err } scanner := bufio.NewScanner(rc) for scanner.Scan() { if e, err := ParseAccessLine(scanner.Text()); err == nil { entries = append(entries, e) } } rc.Close() if err := scanner.Err(); err != nil { return nil, fmt.Errorf("scan %q: %w", path, err) } } return entries, nil } // ReadErrorEntries reads all error log entries from the given files. // Malformed lines are silently skipped. func ReadErrorEntries(files []string) ([]ErrorEntry, error) { var entries []ErrorEntry for _, path := range files { rc, err := openLogFile(path) if err != nil { return nil, err } scanner := bufio.NewScanner(rc) for scanner.Scan() { if e, err := ParseErrorLine(scanner.Text()); err == nil { entries = append(entries, e) } } rc.Close() if err := scanner.Err(); err != nil { return nil, fmt.Errorf("scan %q: %w", path, err) } } return entries, nil }