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 }