package main import ( "context" "fmt" "os" "path/filepath" "testing" "time" ) func writeLine(t *testing.T, f *os.File, website string) { t.Helper() _, err := fmt.Fprintf(f, "%s\t1.2.3.4\t0\tGET\t/path\t200\t0\t0.001\n", website) if err != nil { t.Fatalf("writeLine: %v", err) } } func TestMultiTailerReadsLines(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "access.log") f, err := os.Create(path) if err != nil { t.Fatal(err) } defer f.Close() ch := make(chan LogRecord, 100) mt := NewMultiTailer([]string{path}, 24, 48, ch) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go mt.Run(ctx) // Give the tailer time to open and seek to EOF. time.Sleep(50 * time.Millisecond) writeLine(t, f, "www.example.com") writeLine(t, f, "api.example.com") received := collectN(t, ch, 2, 2*time.Second) websites := map[string]bool{} for _, r := range received { websites[r.Website] = true } if !websites["www.example.com"] || !websites["api.example.com"] { t.Errorf("unexpected records: %v", received) } } func TestMultiTailerMultipleFiles(t *testing.T) { dir := t.TempDir() const numFiles = 5 files := make([]*os.File, numFiles) paths := make([]string, numFiles) for i := range files { p := filepath.Join(dir, fmt.Sprintf("access%d.log", i)) paths[i] = p f, err := os.Create(p) if err != nil { t.Fatal(err) } defer f.Close() files[i] = f } ch := make(chan LogRecord, 200) mt := NewMultiTailer(paths, 24, 48, ch) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go mt.Run(ctx) time.Sleep(50 * time.Millisecond) // Write one line per file for i, f := range files { writeLine(t, f, fmt.Sprintf("site%d.com", i)) } received := collectN(t, ch, numFiles, 2*time.Second) if len(received) != numFiles { t.Errorf("got %d records, want %d", len(received), numFiles) } } func TestMultiTailerLogRotation(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "access.log") f, err := os.Create(path) if err != nil { t.Fatal(err) } ch := make(chan LogRecord, 100) mt := NewMultiTailer([]string{path}, 24, 48, ch) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go mt.Run(ctx) time.Sleep(50 * time.Millisecond) // Write a line to the original file writeLine(t, f, "before.rotation.com") collectN(t, ch, 1, 2*time.Second) // Simulate logrotate: rename the old file, create a new one rotated := filepath.Join(dir, "access.log.1") f.Close() if err := os.Rename(path, rotated); err != nil { t.Fatal(err) } // Give the tailer a moment to detect the rename and start retrying time.Sleep(50 * time.Millisecond) // Create the new log file (as nginx would after logrotate) newF, err := os.Create(path) if err != nil { t.Fatal(err) } defer newF.Close() // Allow retry goroutine to pick it up time.Sleep(300 * time.Millisecond) writeLine(t, newF, "after.rotation.com") received := collectN(t, ch, 1, 3*time.Second) if len(received) == 0 || received[0].Website != "after.rotation.com" { t.Errorf("expected after.rotation.com, got %v", received) } } func TestExpandGlobs(t *testing.T) { dir := t.TempDir() for _, name := range []string{"a.log", "b.log", "other.txt"} { f, _ := os.Create(filepath.Join(dir, name)) f.Close() } pattern := filepath.Join(dir, "*.log") paths := expandGlobs([]string{pattern}) if len(paths) != 2 { t.Errorf("glob expanded to %d paths, want 2: %v", len(paths), paths) } } func TestExpandGlobsDeduplication(t *testing.T) { dir := t.TempDir() p := filepath.Join(dir, "access.log") f, _ := os.Create(p) f.Close() // Same file listed twice via explicit path and glob paths := expandGlobs([]string{p, filepath.Join(dir, "*.log")}) if len(paths) != 1 { t.Errorf("expected 1 deduplicated path, got %d: %v", len(paths), paths) } } // collectN reads exactly n records from ch within timeout, or returns what it got. func collectN(t *testing.T, ch <-chan LogRecord, n int, timeout time.Duration) []LogRecord { t.Helper() var records []LogRecord deadline := time.After(timeout) for len(records) < n { select { case r := <-ch: records = append(records, r) case <-deadline: t.Logf("collectN: timeout waiting for record %d/%d", len(records)+1, n) return records } } return records }