github.com/crowdsecurity/crowdsec@v1.6.1/pkg/acquisition/modules/loki/loki_test.go (about) 1 package loki_test 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "os" 12 "runtime" 13 "strings" 14 "testing" 15 "time" 16 17 log "github.com/sirupsen/logrus" 18 "github.com/stretchr/testify/assert" 19 tomb "gopkg.in/tomb.v2" 20 21 "github.com/crowdsecurity/go-cs-lib/cstest" 22 23 "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" 24 "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/loki" 25 "github.com/crowdsecurity/crowdsec/pkg/types" 26 ) 27 28 func TestConfiguration(t *testing.T) { 29 log.Infof("Test 'TestConfigure'") 30 31 tests := []struct { 32 config string 33 expectedErr string 34 password string 35 waitForReady time.Duration 36 delayFor time.Duration 37 testName string 38 }{ 39 { 40 config: `foobar: asd`, 41 expectedErr: "line 1: field foobar not found in type loki.LokiConfiguration", 42 testName: "Unknown field", 43 }, 44 { 45 config: ` 46 mode: tail 47 source: loki`, 48 expectedErr: "loki query is mandatory", 49 testName: "Missing url", 50 }, 51 { 52 config: ` 53 mode: tail 54 source: loki 55 url: http://localhost:3100/ 56 `, 57 expectedErr: "loki query is mandatory", 58 testName: "Missing query", 59 }, 60 { 61 config: ` 62 mode: tail 63 source: loki 64 url: http://localhost:3100/ 65 query: > 66 {server="demo"} 67 `, 68 expectedErr: "", 69 testName: "Correct config", 70 }, 71 { 72 config: ` 73 mode: tail 74 source: loki 75 url: http://localhost:3100/ 76 wait_for_ready: 5s 77 query: > 78 {server="demo"} 79 `, 80 expectedErr: "", 81 testName: "Correct config with wait_for_ready", 82 waitForReady: 5 * time.Second, 83 }, 84 { 85 config: ` 86 mode: tail 87 source: loki 88 url: http://localhost:3100/ 89 delay_for: 1s 90 query: > 91 {server="demo"} 92 `, 93 expectedErr: "", 94 testName: "Correct config with delay_for", 95 delayFor: 1 * time.Second, 96 }, 97 { 98 99 config: ` 100 mode: tail 101 source: loki 102 url: http://localhost:3100/ 103 auth: 104 username: foo 105 password: bar 106 query: > 107 {server="demo"} 108 `, 109 expectedErr: "", 110 password: "bar", 111 testName: "Correct config with password", 112 }, 113 { 114 115 config: ` 116 mode: tail 117 source: loki 118 url: http://localhost:3100/ 119 delay_for: 10s 120 query: > 121 {server="demo"} 122 `, 123 expectedErr: "delay_for should be a value between 1s and 5s", 124 testName: "Invalid DelayFor", 125 }, 126 } 127 subLogger := log.WithFields(log.Fields{ 128 "type": "loki", 129 }) 130 131 for _, test := range tests { 132 t.Run(test.testName, func(t *testing.T) { 133 lokiSource := loki.LokiSource{} 134 err := lokiSource.Configure([]byte(test.config), subLogger, configuration.METRICS_NONE) 135 cstest.AssertErrorContains(t, err, test.expectedErr) 136 137 if test.password != "" { 138 p := lokiSource.Config.Auth.Password 139 if test.password != p { 140 t.Fatalf("Password mismatch : %s != %s", test.password, p) 141 } 142 } 143 144 if test.waitForReady != 0 { 145 if lokiSource.Config.WaitForReady != test.waitForReady { 146 t.Fatalf("Wrong WaitForReady %v != %v", lokiSource.Config.WaitForReady, test.waitForReady) 147 } 148 } 149 150 if test.delayFor != 0 { 151 if lokiSource.Config.DelayFor != test.delayFor { 152 t.Fatalf("Wrong DelayFor %v != %v", lokiSource.Config.DelayFor, test.delayFor) 153 } 154 } 155 }) 156 } 157 } 158 159 func TestConfigureDSN(t *testing.T) { 160 log.Infof("Test 'TestConfigureDSN'") 161 162 tests := []struct { 163 name string 164 dsn string 165 expectedErr string 166 since time.Time 167 password string 168 scheme string 169 waitForReady time.Duration 170 delayFor time.Duration 171 }{ 172 { 173 name: "Wrong scheme", 174 dsn: "wrong://", 175 expectedErr: "invalid DSN wrong:// for loki source, must start with loki://", 176 }, 177 { 178 name: "Correct DSN", 179 dsn: `loki://localhost:3100/?query={server="demo"}`, 180 expectedErr: "", 181 }, 182 { 183 name: "Empty host", 184 dsn: "loki://", 185 expectedErr: "empty loki host", 186 }, 187 { 188 name: "Invalid DSN", 189 dsn: "loki", 190 expectedErr: "invalid DSN loki for loki source, must start with loki://", 191 }, 192 { 193 name: "Invalid Delay", 194 dsn: `loki://localhost:3100/?query={server="demo"}&delay_for=10s`, 195 expectedErr: "delay_for should be a value between 1s and 5s", 196 }, 197 { 198 name: "Bad since param", 199 dsn: `loki://127.0.0.1:3100/?since=3h&query={server="demo"}`, 200 since: time.Now().Add(-3 * time.Hour), 201 }, 202 { 203 name: "Basic Auth", 204 dsn: `loki://login:password@localhost:3102/?query={server="demo"}`, 205 password: "password", 206 }, 207 { 208 name: "Correct DSN", 209 dsn: `loki://localhost:3100/?query={server="demo"}&wait_for_ready=5s&delay_for=1s`, 210 expectedErr: "", 211 waitForReady: 5 * time.Second, 212 delayFor: 1 * time.Second, 213 }, 214 { 215 name: "SSL DSN", 216 dsn: `loki://localhost:3100/?ssl=true`, 217 scheme: "https", 218 }, 219 } 220 221 for _, test := range tests { 222 subLogger := log.WithFields(log.Fields{ 223 "type": "loki", 224 "name": test.name, 225 }) 226 227 t.Logf("Test : %s", test.name) 228 229 lokiSource := &loki.LokiSource{} 230 err := lokiSource.ConfigureByDSN(test.dsn, map[string]string{"type": "testtype"}, subLogger, "") 231 cstest.AssertErrorContains(t, err, test.expectedErr) 232 233 noDuration, _ := time.ParseDuration("0s") 234 if lokiSource.Config.Since != noDuration && lokiSource.Config.Since.Round(time.Second) != time.Since(test.since).Round(time.Second) { 235 t.Fatalf("Invalid since %v", lokiSource.Config.Since) 236 } 237 238 if test.password != "" { 239 p := lokiSource.Config.Auth.Password 240 if test.password != p { 241 t.Fatalf("Password mismatch : %s != %s", test.password, p) 242 } 243 } 244 245 if test.scheme != "" { 246 url, _ := url.Parse(lokiSource.Config.URL) 247 if test.scheme != url.Scheme { 248 t.Fatalf("Schema mismatch : %s != %s", test.scheme, url.Scheme) 249 } 250 } 251 252 if test.waitForReady != 0 { 253 if lokiSource.Config.WaitForReady != test.waitForReady { 254 t.Fatalf("Wrong WaitForReady %v != %v", lokiSource.Config.WaitForReady, test.waitForReady) 255 } 256 } 257 258 if test.delayFor != 0 { 259 if lokiSource.Config.DelayFor != test.delayFor { 260 t.Fatalf("Wrong DelayFor %v != %v", lokiSource.Config.DelayFor, test.delayFor) 261 } 262 } 263 } 264 } 265 266 func feedLoki(logger *log.Entry, n int, title string) error { 267 streams := LogStreams{ 268 Streams: []LogStream{ 269 { 270 Stream: map[string]string{ 271 "server": "demo", 272 "domain": "cw.example.com", 273 "key": title, 274 }, 275 Values: make([]LogValue, n), 276 }, 277 }, 278 } 279 for i := 0; i < n; i++ { 280 streams.Streams[0].Values[i] = LogValue{ 281 Time: time.Now(), 282 Line: fmt.Sprintf("Log line #%d %v", i, title), 283 } 284 } 285 286 buff, err := json.Marshal(streams) 287 if err != nil { 288 return err 289 } 290 291 req, err := http.NewRequest(http.MethodPost, "http://127.0.0.1:3100/loki/api/v1/push", bytes.NewBuffer(buff)) 292 if err != nil { 293 return err 294 } 295 296 req.Header.Set("Content-Type", "application/json") 297 req.Header.Set("X-Scope-OrgID", "1234") 298 299 resp, err := http.DefaultClient.Do(req) 300 if err != nil { 301 return err 302 } 303 304 defer resp.Body.Close() 305 306 if resp.StatusCode != http.StatusNoContent { 307 b, _ := io.ReadAll(resp.Body) 308 logger.Error(string(b)) 309 310 return fmt.Errorf("Bad post status %d", resp.StatusCode) 311 } 312 313 logger.Info(n, " Events sent") 314 315 return nil 316 } 317 318 func TestOneShotAcquisition(t *testing.T) { 319 if runtime.GOOS == "windows" { 320 t.Skip("Skipping test on windows") 321 } 322 323 log.SetOutput(os.Stdout) 324 log.SetLevel(log.InfoLevel) 325 log.Info("Test 'TestStreamingAcquisition'") 326 327 title := time.Now().String() // Loki will be messy, with a lot of stuff, lets use a unique key 328 tests := []struct { 329 config string 330 }{ 331 { 332 config: fmt.Sprintf(` 333 mode: cat 334 source: loki 335 url: http://127.0.0.1:3100 336 query: '{server="demo",key="%s"}' 337 headers: 338 x-scope-orgid: "1234" 339 since: 1h 340 `, title), 341 }, 342 } 343 344 for _, ts := range tests { 345 logger := log.New() 346 subLogger := logger.WithFields(log.Fields{ 347 "type": "loki", 348 }) 349 lokiSource := loki.LokiSource{} 350 err := lokiSource.Configure([]byte(ts.config), subLogger, configuration.METRICS_NONE) 351 352 if err != nil { 353 t.Fatalf("Unexpected error : %s", err) 354 } 355 356 err = feedLoki(subLogger, 20, title) 357 if err != nil { 358 t.Fatalf("Unexpected error : %s", err) 359 } 360 361 out := make(chan types.Event) 362 read := 0 363 364 go func() { 365 for { 366 <-out 367 368 read++ 369 } 370 }() 371 372 lokiTomb := tomb.Tomb{} 373 374 err = lokiSource.OneShotAcquisition(out, &lokiTomb) 375 if err != nil { 376 t.Fatalf("Unexpected error : %s", err) 377 } 378 379 assert.Equal(t, 20, read) 380 } 381 } 382 383 func TestStreamingAcquisition(t *testing.T) { 384 if runtime.GOOS == "windows" { 385 t.Skip("Skipping test on windows") 386 } 387 388 log.SetOutput(os.Stdout) 389 log.SetLevel(log.InfoLevel) 390 log.Info("Test 'TestStreamingAcquisition'") 391 392 title := time.Now().String() 393 tests := []struct { 394 name string 395 config string 396 expectedErr string 397 streamErr string 398 expectedLines int 399 }{ 400 { 401 name: "Bad port", 402 config: `mode: tail 403 source: loki 404 url: "http://127.0.0.1:3101" 405 headers: 406 x-scope-orgid: "1234" 407 query: > 408 {server="demo"}`, // No Loki server here 409 expectedErr: "", 410 streamErr: `loki is not ready: context deadline exceeded`, 411 expectedLines: 0, 412 }, 413 { 414 name: "ok", 415 config: `mode: tail 416 source: loki 417 url: "http://127.0.0.1:3100" 418 headers: 419 x-scope-orgid: "1234" 420 query: > 421 {server="demo"}`, 422 expectedErr: "", 423 streamErr: "", 424 expectedLines: 20, 425 }, 426 } 427 428 for _, ts := range tests { 429 t.Run(ts.name, func(t *testing.T) { 430 logger := log.New() 431 subLogger := logger.WithFields(log.Fields{ 432 "type": "loki", 433 "name": ts.name, 434 }) 435 436 out := make(chan types.Event) 437 lokiTomb := tomb.Tomb{} 438 lokiSource := loki.LokiSource{} 439 440 err := lokiSource.Configure([]byte(ts.config), subLogger, configuration.METRICS_NONE) 441 if err != nil { 442 t.Fatalf("Unexpected error : %s", err) 443 } 444 445 err = lokiSource.StreamingAcquisition(out, &lokiTomb) 446 cstest.AssertErrorContains(t, err, ts.streamErr) 447 448 if ts.streamErr != "" { 449 return 450 } 451 452 time.Sleep(time.Second * 2) // We need to give time to start reading from the WS 453 454 readTomb := tomb.Tomb{} 455 readCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) 456 count := 0 457 458 readTomb.Go(func() error { 459 defer cancel() 460 461 for { 462 select { 463 case <-readCtx.Done(): 464 return readCtx.Err() 465 case evt := <-out: 466 count++ 467 468 if !strings.HasSuffix(evt.Line.Raw, title) { 469 return fmt.Errorf("Incorrect suffix : %s", evt.Line.Raw) 470 } 471 472 if count == ts.expectedLines { 473 return nil 474 } 475 } 476 } 477 }) 478 479 err = feedLoki(subLogger, ts.expectedLines, title) 480 if err != nil { 481 t.Fatalf("Unexpected error : %s", err) 482 } 483 484 err = readTomb.Wait() 485 486 cancel() 487 488 if err != nil { 489 t.Fatalf("Unexpected error : %s", err) 490 } 491 492 assert.Equal(t, ts.expectedLines, count) 493 }) 494 } 495 } 496 497 func TestStopStreaming(t *testing.T) { 498 if runtime.GOOS == "windows" { 499 t.Skip("Skipping test on windows") 500 } 501 502 config := ` 503 mode: tail 504 source: loki 505 url: http://127.0.0.1:3100 506 headers: 507 x-scope-orgid: "1234" 508 query: > 509 {server="demo"} 510 ` 511 logger := log.New() 512 subLogger := logger.WithFields(log.Fields{ 513 "type": "loki", 514 }) 515 title := time.Now().String() 516 lokiSource := loki.LokiSource{} 517 518 err := lokiSource.Configure([]byte(config), subLogger, configuration.METRICS_NONE) 519 if err != nil { 520 t.Fatalf("Unexpected error : %s", err) 521 } 522 523 out := make(chan types.Event) 524 525 lokiTomb := &tomb.Tomb{} 526 527 err = lokiSource.StreamingAcquisition(out, lokiTomb) 528 if err != nil { 529 t.Fatalf("Unexpected error : %s", err) 530 } 531 532 time.Sleep(time.Second * 2) 533 534 err = feedLoki(subLogger, 1, title) 535 if err != nil { 536 t.Fatalf("Unexpected error : %s", err) 537 } 538 539 lokiTomb.Kill(nil) 540 541 err = lokiTomb.Wait() 542 if err != nil { 543 t.Fatalf("Unexpected error : %s", err) 544 } 545 } 546 547 type LogStreams struct { 548 Streams []LogStream `json:"streams"` 549 } 550 551 type LogStream struct { 552 Stream map[string]string `json:"stream"` 553 Values []LogValue `json:"values"` 554 } 555 556 type LogValue struct { 557 Time time.Time 558 Line string 559 } 560 561 func (l *LogValue) MarshalJSON() ([]byte, error) { 562 line, err := json.Marshal(l.Line) 563 if err != nil { 564 return nil, err 565 } 566 567 return []byte(fmt.Sprintf(`["%d",%s]`, l.Time.UnixNano(), string(line))), nil 568 }