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 <- &regionResult{
   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  }