github.com/honeycombio/honeytail@v1.9.0/tail/tail_test.go (about)

     1  package tail
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha1"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math/rand"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/sirupsen/logrus"
    17  )
    18  
    19  var tailOpts = TailOptions{
    20  	ReadFrom: "start",
    21  	Stop:     true,
    22  }
    23  
    24  func TestTailSingleFile(t *testing.T) {
    25  	ts := &testSetup{}
    26  	ts.start(t)
    27  	defer ts.stop()
    28  
    29  	filename := ts.tmpdir + "/first.log"
    30  	statefilename := filename + ".mystate"
    31  	jsonLines := []string{"{\"a\":1}", "{\"b\":2}", "{\"c\":3}"}
    32  	ts.writeFile(t, filename, strings.Join(jsonLines, "\n"))
    33  
    34  	conf := Config{
    35  		Options: tailOpts,
    36  	}
    37  	tailer, err := getTailer(conf, filename, statefilename)
    38  	if err != nil {
    39  		t.Fatal(err)
    40  	}
    41  	lines := tailSingleFile(ts.ctx, tailer, filename, statefilename)
    42  	checkLinesChan(t, lines, jsonLines)
    43  }
    44  
    45  func TestTailSTDIN(t *testing.T) {
    46  	ts := &testSetup{}
    47  	ts.start(t)
    48  	defer ts.stop()
    49  	conf := Config{
    50  		Options: tailOpts,
    51  		Paths:   make([]string, 1),
    52  	}
    53  	conf.Paths[0] = "-"
    54  	lineChans, err := GetEntries(ts.ctx, conf)
    55  	if err != nil {
    56  		t.Fatal(err)
    57  	}
    58  	if len(lineChans) != 1 {
    59  		t.Errorf("lines chans should have had one channel; instead was length %d", len(lineChans))
    60  	}
    61  }
    62  
    63  func TestGetSampledEntries(t *testing.T) {
    64  	ts := &testSetup{}
    65  	ts.start(t)
    66  	defer ts.stop()
    67  	rand.Seed(3)
    68  
    69  	conf := Config{
    70  		Paths:   make([]string, 3),
    71  		Options: tailOpts,
    72  	}
    73  
    74  	jsonLines := make([][]string, 3)
    75  	filenameRoot := ts.tmpdir + "/json.log"
    76  	for i := 0; i < 3; i++ {
    77  		jsonLines[i] = make([]string, 6)
    78  		for j := 0; j < 6; j++ {
    79  			jsonLines[i][j] = fmt.Sprintf("{\"a\":%d", i)
    80  		}
    81  
    82  		filename := filenameRoot + fmt.Sprint(i)
    83  		conf.Paths[i] = filename
    84  		ts.writeFile(t, filename, strings.Join(jsonLines[i], "\n"))
    85  	}
    86  
    87  	chanArr, err := GetSampledEntries(ts.ctx, conf, 2)
    88  	if err != nil {
    89  		t.Fatal(err)
    90  	}
    91  	// can't check each line because the parallel goroutines screw with the random
    92  	// dropping lines, so you can't know which channel will drop which messages.
    93  	// But the overall count of messages is predictable.
    94  	var lineCounter int
    95  
    96  	for _, ch := range chanArr {
    97  		for _ = range ch {
    98  			lineCounter++
    99  		}
   100  	}
   101  	expectedLines := 10
   102  	if lineCounter != expectedLines {
   103  		t.Errorf("expected to get %d lines, got %d instead", expectedLines, lineCounter)
   104  	}
   105  }
   106  
   107  func TestGetEntries(t *testing.T) {
   108  	ts := &testSetup{}
   109  	ts.start(t)
   110  	defer ts.stop()
   111  
   112  	conf := Config{
   113  		Paths:   make([]string, 3),
   114  		Options: tailOpts,
   115  	}
   116  
   117  	jsonLines := make([][]string, 3)
   118  	filenameRoot := ts.tmpdir + "/json.log"
   119  	for i := 0; i < 3; i++ {
   120  		jsonLines[i] = make([]string, 3)
   121  		for j := 0; j < 3; j++ {
   122  			jsonLines[i][j] = fmt.Sprintf("{\"a\":%d}", i)
   123  		}
   124  
   125  		filename := filenameRoot + fmt.Sprint(i)
   126  		conf.Paths[i] = filename
   127  		ts.writeFile(t, filename, strings.Join(jsonLines[i], "\n"))
   128  	}
   129  
   130  	chanArr, err := GetEntries(ts.ctx, conf)
   131  	if err != nil {
   132  		t.Fatal(err)
   133  	}
   134  	for i, ch := range chanArr {
   135  		checkLinesChan(t, ch, jsonLines[i])
   136  	}
   137  
   138  	// test that if all statefile-like filenames and missing files are removed
   139  	// from the list, it errors
   140  	fn1 := ts.tmpdir + "/sparklestate"
   141  	ts.writeFile(t, fn1, "body")
   142  	fn2 := ts.tmpdir + "/foo.leash.state"
   143  	ts.writeFile(t, fn2, "body")
   144  	conf = Config{
   145  		Paths: []string{fn1, fn2, "/file/does/not/exist"},
   146  		Options: TailOptions{
   147  			StateFile: fn1,
   148  		},
   149  	}
   150  	nilChan, err := GetEntries(ts.ctx, conf)
   151  	if nilChan != nil {
   152  		t.Error("errored getEntries was supposed to respond with a nil channel list")
   153  	}
   154  	if err == nil {
   155  		t.Error("expected error from GetEntries; got nil instead.")
   156  	}
   157  }
   158  
   159  func TestAbortChannel(t *testing.T) {
   160  	ts := &testSetup{}
   161  	ts.start(t)
   162  	defer ts.stop()
   163  
   164  	var tailWait = TailOptions{
   165  		ReadFrom: "start",
   166  		Stop:     false,
   167  	}
   168  
   169  	conf := Config{
   170  		Paths:   make([]string, 3),
   171  		Options: tailWait,
   172  	}
   173  
   174  	jsonLines := make([][]string, 3)
   175  	filenameRoot := ts.tmpdir + "/json.log"
   176  	for i := 0; i < 3; i++ {
   177  		jsonLines[i] = make([]string, 3)
   178  		for j := 0; j < 3; j++ {
   179  			jsonLines[i][j] = fmt.Sprintf("{\"a\":%d}", i)
   180  		}
   181  
   182  		filename := filenameRoot + fmt.Sprint(i)
   183  		conf.Paths[i] = filename
   184  		ts.writeFile(t, filename, strings.Join(jsonLines[i], "\n"))
   185  	}
   186  
   187  	chanArr, err := GetEntries(ts.ctx, conf)
   188  	if err != nil {
   189  		t.Fatal(err)
   190  	}
   191  
   192  	// ok, let's see what happens when we want to quit
   193  	ts.cancel()
   194  	for _, ch := range chanArr {
   195  		checkLinesChanClosed(t, ch)
   196  	}
   197  }
   198  
   199  func TestRemoveStateFiles(t *testing.T) {
   200  	files := []string{
   201  		"foo.bar",
   202  		"/bar.baz",
   203  		"bar.leash.state",
   204  		"myspecialstatefile",
   205  		"baz.foo",
   206  	}
   207  	expectedFilesNoStatefile := []string{
   208  		"foo.bar",
   209  		"/bar.baz",
   210  		"myspecialstatefile",
   211  		"baz.foo",
   212  	}
   213  	expectedFilesConfStatefile := []string{
   214  		"foo.bar",
   215  		"/bar.baz",
   216  		"baz.foo",
   217  	}
   218  	conf := Config{
   219  		Options: TailOptions{},
   220  	}
   221  	newFiles := removeStateFiles(files, conf)
   222  	if !reflect.DeepEqual(newFiles, expectedFilesNoStatefile) {
   223  		t.Errorf("expected %v, instead got %v", expectedFilesNoStatefile, newFiles)
   224  	}
   225  	conf = Config{
   226  		Options: TailOptions{
   227  			StateFile: "myspecialstatefile",
   228  		},
   229  	}
   230  	newFiles = removeStateFiles(files, conf)
   231  	if !reflect.DeepEqual(newFiles, expectedFilesConfStatefile) {
   232  		t.Errorf("expected %v, instead got %v", expectedFilesConfStatefile, newFiles)
   233  	}
   234  }
   235  
   236  func TestRemoveFilteredPaths(t *testing.T) {
   237  	files := []string{
   238  		"/var/log/exactmatch.log",
   239  		"foo.1",
   240  		"foo.2",
   241  		"foobar.1",
   242  		"foobar.2",
   243  		"barfoo.1",
   244  		"xyz",
   245  		"/var/log/123_something.log",
   246  		"/var/log/321_something.log",
   247  		"/var/log/123_somethingelse.log",
   248  	}
   249  	filters := []string{
   250  		"/var/log/exactmatch.log",
   251  		"/var/log/nothing.log",
   252  		"foo*",
   253  		"zbarfoo*",
   254  		"abc",
   255  		"/var/log/*something.log",
   256  	}
   257  	expected := []string{
   258  		"barfoo.1",
   259  		"xyz",
   260  		"/var/log/123_somethingelse.log",
   261  	}
   262  
   263  	filtered := removeFilteredPaths(files, filters)
   264  	if !reflect.DeepEqual(filtered, expected) {
   265  		t.Errorf("expected %v, instead got %v", expected, filtered)
   266  	}
   267  }
   268  func TestGetStateFile(t *testing.T) {
   269  	ts := &testSetup{}
   270  	ts.start(t)
   271  	defer ts.stop()
   272  
   273  	conf := Config{
   274  		Paths:   make([]string, 3),
   275  		Options: tailOpts,
   276  	}
   277  
   278  	filename := "foobar.log"
   279  	statefilename := "foobar.leash.state"
   280  
   281  	existingStateFile := filepath.Join(ts.tmpdir, "existing.state")
   282  	ts.writeFile(t, existingStateFile, "")
   283  	newStateFile := filepath.Join(ts.tmpdir, "new.state")
   284  
   285  	tsts := []struct {
   286  		stateFileConfig string
   287  		numFiles        int
   288  		expected        string
   289  	}{
   290  		{existingStateFile, 1, existingStateFile},
   291  		{existingStateFile, 2, filepath.Join(os.TempDir(), statefilename)},
   292  		{newStateFile, 1, newStateFile},
   293  		{newStateFile, 2, filepath.Join(os.TempDir(), statefilename)},
   294  		{ts.tmpdir, 1, filepath.Join(ts.tmpdir, statefilename)},
   295  		{ts.tmpdir, 2, filepath.Join(ts.tmpdir, statefilename)},
   296  		{"", 1, filepath.Join(os.TempDir(), statefilename)},
   297  		{"", 2, filepath.Join(os.TempDir(), statefilename)},
   298  	}
   299  
   300  	for _, tt := range tsts {
   301  		conf.Options.StateFile = tt.stateFileConfig
   302  		actual := getStateFile(conf, filename, tt.numFiles)
   303  		if actual != tt.expected {
   304  			t.Errorf("getStateFile with config statefile: %s\n\tgot: %s, expected: %s",
   305  				tt.stateFileConfig, actual, tt.expected)
   306  		}
   307  	}
   308  }
   309  func TestGetFileStateWithHashPathEnabled(t *testing.T) {
   310  	ts := &testSetup{}
   311  	ts.start(t)
   312  	defer ts.stop()
   313  
   314  	conf := Config{
   315  		Paths: make([]string, 3),
   316  		Options: TailOptions{
   317  			ReadFrom:              "start",
   318  			Stop:                  true,
   319  			HashStateFileDirPaths: true,
   320  		},
   321  	}
   322  
   323  	filename := "/var/logs/foobar.log"
   324  	statefilename := fmt.Sprintf("foobar.leash.state-%x", sha1.Sum([]byte(filename)))
   325  
   326  	existingStateFile := filepath.Join(ts.tmpdir, "existing.state")
   327  	ts.writeFile(t, existingStateFile, "")
   328  	newStateFile := filepath.Join(ts.tmpdir, "new.state")
   329  
   330  	tsts := []struct {
   331  		stateFileConfig string
   332  		numFiles        int
   333  		expected        string
   334  	}{
   335  		{existingStateFile, 1, existingStateFile},
   336  		{existingStateFile, 2, filepath.Join(os.TempDir(), statefilename)},
   337  		{newStateFile, 1, newStateFile},
   338  		{newStateFile, 2, filepath.Join(os.TempDir(), statefilename)},
   339  		{ts.tmpdir, 1, filepath.Join(ts.tmpdir, statefilename)},
   340  		{ts.tmpdir, 2, filepath.Join(ts.tmpdir, statefilename)},
   341  		{"", 1, filepath.Join(os.TempDir(), statefilename)},
   342  		{"", 2, filepath.Join(os.TempDir(), statefilename)},
   343  	}
   344  
   345  	for _, tt := range tsts {
   346  		conf.Options.StateFile = tt.stateFileConfig
   347  		actual := getStateFile(conf, filename, tt.numFiles)
   348  		if actual != tt.expected {
   349  			t.Errorf("getStateFile with config statefile: %s\n\tgot: %s, expected: %s",
   350  				tt.stateFileConfig, actual, tt.expected)
   351  		}
   352  	}
   353  }
   354  func TestStatefilesWithDifferentPathsGetDifferentHashes(t *testing.T) {
   355  	conf := Config{
   356  		Paths: make([]string, 3),
   357  		Options: TailOptions{
   358  			ReadFrom:              "start",
   359  			Stop:                  true,
   360  			HashStateFileDirPaths: true,
   361  		},
   362  	}
   363  
   364  	statefile1 := getStateFile(conf, "/var/logs/app-1/foobar.log", 1)
   365  	statefile2 := getStateFile(conf, "/var/logs/app-2/foobar.log", 1)
   366  	if statefile1 == statefile2 {
   367  		t.Error("state files with different paths should not be equal")
   368  	}
   369  }
   370  
   371  // boilerplate to spin up a httptest server, create tmpdir, etc.
   372  // to create an environment in which to run these tests
   373  type testSetup struct {
   374  	tmpdir string
   375  	ctx    context.Context
   376  	cancel context.CancelFunc
   377  }
   378  
   379  func (ts *testSetup) start(t *testing.T) {
   380  	logrus.SetOutput(ioutil.Discard)
   381  	tmpdir, err := ioutil.TempDir(os.TempDir(), "test")
   382  	if err != nil {
   383  		t.Fatal(err)
   384  	}
   385  	ts.tmpdir = tmpdir
   386  	ts.ctx, ts.cancel = context.WithCancel(context.Background())
   387  }
   388  
   389  func (ts *testSetup) writeFile(t *testing.T, path string, body string) {
   390  	fh, err := os.Create(path)
   391  	if err != nil {
   392  		t.Fatal(err)
   393  	}
   394  	defer fh.Close()
   395  	fmt.Fprint(fh, body)
   396  }
   397  
   398  func (ts *testSetup) stop() {
   399  	os.RemoveAll(ts.tmpdir)
   400  }
   401  
   402  func checkLinesChan(t *testing.T, actual chan string, expected []string) {
   403  	idx := 0
   404  	for line := range actual {
   405  		if idx < len(expected) && expected[idx] != line {
   406  			t.Errorf("got line '%s', expected line '%s'", line, expected[idx])
   407  		}
   408  		idx++
   409  	}
   410  	if idx != len(expected) {
   411  		t.Errorf("read %d lines from lines channel; expected %d", idx, len(expected))
   412  	}
   413  }
   414  
   415  func checkLinesChanClosed(t *testing.T, actual chan string) {
   416  	// this will block if actual never gets closed
   417  	for {
   418  		select {
   419  		case _, ok := <-actual:
   420  			if !ok {
   421  				return
   422  			}
   423  		case <-time.After(1 * time.Second):
   424  			t.Error("channel read timed out; channel not closed")
   425  			return
   426  		}
   427  	}
   428  }