Files
nginx-logtail/cmd/collector/tailer_test.go
T
pim 6647f95be4 RELEASE 1.0.1: v2 log format, source_tag-labeled metrics, lint cleanup
Wire-format and metric overhaul. Both file and UDP ingest now share one
versioned ParseLine that dispatches on the v<N>\t prefix; v1 stays
unchanged, v2 adds $bytes_sent (replacing $body_bytes_sent),
$request_length, $upstream_response_time, and $upstream_status. File
ingest gains the same versioning, and the legacy positional file format
is removed (no live deployments).

Prometheus exposition is rewritten:

  - nginx_http_bytes_sent and nginx_http_request_duration_seconds gain
    a source_tag label.
  - nginx_http_requests_by_source_total gains status_class.
  - New v2-only metrics: nginx_http_request_bytes,
    nginx_http_upstream_duration_seconds,
    nginx_http_upstream_requests_total{status_class}.
  - Dropped nginx_http_response_body_bytes_by_source (subsumed by the
    dual-labeled bytes_sent metric).

Adds 'make fixstyle' (gofmt -w) and clears all golangci-lint findings
across the repo (errcheck, S1001, ST1005, unused).

Docs in design.md FR-2/FR-8 and user-guide.md are rewritten to present
v2 as the recommended log format.
2026-05-01 15:40:53 +02:00

180 lines
4.4 KiB
Go

package main
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
func writeLine(t *testing.T, f *os.File, website string) {
t.Helper()
// v1 layout: 12 payload fields after the v1 prefix.
_, err := fmt.Fprintf(f, "v1\t%s\t1.2.3.4\tGET\t/path\t200\t0\t0.001\t0\t0\tdirect\t10.0.0.1\thttps\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 func() { _ = f.Close() }()
ch := make(chan LogRecord, 100)
mt := NewMultiTailer([]string{path}, time.Hour, 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 func() { _ = f.Close() }()
files[i] = f
}
ch := make(chan LogRecord, 200)
mt := NewMultiTailer(paths, time.Hour, 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}, time.Hour, 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 func() { _ = 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
}