github.com/zhouyu0/docker-note@v0.0.0-20190722021225-b8d3825084db/daemon/logger/awslogs/cloudwatchlogs_test.go (about) 1 package awslogs // import "github.com/docker/docker/daemon/logger/awslogs" 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "reflect" 11 "regexp" 12 "runtime" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/aws/aws-sdk-go/aws" 18 "github.com/aws/aws-sdk-go/aws/awserr" 19 "github.com/aws/aws-sdk-go/aws/request" 20 "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 21 "github.com/docker/docker/daemon/logger" 22 "github.com/docker/docker/daemon/logger/loggerutils" 23 "github.com/docker/docker/dockerversion" 24 "gotest.tools/assert" 25 is "gotest.tools/assert/cmp" 26 ) 27 28 const ( 29 groupName = "groupName" 30 streamName = "streamName" 31 sequenceToken = "sequenceToken" 32 nextSequenceToken = "nextSequenceToken" 33 logline = "this is a log line\r" 34 multilineLogline = "2017-01-01 01:01:44 This is a multiline log entry\r" 35 ) 36 37 // Generates i multi-line events each with j lines 38 func (l *logStream) logGenerator(lineCount int, multilineCount int) { 39 for i := 0; i < multilineCount; i++ { 40 l.Log(&logger.Message{ 41 Line: []byte(multilineLogline), 42 Timestamp: time.Time{}, 43 }) 44 for j := 0; j < lineCount; j++ { 45 l.Log(&logger.Message{ 46 Line: []byte(logline), 47 Timestamp: time.Time{}, 48 }) 49 } 50 } 51 } 52 53 func testEventBatch(events []wrappedEvent) *eventBatch { 54 batch := newEventBatch() 55 for _, event := range events { 56 eventlen := len([]byte(*event.inputLogEvent.Message)) 57 batch.add(event, eventlen) 58 } 59 return batch 60 } 61 62 func TestNewAWSLogsClientUserAgentHandler(t *testing.T) { 63 info := logger.Info{ 64 Config: map[string]string{ 65 regionKey: "us-east-1", 66 }, 67 } 68 69 client, err := newAWSLogsClient(info) 70 assert.NilError(t, err) 71 72 realClient, ok := client.(*cloudwatchlogs.CloudWatchLogs) 73 assert.Check(t, ok, "Could not cast client to cloudwatchlogs.CloudWatchLogs") 74 75 buildHandlerList := realClient.Handlers.Build 76 request := &request.Request{ 77 HTTPRequest: &http.Request{ 78 Header: http.Header{}, 79 }, 80 } 81 buildHandlerList.Run(request) 82 expectedUserAgentString := fmt.Sprintf("Docker %s (%s) %s/%s (%s; %s; %s)", 83 dockerversion.Version, runtime.GOOS, aws.SDKName, aws.SDKVersion, runtime.Version(), runtime.GOOS, runtime.GOARCH) 84 userAgent := request.HTTPRequest.Header.Get("User-Agent") 85 if userAgent != expectedUserAgentString { 86 t.Errorf("Wrong User-Agent string, expected \"%s\" but was \"%s\"", 87 expectedUserAgentString, userAgent) 88 } 89 } 90 91 func TestNewAWSLogsClientAWSLogsEndpoint(t *testing.T) { 92 endpoint := "mock-endpoint" 93 info := logger.Info{ 94 Config: map[string]string{ 95 regionKey: "us-east-1", 96 endpointKey: endpoint, 97 }, 98 } 99 100 client, err := newAWSLogsClient(info) 101 assert.NilError(t, err) 102 103 realClient, ok := client.(*cloudwatchlogs.CloudWatchLogs) 104 assert.Check(t, ok, "Could not cast client to cloudwatchlogs.CloudWatchLogs") 105 106 endpointWithScheme := realClient.Endpoint 107 expectedEndpointWithScheme := "https://" + endpoint 108 assert.Equal(t, endpointWithScheme, expectedEndpointWithScheme, "Wrong endpoint") 109 } 110 111 func TestNewAWSLogsClientRegionDetect(t *testing.T) { 112 info := logger.Info{ 113 Config: map[string]string{}, 114 } 115 116 mockMetadata := newMockMetadataClient() 117 newRegionFinder = func() regionFinder { 118 return mockMetadata 119 } 120 mockMetadata.regionResult <- ®ionResult{ 121 successResult: "us-east-1", 122 } 123 124 _, err := newAWSLogsClient(info) 125 assert.NilError(t, err) 126 } 127 128 func TestCreateSuccess(t *testing.T) { 129 mockClient := newMockClient() 130 stream := &logStream{ 131 client: mockClient, 132 logGroupName: groupName, 133 logStreamName: streamName, 134 } 135 mockClient.createLogStreamResult <- &createLogStreamResult{} 136 137 err := stream.create() 138 139 if err != nil { 140 t.Errorf("Received unexpected err: %v\n", err) 141 } 142 argument := <-mockClient.createLogStreamArgument 143 if argument.LogGroupName == nil { 144 t.Fatal("Expected non-nil LogGroupName") 145 } 146 if *argument.LogGroupName != groupName { 147 t.Errorf("Expected LogGroupName to be %s", groupName) 148 } 149 if argument.LogStreamName == nil { 150 t.Fatal("Expected non-nil LogStreamName") 151 } 152 if *argument.LogStreamName != streamName { 153 t.Errorf("Expected LogStreamName to be %s", streamName) 154 } 155 } 156 157 func TestCreateLogGroupSuccess(t *testing.T) { 158 mockClient := newMockClient() 159 stream := &logStream{ 160 client: mockClient, 161 logGroupName: groupName, 162 logStreamName: streamName, 163 logCreateGroup: true, 164 } 165 mockClient.createLogGroupResult <- &createLogGroupResult{} 166 mockClient.createLogStreamResult <- &createLogStreamResult{} 167 168 err := stream.create() 169 170 if err != nil { 171 t.Errorf("Received unexpected err: %v\n", err) 172 } 173 argument := <-mockClient.createLogStreamArgument 174 if argument.LogGroupName == nil { 175 t.Fatal("Expected non-nil LogGroupName") 176 } 177 if *argument.LogGroupName != groupName { 178 t.Errorf("Expected LogGroupName to be %s", groupName) 179 } 180 if argument.LogStreamName == nil { 181 t.Fatal("Expected non-nil LogStreamName") 182 } 183 if *argument.LogStreamName != streamName { 184 t.Errorf("Expected LogStreamName to be %s", streamName) 185 } 186 } 187 188 func TestCreateError(t *testing.T) { 189 mockClient := newMockClient() 190 stream := &logStream{ 191 client: mockClient, 192 } 193 mockClient.createLogStreamResult <- &createLogStreamResult{ 194 errorResult: errors.New("Error"), 195 } 196 197 err := stream.create() 198 199 if err == nil { 200 t.Fatal("Expected non-nil err") 201 } 202 } 203 204 func TestCreateAlreadyExists(t *testing.T) { 205 mockClient := newMockClient() 206 stream := &logStream{ 207 client: mockClient, 208 } 209 mockClient.createLogStreamResult <- &createLogStreamResult{ 210 errorResult: awserr.New(resourceAlreadyExistsCode, "", nil), 211 } 212 213 err := stream.create() 214 215 assert.NilError(t, err) 216 } 217 218 func TestLogClosed(t *testing.T) { 219 mockClient := newMockClient() 220 stream := &logStream{ 221 client: mockClient, 222 closed: true, 223 } 224 err := stream.Log(&logger.Message{}) 225 if err == nil { 226 t.Fatal("Expected non-nil error") 227 } 228 } 229 230 func TestLogBlocking(t *testing.T) { 231 mockClient := newMockClient() 232 stream := &logStream{ 233 client: mockClient, 234 messages: make(chan *logger.Message), 235 } 236 237 errorCh := make(chan error, 1) 238 started := make(chan bool) 239 go func() { 240 started <- true 241 err := stream.Log(&logger.Message{}) 242 errorCh <- err 243 }() 244 <-started 245 select { 246 case err := <-errorCh: 247 t.Fatal("Expected stream.Log to block: ", err) 248 default: 249 break 250 } 251 select { 252 case <-stream.messages: 253 break 254 default: 255 t.Fatal("Expected to be able to read from stream.messages but was unable to") 256 } 257 select { 258 case err := <-errorCh: 259 assert.NilError(t, err) 260 261 case <-time.After(30 * time.Second): 262 t.Fatal("timed out waiting for read") 263 } 264 } 265 266 func TestLogNonBlockingBufferEmpty(t *testing.T) { 267 mockClient := newMockClient() 268 stream := &logStream{ 269 client: mockClient, 270 messages: make(chan *logger.Message, 1), 271 logNonBlocking: true, 272 } 273 err := stream.Log(&logger.Message{}) 274 assert.NilError(t, err) 275 } 276 277 func TestLogNonBlockingBufferFull(t *testing.T) { 278 mockClient := newMockClient() 279 stream := &logStream{ 280 client: mockClient, 281 messages: make(chan *logger.Message, 1), 282 logNonBlocking: true, 283 } 284 stream.messages <- &logger.Message{} 285 errorCh := make(chan error) 286 started := make(chan bool) 287 go func() { 288 started <- true 289 err := stream.Log(&logger.Message{}) 290 errorCh <- err 291 }() 292 <-started 293 select { 294 case err := <-errorCh: 295 if err == nil { 296 t.Fatal("Expected non-nil error") 297 } 298 case <-time.After(30 * time.Second): 299 t.Fatal("Expected Log call to not block") 300 } 301 } 302 func TestPublishBatchSuccess(t *testing.T) { 303 mockClient := newMockClient() 304 stream := &logStream{ 305 client: mockClient, 306 logGroupName: groupName, 307 logStreamName: streamName, 308 sequenceToken: aws.String(sequenceToken), 309 } 310 mockClient.putLogEventsResult <- &putLogEventsResult{ 311 successResult: &cloudwatchlogs.PutLogEventsOutput{ 312 NextSequenceToken: aws.String(nextSequenceToken), 313 }, 314 } 315 events := []wrappedEvent{ 316 { 317 inputLogEvent: &cloudwatchlogs.InputLogEvent{ 318 Message: aws.String(logline), 319 }, 320 }, 321 } 322 323 stream.publishBatch(testEventBatch(events)) 324 if stream.sequenceToken == nil { 325 t.Fatal("Expected non-nil sequenceToken") 326 } 327 if *stream.sequenceToken != nextSequenceToken { 328 t.Errorf("Expected sequenceToken to be %s, but was %s", nextSequenceToken, *stream.sequenceToken) 329 } 330 argument := <-mockClient.putLogEventsArgument 331 if argument == nil { 332 t.Fatal("Expected non-nil PutLogEventsInput") 333 } 334 if argument.SequenceToken == nil { 335 t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken") 336 } 337 if *argument.SequenceToken != sequenceToken { 338 t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken) 339 } 340 if len(argument.LogEvents) != 1 { 341 t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) 342 } 343 if argument.LogEvents[0] != events[0].inputLogEvent { 344 t.Error("Expected event to equal input") 345 } 346 } 347 348 func TestPublishBatchError(t *testing.T) { 349 mockClient := newMockClient() 350 stream := &logStream{ 351 client: mockClient, 352 logGroupName: groupName, 353 logStreamName: streamName, 354 sequenceToken: aws.String(sequenceToken), 355 } 356 mockClient.putLogEventsResult <- &putLogEventsResult{ 357 errorResult: errors.New("Error"), 358 } 359 360 events := []wrappedEvent{ 361 { 362 inputLogEvent: &cloudwatchlogs.InputLogEvent{ 363 Message: aws.String(logline), 364 }, 365 }, 366 } 367 368 stream.publishBatch(testEventBatch(events)) 369 if stream.sequenceToken == nil { 370 t.Fatal("Expected non-nil sequenceToken") 371 } 372 if *stream.sequenceToken != sequenceToken { 373 t.Errorf("Expected sequenceToken to be %s, but was %s", sequenceToken, *stream.sequenceToken) 374 } 375 } 376 377 func TestPublishBatchInvalidSeqSuccess(t *testing.T) { 378 mockClient := newMockClientBuffered(2) 379 stream := &logStream{ 380 client: mockClient, 381 logGroupName: groupName, 382 logStreamName: streamName, 383 sequenceToken: aws.String(sequenceToken), 384 } 385 mockClient.putLogEventsResult <- &putLogEventsResult{ 386 errorResult: awserr.New(invalidSequenceTokenCode, "use token token", nil), 387 } 388 mockClient.putLogEventsResult <- &putLogEventsResult{ 389 successResult: &cloudwatchlogs.PutLogEventsOutput{ 390 NextSequenceToken: aws.String(nextSequenceToken), 391 }, 392 } 393 394 events := []wrappedEvent{ 395 { 396 inputLogEvent: &cloudwatchlogs.InputLogEvent{ 397 Message: aws.String(logline), 398 }, 399 }, 400 } 401 402 stream.publishBatch(testEventBatch(events)) 403 if stream.sequenceToken == nil { 404 t.Fatal("Expected non-nil sequenceToken") 405 } 406 if *stream.sequenceToken != nextSequenceToken { 407 t.Errorf("Expected sequenceToken to be %s, but was %s", nextSequenceToken, *stream.sequenceToken) 408 } 409 410 argument := <-mockClient.putLogEventsArgument 411 if argument == nil { 412 t.Fatal("Expected non-nil PutLogEventsInput") 413 } 414 if argument.SequenceToken == nil { 415 t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken") 416 } 417 if *argument.SequenceToken != sequenceToken { 418 t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken) 419 } 420 if len(argument.LogEvents) != 1 { 421 t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) 422 } 423 if argument.LogEvents[0] != events[0].inputLogEvent { 424 t.Error("Expected event to equal input") 425 } 426 427 argument = <-mockClient.putLogEventsArgument 428 if argument == nil { 429 t.Fatal("Expected non-nil PutLogEventsInput") 430 } 431 if argument.SequenceToken == nil { 432 t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken") 433 } 434 if *argument.SequenceToken != "token" { 435 t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", "token", *argument.SequenceToken) 436 } 437 if len(argument.LogEvents) != 1 { 438 t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) 439 } 440 if argument.LogEvents[0] != events[0].inputLogEvent { 441 t.Error("Expected event to equal input") 442 } 443 } 444 445 func TestPublishBatchAlreadyAccepted(t *testing.T) { 446 mockClient := newMockClient() 447 stream := &logStream{ 448 client: mockClient, 449 logGroupName: groupName, 450 logStreamName: streamName, 451 sequenceToken: aws.String(sequenceToken), 452 } 453 mockClient.putLogEventsResult <- &putLogEventsResult{ 454 errorResult: awserr.New(dataAlreadyAcceptedCode, "use token token", nil), 455 } 456 457 events := []wrappedEvent{ 458 { 459 inputLogEvent: &cloudwatchlogs.InputLogEvent{ 460 Message: aws.String(logline), 461 }, 462 }, 463 } 464 465 stream.publishBatch(testEventBatch(events)) 466 if stream.sequenceToken == nil { 467 t.Fatal("Expected non-nil sequenceToken") 468 } 469 if *stream.sequenceToken != "token" { 470 t.Errorf("Expected sequenceToken to be %s, but was %s", "token", *stream.sequenceToken) 471 } 472 473 argument := <-mockClient.putLogEventsArgument 474 if argument == nil { 475 t.Fatal("Expected non-nil PutLogEventsInput") 476 } 477 if argument.SequenceToken == nil { 478 t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken") 479 } 480 if *argument.SequenceToken != sequenceToken { 481 t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken) 482 } 483 if len(argument.LogEvents) != 1 { 484 t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) 485 } 486 if argument.LogEvents[0] != events[0].inputLogEvent { 487 t.Error("Expected event to equal input") 488 } 489 } 490 491 func TestCollectBatchSimple(t *testing.T) { 492 mockClient := newMockClient() 493 stream := &logStream{ 494 client: mockClient, 495 logGroupName: groupName, 496 logStreamName: streamName, 497 sequenceToken: aws.String(sequenceToken), 498 messages: make(chan *logger.Message), 499 } 500 mockClient.putLogEventsResult <- &putLogEventsResult{ 501 successResult: &cloudwatchlogs.PutLogEventsOutput{ 502 NextSequenceToken: aws.String(nextSequenceToken), 503 }, 504 } 505 ticks := make(chan time.Time) 506 newTicker = func(_ time.Duration) *time.Ticker { 507 return &time.Ticker{ 508 C: ticks, 509 } 510 } 511 d := make(chan bool) 512 close(d) 513 go stream.collectBatch(d) 514 515 stream.Log(&logger.Message{ 516 Line: []byte(logline), 517 Timestamp: time.Time{}, 518 }) 519 520 ticks <- time.Time{} 521 stream.Close() 522 523 argument := <-mockClient.putLogEventsArgument 524 if argument == nil { 525 t.Fatal("Expected non-nil PutLogEventsInput") 526 } 527 if len(argument.LogEvents) != 1 { 528 t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) 529 } 530 if *argument.LogEvents[0].Message != logline { 531 t.Errorf("Expected message to be %s but was %s", logline, *argument.LogEvents[0].Message) 532 } 533 } 534 535 func TestCollectBatchTicker(t *testing.T) { 536 mockClient := newMockClient() 537 stream := &logStream{ 538 client: mockClient, 539 logGroupName: groupName, 540 logStreamName: streamName, 541 sequenceToken: aws.String(sequenceToken), 542 messages: make(chan *logger.Message), 543 } 544 mockClient.putLogEventsResult <- &putLogEventsResult{ 545 successResult: &cloudwatchlogs.PutLogEventsOutput{ 546 NextSequenceToken: aws.String(nextSequenceToken), 547 }, 548 } 549 ticks := make(chan time.Time) 550 newTicker = func(_ time.Duration) *time.Ticker { 551 return &time.Ticker{ 552 C: ticks, 553 } 554 } 555 556 d := make(chan bool) 557 close(d) 558 go stream.collectBatch(d) 559 560 stream.Log(&logger.Message{ 561 Line: []byte(logline + " 1"), 562 Timestamp: time.Time{}, 563 }) 564 stream.Log(&logger.Message{ 565 Line: []byte(logline + " 2"), 566 Timestamp: time.Time{}, 567 }) 568 569 ticks <- time.Time{} 570 571 // Verify first batch 572 argument := <-mockClient.putLogEventsArgument 573 if argument == nil { 574 t.Fatal("Expected non-nil PutLogEventsInput") 575 } 576 if len(argument.LogEvents) != 2 { 577 t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents)) 578 } 579 if *argument.LogEvents[0].Message != logline+" 1" { 580 t.Errorf("Expected message to be %s but was %s", logline+" 1", *argument.LogEvents[0].Message) 581 } 582 if *argument.LogEvents[1].Message != logline+" 2" { 583 t.Errorf("Expected message to be %s but was %s", logline+" 2", *argument.LogEvents[0].Message) 584 } 585 586 stream.Log(&logger.Message{ 587 Line: []byte(logline + " 3"), 588 Timestamp: time.Time{}, 589 }) 590 591 ticks <- time.Time{} 592 argument = <-mockClient.putLogEventsArgument 593 if argument == nil { 594 t.Fatal("Expected non-nil PutLogEventsInput") 595 } 596 if len(argument.LogEvents) != 1 { 597 t.Errorf("Expected LogEvents to contain 1 elements, but contains %d", len(argument.LogEvents)) 598 } 599 if *argument.LogEvents[0].Message != logline+" 3" { 600 t.Errorf("Expected message to be %s but was %s", logline+" 3", *argument.LogEvents[0].Message) 601 } 602 603 stream.Close() 604 605 } 606 607 func TestCollectBatchMultilinePattern(t *testing.T) { 608 mockClient := newMockClient() 609 multilinePattern := regexp.MustCompile("xxxx") 610 stream := &logStream{ 611 client: mockClient, 612 logGroupName: groupName, 613 logStreamName: streamName, 614 multilinePattern: multilinePattern, 615 sequenceToken: aws.String(sequenceToken), 616 messages: make(chan *logger.Message), 617 } 618 mockClient.putLogEventsResult <- &putLogEventsResult{ 619 successResult: &cloudwatchlogs.PutLogEventsOutput{ 620 NextSequenceToken: aws.String(nextSequenceToken), 621 }, 622 } 623 ticks := make(chan time.Time) 624 newTicker = func(_ time.Duration) *time.Ticker { 625 return &time.Ticker{ 626 C: ticks, 627 } 628 } 629 630 d := make(chan bool) 631 close(d) 632 go stream.collectBatch(d) 633 634 stream.Log(&logger.Message{ 635 Line: []byte(logline), 636 Timestamp: time.Now(), 637 }) 638 stream.Log(&logger.Message{ 639 Line: []byte(logline), 640 Timestamp: time.Now(), 641 }) 642 stream.Log(&logger.Message{ 643 Line: []byte("xxxx " + logline), 644 Timestamp: time.Now(), 645 }) 646 647 ticks <- time.Now() 648 649 // Verify single multiline event 650 argument := <-mockClient.putLogEventsArgument 651 assert.Check(t, argument != nil, "Expected non-nil PutLogEventsInput") 652 assert.Check(t, is.Equal(1, len(argument.LogEvents)), "Expected single multiline event") 653 assert.Check(t, is.Equal(logline+"\n"+logline+"\n", *argument.LogEvents[0].Message), "Received incorrect multiline message") 654 655 stream.Close() 656 657 // Verify single event 658 argument = <-mockClient.putLogEventsArgument 659 assert.Check(t, argument != nil, "Expected non-nil PutLogEventsInput") 660 assert.Check(t, is.Equal(1, len(argument.LogEvents)), "Expected single multiline event") 661 assert.Check(t, is.Equal("xxxx "+logline+"\n", *argument.LogEvents[0].Message), "Received incorrect multiline message") 662 } 663 664 func BenchmarkCollectBatch(b *testing.B) { 665 for i := 0; i < b.N; i++ { 666 mockClient := newMockClient() 667 stream := &logStream{ 668 client: mockClient, 669 logGroupName: groupName, 670 logStreamName: streamName, 671 sequenceToken: aws.String(sequenceToken), 672 messages: make(chan *logger.Message), 673 } 674 mockClient.putLogEventsResult <- &putLogEventsResult{ 675 successResult: &cloudwatchlogs.PutLogEventsOutput{ 676 NextSequenceToken: aws.String(nextSequenceToken), 677 }, 678 } 679 ticks := make(chan time.Time) 680 newTicker = func(_ time.Duration) *time.Ticker { 681 return &time.Ticker{ 682 C: ticks, 683 } 684 } 685 686 d := make(chan bool) 687 close(d) 688 go stream.collectBatch(d) 689 stream.logGenerator(10, 100) 690 ticks <- time.Time{} 691 stream.Close() 692 } 693 } 694 695 func BenchmarkCollectBatchMultilinePattern(b *testing.B) { 696 for i := 0; i < b.N; i++ { 697 mockClient := newMockClient() 698 multilinePattern := regexp.MustCompile(`\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1,2][0-9]|3[0,1]) (?:[0,1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]`) 699 stream := &logStream{ 700 client: mockClient, 701 logGroupName: groupName, 702 logStreamName: streamName, 703 multilinePattern: multilinePattern, 704 sequenceToken: aws.String(sequenceToken), 705 messages: make(chan *logger.Message), 706 } 707 mockClient.putLogEventsResult <- &putLogEventsResult{ 708 successResult: &cloudwatchlogs.PutLogEventsOutput{ 709 NextSequenceToken: aws.String(nextSequenceToken), 710 }, 711 } 712 ticks := make(chan time.Time) 713 newTicker = func(_ time.Duration) *time.Ticker { 714 return &time.Ticker{ 715 C: ticks, 716 } 717 } 718 d := make(chan bool) 719 close(d) 720 go stream.collectBatch(d) 721 stream.logGenerator(10, 100) 722 ticks <- time.Time{} 723 stream.Close() 724 } 725 } 726 727 func TestCollectBatchMultilinePatternMaxEventAge(t *testing.T) { 728 mockClient := newMockClient() 729 multilinePattern := regexp.MustCompile("xxxx") 730 stream := &logStream{ 731 client: mockClient, 732 logGroupName: groupName, 733 logStreamName: streamName, 734 multilinePattern: multilinePattern, 735 sequenceToken: aws.String(sequenceToken), 736 messages: make(chan *logger.Message), 737 } 738 mockClient.putLogEventsResult <- &putLogEventsResult{ 739 successResult: &cloudwatchlogs.PutLogEventsOutput{ 740 NextSequenceToken: aws.String(nextSequenceToken), 741 }, 742 } 743 ticks := make(chan time.Time) 744 newTicker = func(_ time.Duration) *time.Ticker { 745 return &time.Ticker{ 746 C: ticks, 747 } 748 } 749 750 d := make(chan bool) 751 close(d) 752 go stream.collectBatch(d) 753 754 stream.Log(&logger.Message{ 755 Line: []byte(logline), 756 Timestamp: time.Now(), 757 }) 758 759 // Log an event 1 second later 760 stream.Log(&logger.Message{ 761 Line: []byte(logline), 762 Timestamp: time.Now().Add(time.Second), 763 }) 764 765 // Fire ticker batchPublishFrequency seconds later 766 ticks <- time.Now().Add(batchPublishFrequency + time.Second) 767 768 // Verify single multiline event is flushed after maximum event buffer age (batchPublishFrequency) 769 argument := <-mockClient.putLogEventsArgument 770 assert.Check(t, argument != nil, "Expected non-nil PutLogEventsInput") 771 assert.Check(t, is.Equal(1, len(argument.LogEvents)), "Expected single multiline event") 772 assert.Check(t, is.Equal(logline+"\n"+logline+"\n", *argument.LogEvents[0].Message), "Received incorrect multiline message") 773 774 // Log an event 1 second later 775 stream.Log(&logger.Message{ 776 Line: []byte(logline), 777 Timestamp: time.Now().Add(time.Second), 778 }) 779 780 // Fire ticker another batchPublishFrequency seconds later 781 ticks <- time.Now().Add(2*batchPublishFrequency + time.Second) 782 783 // Verify the event buffer is truly flushed - we should only receive a single event 784 argument = <-mockClient.putLogEventsArgument 785 assert.Check(t, argument != nil, "Expected non-nil PutLogEventsInput") 786 assert.Check(t, is.Equal(1, len(argument.LogEvents)), "Expected single multiline event") 787 assert.Check(t, is.Equal(logline+"\n", *argument.LogEvents[0].Message), "Received incorrect multiline message") 788 stream.Close() 789 } 790 791 func TestCollectBatchMultilinePatternNegativeEventAge(t *testing.T) { 792 mockClient := newMockClient() 793 multilinePattern := regexp.MustCompile("xxxx") 794 stream := &logStream{ 795 client: mockClient, 796 logGroupName: groupName, 797 logStreamName: streamName, 798 multilinePattern: multilinePattern, 799 sequenceToken: aws.String(sequenceToken), 800 messages: make(chan *logger.Message), 801 } 802 mockClient.putLogEventsResult <- &putLogEventsResult{ 803 successResult: &cloudwatchlogs.PutLogEventsOutput{ 804 NextSequenceToken: aws.String(nextSequenceToken), 805 }, 806 } 807 ticks := make(chan time.Time) 808 newTicker = func(_ time.Duration) *time.Ticker { 809 return &time.Ticker{ 810 C: ticks, 811 } 812 } 813 814 d := make(chan bool) 815 close(d) 816 go stream.collectBatch(d) 817 818 stream.Log(&logger.Message{ 819 Line: []byte(logline), 820 Timestamp: time.Now(), 821 }) 822 823 // Log an event 1 second later 824 stream.Log(&logger.Message{ 825 Line: []byte(logline), 826 Timestamp: time.Now().Add(time.Second), 827 }) 828 829 // Fire ticker in past to simulate negative event buffer age 830 ticks <- time.Now().Add(-time.Second) 831 832 // Verify single multiline event is flushed with a negative event buffer age 833 argument := <-mockClient.putLogEventsArgument 834 assert.Check(t, argument != nil, "Expected non-nil PutLogEventsInput") 835 assert.Check(t, is.Equal(1, len(argument.LogEvents)), "Expected single multiline event") 836 assert.Check(t, is.Equal(logline+"\n"+logline+"\n", *argument.LogEvents[0].Message), "Received incorrect multiline message") 837 838 stream.Close() 839 } 840 841 func TestCollectBatchMultilinePatternMaxEventSize(t *testing.T) { 842 mockClient := newMockClient() 843 multilinePattern := regexp.MustCompile("xxxx") 844 stream := &logStream{ 845 client: mockClient, 846 logGroupName: groupName, 847 logStreamName: streamName, 848 multilinePattern: multilinePattern, 849 sequenceToken: aws.String(sequenceToken), 850 messages: make(chan *logger.Message), 851 } 852 mockClient.putLogEventsResult <- &putLogEventsResult{ 853 successResult: &cloudwatchlogs.PutLogEventsOutput{ 854 NextSequenceToken: aws.String(nextSequenceToken), 855 }, 856 } 857 ticks := make(chan time.Time) 858 newTicker = func(_ time.Duration) *time.Ticker { 859 return &time.Ticker{ 860 C: ticks, 861 } 862 } 863 864 d := make(chan bool) 865 close(d) 866 go stream.collectBatch(d) 867 868 // Log max event size 869 longline := strings.Repeat("A", maximumBytesPerEvent) 870 stream.Log(&logger.Message{ 871 Line: []byte(longline), 872 Timestamp: time.Now(), 873 }) 874 875 // Log short event 876 shortline := strings.Repeat("B", 100) 877 stream.Log(&logger.Message{ 878 Line: []byte(shortline), 879 Timestamp: time.Now(), 880 }) 881 882 // Fire ticker 883 ticks <- time.Now().Add(batchPublishFrequency) 884 885 // Verify multiline events 886 // We expect a maximum sized event with no new line characters and a 887 // second short event with a new line character at the end 888 argument := <-mockClient.putLogEventsArgument 889 assert.Check(t, argument != nil, "Expected non-nil PutLogEventsInput") 890 assert.Check(t, is.Equal(2, len(argument.LogEvents)), "Expected two events") 891 assert.Check(t, is.Equal(longline, *argument.LogEvents[0].Message), "Received incorrect multiline message") 892 assert.Check(t, is.Equal(shortline+"\n", *argument.LogEvents[1].Message), "Received incorrect multiline message") 893 stream.Close() 894 } 895 896 func TestCollectBatchClose(t *testing.T) { 897 mockClient := newMockClient() 898 stream := &logStream{ 899 client: mockClient, 900 logGroupName: groupName, 901 logStreamName: streamName, 902 sequenceToken: aws.String(sequenceToken), 903 messages: make(chan *logger.Message), 904 } 905 mockClient.putLogEventsResult <- &putLogEventsResult{ 906 successResult: &cloudwatchlogs.PutLogEventsOutput{ 907 NextSequenceToken: aws.String(nextSequenceToken), 908 }, 909 } 910 var ticks = make(chan time.Time) 911 newTicker = func(_ time.Duration) *time.Ticker { 912 return &time.Ticker{ 913 C: ticks, 914 } 915 } 916 917 d := make(chan bool) 918 close(d) 919 go stream.collectBatch(d) 920 921 stream.Log(&logger.Message{ 922 Line: []byte(logline), 923 Timestamp: time.Time{}, 924 }) 925 926 // no ticks 927 stream.Close() 928 929 argument := <-mockClient.putLogEventsArgument 930 if argument == nil { 931 t.Fatal("Expected non-nil PutLogEventsInput") 932 } 933 if len(argument.LogEvents) != 1 { 934 t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents)) 935 } 936 if *argument.LogEvents[0].Message != logline { 937 t.Errorf("Expected message to be %s but was %s", logline, *argument.LogEvents[0].Message) 938 } 939 } 940 941 func TestEffectiveLen(t *testing.T) { 942 tests := []struct { 943 str string 944 effectiveBytes int 945 }{ 946 {"Hello", 5}, 947 {string([]byte{1, 2, 3, 4}), 4}, 948 {"🙃", 4}, 949 {string([]byte{0xFF, 0xFF, 0xFF, 0xFF}), 12}, 950 {"He\xff\xffo", 9}, 951 {"", 0}, 952 } 953 for i, tc := range tests { 954 t.Run(fmt.Sprintf("%d/%s", i, tc.str), func(t *testing.T) { 955 assert.Equal(t, tc.effectiveBytes, effectiveLen(tc.str)) 956 }) 957 } 958 } 959 960 func TestFindValidSplit(t *testing.T) { 961 tests := []struct { 962 str string 963 maxEffectiveBytes int 964 splitOffset int 965 effectiveBytes int 966 }{ 967 {"", 10, 0, 0}, 968 {"Hello", 6, 5, 5}, 969 {"Hello", 2, 2, 2}, 970 {"Hello", 0, 0, 0}, 971 {"🙃", 3, 0, 0}, 972 {"🙃", 4, 4, 4}, 973 {string([]byte{'a', 0xFF}), 2, 1, 1}, 974 {string([]byte{'a', 0xFF}), 4, 2, 4}, 975 } 976 for i, tc := range tests { 977 t.Run(fmt.Sprintf("%d/%s", i, tc.str), func(t *testing.T) { 978 splitOffset, effectiveBytes := findValidSplit(tc.str, tc.maxEffectiveBytes) 979 assert.Equal(t, tc.splitOffset, splitOffset, "splitOffset") 980 assert.Equal(t, tc.effectiveBytes, effectiveBytes, "effectiveBytes") 981 t.Log(tc.str[:tc.splitOffset]) 982 t.Log(tc.str[tc.splitOffset:]) 983 }) 984 } 985 } 986 987 func TestProcessEventEmoji(t *testing.T) { 988 stream := &logStream{} 989 batch := &eventBatch{} 990 bytes := []byte(strings.Repeat("🙃", maximumBytesPerEvent/4+1)) 991 stream.processEvent(batch, bytes, 0) 992 assert.Equal(t, 2, len(batch.batch), "should be two events in the batch") 993 assert.Equal(t, strings.Repeat("🙃", maximumBytesPerEvent/4), aws.StringValue(batch.batch[0].inputLogEvent.Message)) 994 assert.Equal(t, "🙃", aws.StringValue(batch.batch[1].inputLogEvent.Message)) 995 } 996 997 func TestCollectBatchLineSplit(t *testing.T) { 998 mockClient := newMockClient() 999 stream := &logStream{ 1000 client: mockClient, 1001 logGroupName: groupName, 1002 logStreamName: streamName, 1003 sequenceToken: aws.String(sequenceToken), 1004 messages: make(chan *logger.Message), 1005 } 1006 mockClient.putLogEventsResult <- &putLogEventsResult{ 1007 successResult: &cloudwatchlogs.PutLogEventsOutput{ 1008 NextSequenceToken: aws.String(nextSequenceToken), 1009 }, 1010 } 1011 var ticks = make(chan time.Time) 1012 newTicker = func(_ time.Duration) *time.Ticker { 1013 return &time.Ticker{ 1014 C: ticks, 1015 } 1016 } 1017 1018 d := make(chan bool) 1019 close(d) 1020 go stream.collectBatch(d) 1021 1022 longline := strings.Repeat("A", maximumBytesPerEvent) 1023 stream.Log(&logger.Message{ 1024 Line: []byte(longline + "B"), 1025 Timestamp: time.Time{}, 1026 }) 1027 1028 // no ticks 1029 stream.Close() 1030 1031 argument := <-mockClient.putLogEventsArgument 1032 if argument == nil { 1033 t.Fatal("Expected non-nil PutLogEventsInput") 1034 } 1035 if len(argument.LogEvents) != 2 { 1036 t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents)) 1037 } 1038 if *argument.LogEvents[0].Message != longline { 1039 t.Errorf("Expected message to be %s but was %s", longline, *argument.LogEvents[0].Message) 1040 } 1041 if *argument.LogEvents[1].Message != "B" { 1042 t.Errorf("Expected message to be %s but was %s", "B", *argument.LogEvents[1].Message) 1043 } 1044 } 1045 1046 func TestCollectBatchLineSplitWithBinary(t *testing.T) { 1047 mockClient := newMockClient() 1048 stream := &logStream{ 1049 client: mockClient, 1050 logGroupName: groupName, 1051 logStreamName: streamName, 1052 sequenceToken: aws.String(sequenceToken), 1053 messages: make(chan *logger.Message), 1054 } 1055 mockClient.putLogEventsResult <- &putLogEventsResult{ 1056 successResult: &cloudwatchlogs.PutLogEventsOutput{ 1057 NextSequenceToken: aws.String(nextSequenceToken), 1058 }, 1059 } 1060 var ticks = make(chan time.Time) 1061 newTicker = func(_ time.Duration) *time.Ticker { 1062 return &time.Ticker{ 1063 C: ticks, 1064 } 1065 } 1066 1067 d := make(chan bool) 1068 close(d) 1069 go stream.collectBatch(d) 1070 1071 longline := strings.Repeat("\xFF", maximumBytesPerEvent/3) // 0xFF is counted as the 3-byte utf8.RuneError 1072 stream.Log(&logger.Message{ 1073 Line: []byte(longline + "\xFD"), 1074 Timestamp: time.Time{}, 1075 }) 1076 1077 // no ticks 1078 stream.Close() 1079 1080 argument := <-mockClient.putLogEventsArgument 1081 if argument == nil { 1082 t.Fatal("Expected non-nil PutLogEventsInput") 1083 } 1084 if len(argument.LogEvents) != 2 { 1085 t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents)) 1086 } 1087 if *argument.LogEvents[0].Message != longline { 1088 t.Errorf("Expected message to be %s but was %s", longline, *argument.LogEvents[0].Message) 1089 } 1090 if *argument.LogEvents[1].Message != "\xFD" { 1091 t.Errorf("Expected message to be %s but was %s", "\xFD", *argument.LogEvents[1].Message) 1092 } 1093 } 1094 1095 func TestCollectBatchMaxEvents(t *testing.T) { 1096 mockClient := newMockClientBuffered(1) 1097 stream := &logStream{ 1098 client: mockClient, 1099 logGroupName: groupName, 1100 logStreamName: streamName, 1101 sequenceToken: aws.String(sequenceToken), 1102 messages: make(chan *logger.Message), 1103 } 1104 mockClient.putLogEventsResult <- &putLogEventsResult{ 1105 successResult: &cloudwatchlogs.PutLogEventsOutput{ 1106 NextSequenceToken: aws.String(nextSequenceToken), 1107 }, 1108 } 1109 var ticks = make(chan time.Time) 1110 newTicker = func(_ time.Duration) *time.Ticker { 1111 return &time.Ticker{ 1112 C: ticks, 1113 } 1114 } 1115 1116 d := make(chan bool) 1117 close(d) 1118 go stream.collectBatch(d) 1119 1120 line := "A" 1121 for i := 0; i <= maximumLogEventsPerPut; i++ { 1122 stream.Log(&logger.Message{ 1123 Line: []byte(line), 1124 Timestamp: time.Time{}, 1125 }) 1126 } 1127 1128 // no ticks 1129 stream.Close() 1130 1131 argument := <-mockClient.putLogEventsArgument 1132 if argument == nil { 1133 t.Fatal("Expected non-nil PutLogEventsInput") 1134 } 1135 if len(argument.LogEvents) != maximumLogEventsPerPut { 1136 t.Errorf("Expected LogEvents to contain %d elements, but contains %d", maximumLogEventsPerPut, len(argument.LogEvents)) 1137 } 1138 1139 argument = <-mockClient.putLogEventsArgument 1140 if argument == nil { 1141 t.Fatal("Expected non-nil PutLogEventsInput") 1142 } 1143 if len(argument.LogEvents) != 1 { 1144 t.Errorf("Expected LogEvents to contain %d elements, but contains %d", 1, len(argument.LogEvents)) 1145 } 1146 } 1147 1148 func TestCollectBatchMaxTotalBytes(t *testing.T) { 1149 expectedPuts := 2 1150 mockClient := newMockClientBuffered(expectedPuts) 1151 stream := &logStream{ 1152 client: mockClient, 1153 logGroupName: groupName, 1154 logStreamName: streamName, 1155 sequenceToken: aws.String(sequenceToken), 1156 messages: make(chan *logger.Message), 1157 } 1158 for i := 0; i < expectedPuts; i++ { 1159 mockClient.putLogEventsResult <- &putLogEventsResult{ 1160 successResult: &cloudwatchlogs.PutLogEventsOutput{ 1161 NextSequenceToken: aws.String(nextSequenceToken), 1162 }, 1163 } 1164 } 1165 1166 var ticks = make(chan time.Time) 1167 newTicker = func(_ time.Duration) *time.Ticker { 1168 return &time.Ticker{ 1169 C: ticks, 1170 } 1171 } 1172 1173 d := make(chan bool) 1174 close(d) 1175 go stream.collectBatch(d) 1176 1177 numPayloads := maximumBytesPerPut / (maximumBytesPerEvent + perEventBytes) 1178 // maxline is the maximum line that could be submitted after 1179 // accounting for its overhead. 1180 maxline := strings.Repeat("A", maximumBytesPerPut-(perEventBytes*numPayloads)) 1181 // This will be split and batched up to the `maximumBytesPerPut' 1182 // (+/- `maximumBytesPerEvent'). This /should/ be aligned, but 1183 // should also tolerate an offset within that range. 1184 stream.Log(&logger.Message{ 1185 Line: []byte(maxline[:len(maxline)/2]), 1186 Timestamp: time.Time{}, 1187 }) 1188 stream.Log(&logger.Message{ 1189 Line: []byte(maxline[len(maxline)/2:]), 1190 Timestamp: time.Time{}, 1191 }) 1192 stream.Log(&logger.Message{ 1193 Line: []byte("B"), 1194 Timestamp: time.Time{}, 1195 }) 1196 1197 // no ticks, guarantee batch by size (and chan close) 1198 stream.Close() 1199 1200 argument := <-mockClient.putLogEventsArgument 1201 if argument == nil { 1202 t.Fatal("Expected non-nil PutLogEventsInput") 1203 } 1204 1205 // Should total to the maximum allowed bytes. 1206 eventBytes := 0 1207 for _, event := range argument.LogEvents { 1208 eventBytes += len(*event.Message) 1209 } 1210 eventsOverhead := len(argument.LogEvents) * perEventBytes 1211 payloadTotal := eventBytes + eventsOverhead 1212 // lowestMaxBatch allows the payload to be offset if the messages 1213 // don't lend themselves to align with the maximum event size. 1214 lowestMaxBatch := maximumBytesPerPut - maximumBytesPerEvent 1215 1216 if payloadTotal > maximumBytesPerPut { 1217 t.Errorf("Expected <= %d bytes but was %d", maximumBytesPerPut, payloadTotal) 1218 } 1219 if payloadTotal < lowestMaxBatch { 1220 t.Errorf("Batch to be no less than %d but was %d", lowestMaxBatch, payloadTotal) 1221 } 1222 1223 argument = <-mockClient.putLogEventsArgument 1224 if len(argument.LogEvents) != 1 { 1225 t.Errorf("Expected LogEvents to contain 1 elements, but contains %d", len(argument.LogEvents)) 1226 } 1227 message := *argument.LogEvents[len(argument.LogEvents)-1].Message 1228 if message[len(message)-1:] != "B" { 1229 t.Errorf("Expected message to be %s but was %s", "B", message[len(message)-1:]) 1230 } 1231 } 1232 1233 func TestCollectBatchMaxTotalBytesWithBinary(t *testing.T) { 1234 expectedPuts := 2 1235 mockClient := newMockClientBuffered(expectedPuts) 1236 stream := &logStream{ 1237 client: mockClient, 1238 logGroupName: groupName, 1239 logStreamName: streamName, 1240 sequenceToken: aws.String(sequenceToken), 1241 messages: make(chan *logger.Message), 1242 } 1243 for i := 0; i < expectedPuts; i++ { 1244 mockClient.putLogEventsResult <- &putLogEventsResult{ 1245 successResult: &cloudwatchlogs.PutLogEventsOutput{ 1246 NextSequenceToken: aws.String(nextSequenceToken), 1247 }, 1248 } 1249 } 1250 1251 var ticks = make(chan time.Time) 1252 newTicker = func(_ time.Duration) *time.Ticker { 1253 return &time.Ticker{ 1254 C: ticks, 1255 } 1256 } 1257 1258 d := make(chan bool) 1259 close(d) 1260 go stream.collectBatch(d) 1261 1262 // maxline is the maximum line that could be submitted after 1263 // accounting for its overhead. 1264 maxline := strings.Repeat("\xFF", (maximumBytesPerPut-perEventBytes)/3) // 0xFF is counted as the 3-byte utf8.RuneError 1265 // This will be split and batched up to the `maximumBytesPerPut' 1266 // (+/- `maximumBytesPerEvent'). This /should/ be aligned, but 1267 // should also tolerate an offset within that range. 1268 stream.Log(&logger.Message{ 1269 Line: []byte(maxline), 1270 Timestamp: time.Time{}, 1271 }) 1272 stream.Log(&logger.Message{ 1273 Line: []byte("B"), 1274 Timestamp: time.Time{}, 1275 }) 1276 1277 // no ticks, guarantee batch by size (and chan close) 1278 stream.Close() 1279 1280 argument := <-mockClient.putLogEventsArgument 1281 if argument == nil { 1282 t.Fatal("Expected non-nil PutLogEventsInput") 1283 } 1284 1285 // Should total to the maximum allowed bytes. 1286 eventBytes := 0 1287 for _, event := range argument.LogEvents { 1288 eventBytes += effectiveLen(*event.Message) 1289 } 1290 eventsOverhead := len(argument.LogEvents) * perEventBytes 1291 payloadTotal := eventBytes + eventsOverhead 1292 // lowestMaxBatch allows the payload to be offset if the messages 1293 // don't lend themselves to align with the maximum event size. 1294 lowestMaxBatch := maximumBytesPerPut - maximumBytesPerEvent 1295 1296 if payloadTotal > maximumBytesPerPut { 1297 t.Errorf("Expected <= %d bytes but was %d", maximumBytesPerPut, payloadTotal) 1298 } 1299 if payloadTotal < lowestMaxBatch { 1300 t.Errorf("Batch to be no less than %d but was %d", lowestMaxBatch, payloadTotal) 1301 } 1302 1303 argument = <-mockClient.putLogEventsArgument 1304 message := *argument.LogEvents[len(argument.LogEvents)-1].Message 1305 if message[len(message)-1:] != "B" { 1306 t.Errorf("Expected message to be %s but was %s", "B", message[len(message)-1:]) 1307 } 1308 } 1309 1310 func TestCollectBatchWithDuplicateTimestamps(t *testing.T) { 1311 mockClient := newMockClient() 1312 stream := &logStream{ 1313 client: mockClient, 1314 logGroupName: groupName, 1315 logStreamName: streamName, 1316 sequenceToken: aws.String(sequenceToken), 1317 messages: make(chan *logger.Message), 1318 } 1319 mockClient.putLogEventsResult <- &putLogEventsResult{ 1320 successResult: &cloudwatchlogs.PutLogEventsOutput{ 1321 NextSequenceToken: aws.String(nextSequenceToken), 1322 }, 1323 } 1324 ticks := make(chan time.Time) 1325 newTicker = func(_ time.Duration) *time.Ticker { 1326 return &time.Ticker{ 1327 C: ticks, 1328 } 1329 } 1330 1331 d := make(chan bool) 1332 close(d) 1333 go stream.collectBatch(d) 1334 1335 var expectedEvents []*cloudwatchlogs.InputLogEvent 1336 times := maximumLogEventsPerPut 1337 timestamp := time.Now() 1338 for i := 0; i < times; i++ { 1339 line := fmt.Sprintf("%d", i) 1340 if i%2 == 0 { 1341 timestamp.Add(1 * time.Nanosecond) 1342 } 1343 stream.Log(&logger.Message{ 1344 Line: []byte(line), 1345 Timestamp: timestamp, 1346 }) 1347 expectedEvents = append(expectedEvents, &cloudwatchlogs.InputLogEvent{ 1348 Message: aws.String(line), 1349 Timestamp: aws.Int64(timestamp.UnixNano() / int64(time.Millisecond)), 1350 }) 1351 } 1352 1353 ticks <- time.Time{} 1354 stream.Close() 1355 1356 argument := <-mockClient.putLogEventsArgument 1357 if argument == nil { 1358 t.Fatal("Expected non-nil PutLogEventsInput") 1359 } 1360 if len(argument.LogEvents) != times { 1361 t.Errorf("Expected LogEvents to contain %d elements, but contains %d", times, len(argument.LogEvents)) 1362 } 1363 for i := 0; i < times; i++ { 1364 if !reflect.DeepEqual(*argument.LogEvents[i], *expectedEvents[i]) { 1365 t.Errorf("Expected event to be %v but was %v", *expectedEvents[i], *argument.LogEvents[i]) 1366 } 1367 } 1368 } 1369 1370 func TestParseLogOptionsMultilinePattern(t *testing.T) { 1371 info := logger.Info{ 1372 Config: map[string]string{ 1373 multilinePatternKey: "^xxxx", 1374 }, 1375 } 1376 1377 multilinePattern, err := parseMultilineOptions(info) 1378 assert.Check(t, err, "Received unexpected error") 1379 assert.Check(t, multilinePattern.MatchString("xxxx"), "No multiline pattern match found") 1380 } 1381 1382 func TestParseLogOptionsDatetimeFormat(t *testing.T) { 1383 datetimeFormatTests := []struct { 1384 format string 1385 match string 1386 }{ 1387 {"%d/%m/%y %a %H:%M:%S%L %Z", "31/12/10 Mon 08:42:44.345 NZDT"}, 1388 {"%Y-%m-%d %A %I:%M:%S.%f%p%z", "2007-12-04 Monday 08:42:44.123456AM+1200"}, 1389 {"%b|%b|%b|%b|%b|%b|%b|%b|%b|%b|%b|%b", "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec"}, 1390 {"%B|%B|%B|%B|%B|%B|%B|%B|%B|%B|%B|%B", "January|February|March|April|May|June|July|August|September|October|November|December"}, 1391 {"%A|%A|%A|%A|%A|%A|%A", "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday"}, 1392 {"%a|%a|%a|%a|%a|%a|%a", "Mon|Tue|Wed|Thu|Fri|Sat|Sun"}, 1393 {"Day of the week: %w, Day of the year: %j", "Day of the week: 4, Day of the year: 091"}, 1394 } 1395 for _, dt := range datetimeFormatTests { 1396 t.Run(dt.match, func(t *testing.T) { 1397 info := logger.Info{ 1398 Config: map[string]string{ 1399 datetimeFormatKey: dt.format, 1400 }, 1401 } 1402 multilinePattern, err := parseMultilineOptions(info) 1403 assert.Check(t, err, "Received unexpected error") 1404 assert.Check(t, multilinePattern.MatchString(dt.match), "No multiline pattern match found") 1405 }) 1406 } 1407 } 1408 1409 func TestValidateLogOptionsDatetimeFormatAndMultilinePattern(t *testing.T) { 1410 cfg := map[string]string{ 1411 multilinePatternKey: "^xxxx", 1412 datetimeFormatKey: "%Y-%m-%d", 1413 logGroupKey: groupName, 1414 } 1415 conflictingLogOptionsError := "you cannot configure log opt 'awslogs-datetime-format' and 'awslogs-multiline-pattern' at the same time" 1416 1417 err := ValidateLogOpt(cfg) 1418 assert.Check(t, err != nil, "Expected an error") 1419 assert.Check(t, is.Equal(err.Error(), conflictingLogOptionsError), "Received invalid error") 1420 } 1421 1422 func TestCreateTagSuccess(t *testing.T) { 1423 mockClient := newMockClient() 1424 info := logger.Info{ 1425 ContainerName: "/test-container", 1426 ContainerID: "container-abcdefghijklmnopqrstuvwxyz01234567890", 1427 Config: map[string]string{"tag": "{{.Name}}/{{.FullID}}"}, 1428 } 1429 logStreamName, e := loggerutils.ParseLogTag(info, loggerutils.DefaultTemplate) 1430 if e != nil { 1431 t.Errorf("Error generating tag: %q", e) 1432 } 1433 stream := &logStream{ 1434 client: mockClient, 1435 logGroupName: groupName, 1436 logStreamName: logStreamName, 1437 } 1438 mockClient.createLogStreamResult <- &createLogStreamResult{} 1439 1440 err := stream.create() 1441 1442 assert.NilError(t, err) 1443 argument := <-mockClient.createLogStreamArgument 1444 1445 if *argument.LogStreamName != "test-container/container-abcdefghijklmnopqrstuvwxyz01234567890" { 1446 t.Errorf("Expected LogStreamName to be %s", "test-container/container-abcdefghijklmnopqrstuvwxyz01234567890") 1447 } 1448 } 1449 1450 func BenchmarkUnwrapEvents(b *testing.B) { 1451 events := make([]wrappedEvent, maximumLogEventsPerPut) 1452 for i := 0; i < maximumLogEventsPerPut; i++ { 1453 mes := strings.Repeat("0", maximumBytesPerEvent) 1454 events[i].inputLogEvent = &cloudwatchlogs.InputLogEvent{ 1455 Message: &mes, 1456 } 1457 } 1458 1459 b.ResetTimer() 1460 for i := 0; i < b.N; i++ { 1461 res := unwrapEvents(events) 1462 assert.Check(b, is.Len(res, maximumLogEventsPerPut)) 1463 } 1464 } 1465 1466 func TestNewAWSLogsClientCredentialEndpointDetect(t *testing.T) { 1467 // required for the cloudwatchlogs client 1468 os.Setenv("AWS_REGION", "us-west-2") 1469 defer os.Unsetenv("AWS_REGION") 1470 1471 credsResp := `{ 1472 "AccessKeyId" : "test-access-key-id", 1473 "SecretAccessKey": "test-secret-access-key" 1474 }` 1475 1476 expectedAccessKeyID := "test-access-key-id" 1477 expectedSecretAccessKey := "test-secret-access-key" 1478 1479 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1480 w.Header().Set("Content-Type", "application/json") 1481 fmt.Fprintln(w, credsResp) 1482 })) 1483 defer testServer.Close() 1484 1485 // set the SDKEndpoint in the driver 1486 newSDKEndpoint = testServer.URL 1487 1488 info := logger.Info{ 1489 Config: map[string]string{}, 1490 } 1491 1492 info.Config["awslogs-credentials-endpoint"] = "/creds" 1493 1494 c, err := newAWSLogsClient(info) 1495 assert.Check(t, err) 1496 1497 client := c.(*cloudwatchlogs.CloudWatchLogs) 1498 1499 creds, err := client.Config.Credentials.Get() 1500 assert.Check(t, err) 1501 1502 assert.Check(t, is.Equal(expectedAccessKeyID, creds.AccessKeyID)) 1503 assert.Check(t, is.Equal(expectedSecretAccessKey, creds.SecretAccessKey)) 1504 } 1505 1506 func TestNewAWSLogsClientCredentialEnvironmentVariable(t *testing.T) { 1507 // required for the cloudwatchlogs client 1508 os.Setenv("AWS_REGION", "us-west-2") 1509 defer os.Unsetenv("AWS_REGION") 1510 1511 expectedAccessKeyID := "test-access-key-id" 1512 expectedSecretAccessKey := "test-secret-access-key" 1513 1514 os.Setenv("AWS_ACCESS_KEY_ID", expectedAccessKeyID) 1515 defer os.Unsetenv("AWS_ACCESS_KEY_ID") 1516 1517 os.Setenv("AWS_SECRET_ACCESS_KEY", expectedSecretAccessKey) 1518 defer os.Unsetenv("AWS_SECRET_ACCESS_KEY") 1519 1520 info := logger.Info{ 1521 Config: map[string]string{}, 1522 } 1523 1524 c, err := newAWSLogsClient(info) 1525 assert.Check(t, err) 1526 1527 client := c.(*cloudwatchlogs.CloudWatchLogs) 1528 1529 creds, err := client.Config.Credentials.Get() 1530 assert.Check(t, err) 1531 1532 assert.Check(t, is.Equal(expectedAccessKeyID, creds.AccessKeyID)) 1533 assert.Check(t, is.Equal(expectedSecretAccessKey, creds.SecretAccessKey)) 1534 } 1535 1536 func TestNewAWSLogsClientCredentialSharedFile(t *testing.T) { 1537 // required for the cloudwatchlogs client 1538 os.Setenv("AWS_REGION", "us-west-2") 1539 defer os.Unsetenv("AWS_REGION") 1540 1541 expectedAccessKeyID := "test-access-key-id" 1542 expectedSecretAccessKey := "test-secret-access-key" 1543 1544 contentStr := ` 1545 [default] 1546 aws_access_key_id = "test-access-key-id" 1547 aws_secret_access_key = "test-secret-access-key" 1548 ` 1549 content := []byte(contentStr) 1550 1551 tmpfile, err := ioutil.TempFile("", "example") 1552 defer os.Remove(tmpfile.Name()) // clean up 1553 assert.Check(t, err) 1554 1555 _, err = tmpfile.Write(content) 1556 assert.Check(t, err) 1557 1558 err = tmpfile.Close() 1559 assert.Check(t, err) 1560 1561 os.Unsetenv("AWS_ACCESS_KEY_ID") 1562 os.Unsetenv("AWS_SECRET_ACCESS_KEY") 1563 1564 os.Setenv("AWS_SHARED_CREDENTIALS_FILE", tmpfile.Name()) 1565 defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE") 1566 1567 info := logger.Info{ 1568 Config: map[string]string{}, 1569 } 1570 1571 c, err := newAWSLogsClient(info) 1572 assert.Check(t, err) 1573 1574 client := c.(*cloudwatchlogs.CloudWatchLogs) 1575 1576 creds, err := client.Config.Credentials.Get() 1577 assert.Check(t, err) 1578 1579 assert.Check(t, is.Equal(expectedAccessKeyID, creds.AccessKeyID)) 1580 assert.Check(t, is.Equal(expectedSecretAccessKey, creds.SecretAccessKey)) 1581 }