initial commit
This commit is contained in:
commit
885e7d6e02
22 changed files with 3001 additions and 0 deletions
167
internal/parser/scanner.go
Normal file
167
internal/parser/scanner.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue