github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/elasticsearch_integration_test.go (about) 1 package writer 2 3 import ( 4 "context" 5 "encoding/json" 6 "flag" 7 "fmt" 8 "net/http" 9 "regexp" 10 "strings" 11 "sync" 12 "testing" 13 "time" 14 15 "github.com/Jeffail/benthos/v3/lib/log" 16 "github.com/Jeffail/benthos/v3/lib/message" 17 "github.com/Jeffail/benthos/v3/lib/metrics" 18 "github.com/olivere/elastic/v7" 19 "github.com/ory/dockertest/v3" 20 ) 21 22 func TestElasticIntegration(t *testing.T) { 23 if m := flag.Lookup("test.run").Value.String(); m == "" || regexp.MustCompile(strings.Split(m, "/")[0]).FindString(t.Name()) == "" { 24 t.Skip("Skipping as execution was not requested explicitly using go test -run ^TestIntegration$") 25 } 26 27 if testing.Short() { 28 t.Skip("Skipping integration test in short mode") 29 } 30 31 pool, err := dockertest.NewPool("") 32 if err != nil { 33 t.Skipf("Could not connect to docker: %s", err) 34 } 35 pool.MaxWait = time.Second * 30 36 37 resource, err := pool.Run("elasticsearch", "7.17.0", []string{ 38 "discovery.type=single-node", 39 }) 40 if err != nil { 41 t.Fatalf("Could not start resource: %s", err) 42 } 43 44 urls := []string{fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("9200/tcp"))} 45 46 var client *elastic.Client 47 48 if err = pool.Retry(func() error { 49 opts := []elastic.ClientOptionFunc{ 50 elastic.SetURL(urls...), 51 elastic.SetHttpClient(&http.Client{ 52 Timeout: time.Second, 53 }), 54 elastic.SetSniff(false), 55 } 56 57 var cerr error 58 client, cerr = elastic.NewClient(opts...) 59 60 if cerr == nil { 61 index := `{ 62 "settings":{ 63 "number_of_shards": 1, 64 "number_of_replicas": 0 65 }, 66 "mappings":{ 67 "properties": { 68 "user":{ 69 "type":"keyword" 70 }, 71 "message":{ 72 "type":"text", 73 "store": true, 74 "fielddata": true 75 } 76 } 77 } 78 }` 79 _, cerr = client. 80 CreateIndex("test_conn_index"). 81 Timeout("20s"). 82 Body(index). 83 Do(context.Background()) 84 if cerr == nil { 85 _, cerr = client. 86 CreateIndex("test_conn_index_2"). 87 Timeout("20s"). 88 Body(index). 89 Do(context.Background()) 90 } 91 92 } 93 return cerr 94 }); err != nil { 95 t.Fatalf("Could not connect to docker resource: %s", err) 96 } 97 98 defer func() { 99 if err = pool.Purge(resource); err != nil { 100 t.Logf("Failed to clean up docker resource: %v", err) 101 } 102 }() 103 104 t.Run("TestElasticNoIndex", func(te *testing.T) { 105 testElasticNoIndex(urls, client, te) 106 }) 107 108 t.Run("TestElasticParallelWrites", func(te *testing.T) { 109 testElasticParallelWrites(urls, client, te) 110 }) 111 112 t.Run("TestElasticErrorHandling", func(te *testing.T) { 113 testElasticErrorHandling(urls, client, te) 114 }) 115 116 t.Run("TestElasticConnect", func(te *testing.T) { 117 testElasticConnect(urls, client, te) 118 }) 119 120 t.Run("TestElasticIndexInterpolation", func(te *testing.T) { 121 testElasticIndexInterpolation(urls, client, te) 122 }) 123 124 t.Run("TestElasticBatch", func(te *testing.T) { 125 testElasticBatch(urls, client, te) 126 }) 127 128 t.Run("TestElasticBatchDelete", func(te *testing.T) { 129 testElasticBatchDelete(urls, client, te) 130 }) 131 132 t.Run("TestElasticBatchIDCollision", func(te *testing.T) { 133 testElasticBatchIDCollision(urls, client, te) 134 }) 135 } 136 137 func testElasticNoIndex(urls []string, client *elastic.Client, t *testing.T) { 138 conf := NewElasticsearchConfig() 139 conf.Index = "does_not_exist" 140 conf.ID = "foo-${!count(\"noIndexTest\")}" 141 conf.URLs = urls 142 conf.MaxRetries = 1 143 conf.Backoff.MaxElapsedTime = "1s" 144 conf.Sniff = false 145 146 m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop()) 147 if err != nil { 148 t.Fatal(err) 149 } 150 151 if err = m.Connect(); err != nil { 152 t.Error(err) 153 } 154 155 defer func() { 156 m.CloseAsync() 157 if cErr := m.WaitForClose(time.Second); cErr != nil { 158 t.Error(cErr) 159 } 160 }() 161 162 if err = m.Write(message.New([][]byte{[]byte(`{"message":"hello world","user":"1"}`)})); err != nil { 163 t.Error(err) 164 } 165 166 if err = m.Write(message.New([][]byte{ 167 []byte(`{"message":"hello world","user":"2"}`), 168 []byte(`{"message":"hello world","user":"3"}`), 169 })); err != nil { 170 t.Error(err) 171 } 172 173 for i := 0; i < 3; i++ { 174 id := fmt.Sprintf("foo-%v", i+1) 175 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 176 get, err := client.Get(). 177 Index("does_not_exist"). 178 Id(id). 179 Do(context.Background()) 180 if err != nil { 181 t.Fatalf("Failed to get doc '%v': %v", id, err) 182 } 183 if !get.Found { 184 t.Errorf("document %v not found", i) 185 } 186 } 187 } 188 189 func testElasticParallelWrites(urls []string, client *elastic.Client, t *testing.T) { 190 conf := NewElasticsearchConfig() 191 conf.Index = "new_index_parallel_writes" 192 conf.ID = "${!json(\"key\")}" 193 conf.URLs = urls 194 conf.MaxRetries = 1 195 conf.Backoff.MaxElapsedTime = "1s" 196 conf.Sniff = false 197 198 m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop()) 199 if err != nil { 200 t.Fatal(err) 201 } 202 203 if err = m.Connect(); err != nil { 204 t.Error(err) 205 } 206 207 defer func() { 208 m.CloseAsync() 209 if cErr := m.WaitForClose(time.Second); cErr != nil { 210 t.Error(cErr) 211 } 212 }() 213 214 N := 10 215 216 startChan := make(chan struct{}) 217 wg := sync.WaitGroup{} 218 wg.Add(N) 219 220 docs := map[string]string{} 221 222 for i := 0; i < N; i++ { 223 str := fmt.Sprintf(`{"key":"doc-%v","message":"foobar"}`, i) 224 docs[fmt.Sprintf("doc-%v", i)] = str 225 go func(content string) { 226 <-startChan 227 if lerr := m.Write(message.New([][]byte{[]byte(content)})); lerr != nil { 228 t.Error(lerr) 229 } 230 wg.Done() 231 }(str) 232 } 233 234 close(startChan) 235 wg.Wait() 236 237 for id, exp := range docs { 238 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 239 get, err := client.Get(). 240 Index("new_index_parallel_writes"). 241 Type("_doc"). 242 Id(id). 243 Do(context.Background()) 244 if err != nil { 245 t.Fatalf("Failed to get doc '%v': %v", id, err) 246 } 247 if !get.Found { 248 t.Errorf("document %v not found", id) 249 } else { 250 rawBytes, err := get.Source.MarshalJSON() 251 if err != nil { 252 t.Error(err) 253 } else if act := string(rawBytes); act != exp { 254 t.Errorf("Wrong result: %v != %v", act, exp) 255 } 256 } 257 } 258 } 259 260 func testElasticErrorHandling(urls []string, client *elastic.Client, t *testing.T) { 261 conf := NewElasticsearchConfig() 262 conf.Index = "test_conn_index?" 263 conf.ID = "foo-static" 264 conf.URLs = urls 265 conf.Backoff.MaxInterval = "1s" 266 conf.Sniff = false 267 268 m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop()) 269 if err != nil { 270 t.Fatal(err) 271 } 272 273 if err = m.Connect(); err != nil { 274 t.Fatal(err) 275 } 276 277 defer func() { 278 m.CloseAsync() 279 if cErr := m.WaitForClose(time.Second); cErr != nil { 280 t.Error(cErr) 281 } 282 }() 283 284 if err = m.Write(message.New([][]byte{[]byte(`{"message":true}`)})); err == nil { 285 t.Error("Expected error") 286 } 287 288 if err = m.Write(message.New([][]byte{[]byte(`{"message":"foo"}`), []byte(`{"message":"bar"}`)})); err == nil { 289 t.Error("Expected error") 290 } 291 } 292 293 func testElasticConnect(urls []string, client *elastic.Client, t *testing.T) { 294 conf := NewElasticsearchConfig() 295 conf.Index = "test_conn_index" 296 conf.ID = "foo-${!count(\"foo\")}" 297 conf.URLs = urls 298 conf.Type = "_doc" 299 conf.Sniff = false 300 301 m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop()) 302 if err != nil { 303 t.Fatal(err) 304 } 305 306 if err = m.Connect(); err != nil { 307 t.Fatal(err) 308 } 309 310 defer func() { 311 m.CloseAsync() 312 if cErr := m.WaitForClose(time.Second); cErr != nil { 313 t.Error(cErr) 314 } 315 }() 316 317 N := 10 318 319 testMsgs := [][][]byte{} 320 for i := 0; i < N; i++ { 321 testMsgs = append(testMsgs, [][]byte{ 322 []byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)), 323 }) 324 } 325 for i := 0; i < N; i++ { 326 if err = m.Write(message.New(testMsgs[i])); err != nil { 327 t.Fatal(err) 328 } 329 } 330 for i := 0; i < N; i++ { 331 id := fmt.Sprintf("foo-%v", i+1) 332 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 333 get, err := client.Get(). 334 Index("test_conn_index"). 335 Type("_doc"). 336 Id(id). 337 Do(context.Background()) 338 if err != nil { 339 t.Fatalf("Failed to get doc '%v': %v", id, err) 340 } 341 if !get.Found { 342 t.Errorf("document %v not found", i) 343 } 344 345 var sourceBytes []byte 346 sourceBytes, err = get.Source.MarshalJSON() 347 if err != nil { 348 t.Error(err) 349 } else if exp, act := string(testMsgs[i][0]), string(sourceBytes); exp != act { 350 t.Errorf("wrong user field returned: %v != %v", act, exp) 351 } 352 } 353 } 354 355 func testElasticIndexInterpolation(urls []string, client *elastic.Client, t *testing.T) { 356 conf := NewElasticsearchConfig() 357 conf.Index = "${!meta(\"index\")}" 358 conf.ID = "bar-${!count(\"bar\")}" 359 conf.URLs = urls 360 conf.Type = "_doc" 361 conf.Sniff = false 362 363 m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop()) 364 if err != nil { 365 t.Fatal(err) 366 } 367 368 if err = m.Connect(); err != nil { 369 t.Fatal(err) 370 } 371 372 defer func() { 373 m.CloseAsync() 374 if cErr := m.WaitForClose(time.Second); cErr != nil { 375 t.Error(cErr) 376 } 377 }() 378 379 N := 10 380 381 testMsgs := [][][]byte{} 382 for i := 0; i < N; i++ { 383 testMsgs = append(testMsgs, [][]byte{ 384 []byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)), 385 }) 386 } 387 for i := 0; i < N; i++ { 388 msg := message.New(testMsgs[i]) 389 msg.Get(0).Metadata().Set("index", "test_conn_index") 390 if err = m.Write(msg); err != nil { 391 t.Fatal(err) 392 } 393 } 394 for i := 0; i < N; i++ { 395 id := fmt.Sprintf("bar-%v", i+1) 396 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 397 get, err := client.Get(). 398 Index("test_conn_index"). 399 Type("_doc"). 400 Id(id). 401 Do(context.Background()) 402 if err != nil { 403 t.Fatalf("Failed to get doc '%v': %v", id, err) 404 } 405 if !get.Found { 406 t.Errorf("document %v not found", i) 407 } 408 409 var sourceBytes []byte 410 sourceBytes, err = get.Source.MarshalJSON() 411 if err != nil { 412 t.Error(err) 413 } else if exp, act := string(testMsgs[i][0]), string(sourceBytes); exp != act { 414 t.Errorf("wrong user field returned: %v != %v", act, exp) 415 } 416 } 417 } 418 419 func testElasticBatch(urls []string, client *elastic.Client, t *testing.T) { 420 conf := NewElasticsearchConfig() 421 conf.Index = "${!meta(\"index\")}" 422 conf.ID = "bar-${!count(\"bar\")}" 423 conf.URLs = urls 424 conf.Sniff = false 425 conf.Type = "_doc" 426 427 m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop()) 428 if err != nil { 429 t.Fatal(err) 430 } 431 432 if err = m.Connect(); err != nil { 433 t.Fatal(err) 434 } 435 436 defer func() { 437 m.CloseAsync() 438 if cErr := m.WaitForClose(time.Second); cErr != nil { 439 t.Error(cErr) 440 } 441 }() 442 443 N := 10 444 445 testMsg := [][]byte{} 446 for i := 0; i < N; i++ { 447 testMsg = append(testMsg, 448 []byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)), 449 ) 450 } 451 msg := message.New(testMsg) 452 for i := 0; i < N; i++ { 453 msg.Get(i).Metadata().Set("index", "test_conn_index") 454 } 455 if err = m.Write(msg); err != nil { 456 t.Fatal(err) 457 } 458 for i := 0; i < N; i++ { 459 id := fmt.Sprintf("bar-%v", i+1) 460 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 461 get, err := client.Get(). 462 Index("test_conn_index"). 463 Type("_doc"). 464 Id(id). 465 Do(context.Background()) 466 if err != nil { 467 t.Fatalf("Failed to get doc '%v': %v", id, err) 468 } 469 if !get.Found { 470 t.Errorf("document %v not found", i) 471 } 472 473 var sourceBytes []byte 474 sourceBytes, err = get.Source.MarshalJSON() 475 if err != nil { 476 t.Error(err) 477 } else if exp, act := string(testMsg[i]), string(sourceBytes); exp != act { 478 t.Errorf("wrong user field returned: %v != %v", act, exp) 479 } 480 } 481 } 482 483 func testElasticBatchDelete(urls []string, client *elastic.Client, t *testing.T) { 484 conf := NewElasticsearchConfig() 485 conf.Index = "${!meta(\"index\")}" 486 conf.ID = "bar-${!count(\"elasticBatchDeleteMessages\")}" 487 conf.Action = "${!meta(\"elastic_action\")}" 488 conf.URLs = urls 489 conf.Sniff = false 490 conf.Type = "_doc" 491 492 m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop()) 493 if err != nil { 494 t.Fatal(err) 495 } 496 497 if err = m.Connect(); err != nil { 498 t.Fatal(err) 499 } 500 501 defer func() { 502 m.CloseAsync() 503 if cErr := m.WaitForClose(time.Second); cErr != nil { 504 t.Error(cErr) 505 } 506 }() 507 508 N := 10 509 510 testMsg := [][]byte{} 511 for i := 0; i < N; i++ { 512 testMsg = append(testMsg, 513 []byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)), 514 ) 515 } 516 msg := message.New(testMsg) 517 for i := 0; i < N; i++ { 518 msg.Get(i).Metadata().Set("index", "test_conn_index") 519 msg.Get(i).Metadata().Set("elastic_action", "index") 520 } 521 if err = m.Write(msg); err != nil { 522 t.Fatal(err) 523 } 524 for i := 0; i < N; i++ { 525 id := fmt.Sprintf("bar-%v", i+1) 526 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 527 get, err := client.Get(). 528 Index("test_conn_index"). 529 Type("_doc"). 530 Id(id). 531 Do(context.Background()) 532 if err != nil { 533 t.Fatalf("Failed to get doc '%v': %v", id, err) 534 } 535 if !get.Found { 536 t.Errorf("document %v not found", i) 537 } 538 539 var sourceBytes []byte 540 sourceBytes, err = get.Source.MarshalJSON() 541 if err != nil { 542 t.Error(err) 543 } else if exp, act := string(testMsg[i]), string(sourceBytes); exp != act { 544 t.Errorf("wrong user field returned: %v != %v", act, exp) 545 } 546 } 547 548 // Set elastic_action to deleted for some message parts 549 for i := N / 2; i < N; i++ { 550 msg.Get(i).Metadata().Set("elastic_action", "delete") 551 } 552 553 if err = m.Write(msg); err != nil { 554 t.Fatal(err) 555 } 556 557 for i := 0; i < N; i++ { 558 id := fmt.Sprintf("bar-%v", i+1) 559 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 560 get, err := client.Get(). 561 Index("test_conn_index"). 562 Type("_doc"). 563 Id(id). 564 Do(context.Background()) 565 if err != nil { 566 t.Fatalf("Failed to get doc '%v': %v", id, err) 567 } 568 partAction := msg.Get(i).Metadata().Get("elastic_action") 569 if partAction == "deleted" && get.Found { 570 t.Errorf("document %v found when it should have been deleted", i) 571 } else if partAction != "deleted" && !get.Found { 572 t.Errorf("document %v was not found", i) 573 } 574 } 575 } 576 577 func testElasticBatchIDCollision(urls []string, client *elastic.Client, t *testing.T) { 578 conf := NewElasticsearchConfig() 579 conf.Index = `${!meta("index")}` 580 conf.ID = "bar-id" 581 conf.URLs = urls 582 conf.Sniff = false 583 conf.Type = "_doc" 584 585 m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop()) 586 if err != nil { 587 t.Fatal(err) 588 } 589 590 if err = m.Connect(); err != nil { 591 t.Fatal(err) 592 } 593 594 defer func() { 595 m.CloseAsync() 596 if cErr := m.WaitForClose(time.Second); cErr != nil { 597 t.Error(cErr) 598 } 599 }() 600 601 N := 2 602 603 testMsg := [][]byte{} 604 for i := 0; i < N; i++ { 605 testMsg = append(testMsg, 606 []byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)), 607 ) 608 } 609 610 msg := message.New(testMsg) 611 msg.Get(0).Metadata().Set("index", "test_conn_index") 612 msg.Get(1).Metadata().Set("index", "test_conn_index_2") 613 614 if err = m.Write(msg); err != nil { 615 t.Fatal(err) 616 } 617 for i := 0; i < N; i++ { 618 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 619 get, err := client.Get(). 620 Index(msg.Get(i).Metadata().Get("index")). 621 Type("_doc"). 622 Id(conf.ID). 623 Do(context.Background()) 624 if err != nil { 625 t.Fatalf("Failed to get doc '%v': %v", conf.ID, err) 626 } 627 if !get.Found { 628 t.Errorf("document %v not found", i) 629 } 630 631 var sourceBytes []byte 632 sourceBytes, err = get.Source.MarshalJSON() 633 if err != nil { 634 t.Error(err) 635 } else if exp, act := string(testMsg[i]), string(sourceBytes); exp != act { 636 t.Errorf("wrong user field returned: %v != %v", act, exp) 637 } 638 } 639 640 // testing sequential updates to a document created above 641 conf.Action = "update" 642 conf.Index = "test_conn_index" 643 conf.ID = "bar-id" 644 645 m, err = NewElasticsearch(conf, log.Noop(), metrics.Noop()) 646 if err != nil { 647 t.Fatal(err) 648 } 649 650 if err = m.Connect(); err != nil { 651 t.Fatal(err) 652 } 653 654 defer func() { 655 m.CloseAsync() 656 if cErr := m.WaitForClose(time.Second); cErr != nil { 657 t.Error(cErr) 658 } 659 }() 660 661 testMsg = [][]byte{ 662 []byte(`{"message":"goodbye"}`), 663 []byte(`{"user": "updated"}`), 664 } 665 msg = message.New(testMsg) 666 if err = m.Write(msg); err != nil { 667 t.Fatal(err) 668 } 669 670 // nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index() 671 get, err := client.Get(). 672 Index("test_conn_index"). 673 Type("_doc"). 674 Id(conf.ID). 675 Do(context.Background()) 676 677 if err != nil { 678 t.Fatalf("Failed to get doc '%v': %v", conf.ID, err) 679 } 680 if !get.Found { 681 t.Errorf("document not found") 682 } 683 684 var doc struct { 685 Message string `json:"message"` 686 User string `json:"user"` 687 } 688 err = json.Unmarshal(get.Source, &doc) 689 if err != nil { 690 t.Error(err) 691 } else if doc.User != "updated" { 692 t.Errorf("wrong user field returned: %v != %v", doc.User, "updated") 693 } else if doc.Message != "goodbye" { 694 t.Errorf("wrong message field returned: %v != %v", doc.Message, "goodbye") 695 } 696 }