167 lines
4.1 KiB
Go
167 lines
4.1 KiB
Go
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
|
|
}
|