github.com/jiasir/docker@v1.3.3-0.20170609024000-252e610103e7/daemon/logger/awslogs/cloudwatchlogs.go (about)

     1  // Package awslogs provides the logdriver for forwarding container logs to Amazon CloudWatch Logs
     2  package awslogs
     3  
     4  import (
     5  	"bytes"
     6  	"fmt"
     7  	"os"
     8  	"regexp"
     9  	"runtime"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/Sirupsen/logrus"
    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/ec2metadata"
    20  	"github.com/aws/aws-sdk-go/aws/request"
    21  	"github.com/aws/aws-sdk-go/aws/session"
    22  	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
    23  	"github.com/docker/docker/daemon/logger"
    24  	"github.com/docker/docker/daemon/logger/loggerutils"
    25  	"github.com/docker/docker/dockerversion"
    26  	"github.com/docker/docker/pkg/templates"
    27  	"github.com/pkg/errors"
    28  )
    29  
    30  const (
    31  	name                  = "awslogs"
    32  	regionKey             = "awslogs-region"
    33  	regionEnvKey          = "AWS_REGION"
    34  	logGroupKey           = "awslogs-group"
    35  	logStreamKey          = "awslogs-stream"
    36  	logCreateGroupKey     = "awslogs-create-group"
    37  	tagKey                = "tag"
    38  	datetimeFormatKey     = "awslogs-datetime-format"
    39  	multilinePatternKey   = "awslogs-multiline-pattern"
    40  	batchPublishFrequency = 5 * time.Second
    41  
    42  	// See: http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
    43  	perEventBytes          = 26
    44  	maximumBytesPerPut     = 1048576
    45  	maximumLogEventsPerPut = 10000
    46  
    47  	// See: http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_limits.html
    48  	maximumBytesPerEvent = 262144 - perEventBytes
    49  
    50  	resourceAlreadyExistsCode = "ResourceAlreadyExistsException"
    51  	dataAlreadyAcceptedCode   = "DataAlreadyAcceptedException"
    52  	invalidSequenceTokenCode  = "InvalidSequenceTokenException"
    53  	resourceNotFoundCode      = "ResourceNotFoundException"
    54  
    55  	userAgentHeader = "User-Agent"
    56  )
    57  
    58  type logStream struct {
    59  	logStreamName    string
    60  	logGroupName     string
    61  	logCreateGroup   bool
    62  	multilinePattern *regexp.Regexp
    63  	client           api
    64  	messages         chan *logger.Message
    65  	lock             sync.RWMutex
    66  	closed           bool
    67  	sequenceToken    *string
    68  }
    69  
    70  type api interface {
    71  	CreateLogGroup(*cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error)
    72  	CreateLogStream(*cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error)
    73  	PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error)
    74  }
    75  
    76  type regionFinder interface {
    77  	Region() (string, error)
    78  }
    79  
    80  type wrappedEvent struct {
    81  	inputLogEvent *cloudwatchlogs.InputLogEvent
    82  	insertOrder   int
    83  }
    84  type byTimestamp []wrappedEvent
    85  
    86  // init registers the awslogs driver
    87  func init() {
    88  	if err := logger.RegisterLogDriver(name, New); err != nil {
    89  		logrus.Fatal(err)
    90  	}
    91  	if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil {
    92  		logrus.Fatal(err)
    93  	}
    94  }
    95  
    96  // New creates an awslogs logger using the configuration passed in on the
    97  // context.  Supported context configuration variables are awslogs-region,
    98  // awslogs-group, awslogs-stream, awslogs-create-group, awslogs-multiline-pattern
    99  // and awslogs-datetime-format.  When available, configuration is
   100  // also taken from environment variables AWS_REGION, AWS_ACCESS_KEY_ID,
   101  // AWS_SECRET_ACCESS_KEY, the shared credentials file (~/.aws/credentials), and
   102  // the EC2 Instance Metadata Service.
   103  func New(info logger.Info) (logger.Logger, error) {
   104  	logGroupName := info.Config[logGroupKey]
   105  	logStreamName, err := loggerutils.ParseLogTag(info, "{{.FullID}}")
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	logCreateGroup := false
   110  	if info.Config[logCreateGroupKey] != "" {
   111  		logCreateGroup, err = strconv.ParseBool(info.Config[logCreateGroupKey])
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  
   117  	if info.Config[logStreamKey] != "" {
   118  		logStreamName = info.Config[logStreamKey]
   119  	}
   120  
   121  	multilinePattern, err := parseMultilineOptions(info)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	client, err := newAWSLogsClient(info)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	containerStream := &logStream{
   131  		logStreamName:    logStreamName,
   132  		logGroupName:     logGroupName,
   133  		logCreateGroup:   logCreateGroup,
   134  		multilinePattern: multilinePattern,
   135  		client:           client,
   136  		messages:         make(chan *logger.Message, 4096),
   137  	}
   138  	err = containerStream.create()
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	go containerStream.collectBatch()
   143  
   144  	return containerStream, nil
   145  }
   146  
   147  // Parses awslogs-multiline-pattern and awslogs-datetime-format options
   148  // If awslogs-datetime-format is present, convert the format from strftime
   149  // to regexp and return.
   150  // If awslogs-multiline-pattern is present, compile regexp and return
   151  func parseMultilineOptions(info logger.Info) (*regexp.Regexp, error) {
   152  	dateTimeFormat := info.Config[datetimeFormatKey]
   153  	multilinePatternKey := info.Config[multilinePatternKey]
   154  	// strftime input is parsed into a regular expression
   155  	if dateTimeFormat != "" {
   156  		// %. matches each strftime format sequence and ReplaceAllStringFunc
   157  		// looks up each format sequence in the conversion table strftimeToRegex
   158  		// to replace with a defined regular expression
   159  		r := regexp.MustCompile("%.")
   160  		multilinePatternKey = r.ReplaceAllStringFunc(dateTimeFormat, func(s string) string {
   161  			return strftimeToRegex[s]
   162  		})
   163  	}
   164  	if multilinePatternKey != "" {
   165  		multilinePattern, err := regexp.Compile(multilinePatternKey)
   166  		if err != nil {
   167  			return nil, errors.Wrapf(err, "awslogs could not parse multiline pattern key %q", multilinePatternKey)
   168  		}
   169  		return multilinePattern, nil
   170  	}
   171  	return nil, nil
   172  }
   173  
   174  // Maps strftime format strings to regex
   175  var strftimeToRegex = map[string]string{
   176  	/*weekdayShort          */ `%a`: `(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)`,
   177  	/*weekdayFull           */ `%A`: `(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)`,
   178  	/*weekdayZeroIndex      */ `%w`: `[0-6]`,
   179  	/*dayZeroPadded         */ `%d`: `(?:0[1-9]|[1,2][0-9]|3[0,1])`,
   180  	/*monthShort            */ `%b`: `(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)`,
   181  	/*monthFull             */ `%B`: `(?:January|February|March|April|May|June|July|August|September|October|November|December)`,
   182  	/*monthZeroPadded       */ `%m`: `(?:0[1-9]|1[0-2])`,
   183  	/*yearCentury           */ `%Y`: `\d{4}`,
   184  	/*yearZeroPadded        */ `%y`: `\d{2}`,
   185  	/*hour24ZeroPadded      */ `%H`: `(?:[0,1][0-9]|2[0-3])`,
   186  	/*hour12ZeroPadded      */ `%I`: `(?:0[0-9]|1[0-2])`,
   187  	/*AM or PM              */ `%p`: "[A,P]M",
   188  	/*minuteZeroPadded      */ `%M`: `[0-5][0-9]`,
   189  	/*secondZeroPadded      */ `%S`: `[0-5][0-9]`,
   190  	/*microsecondZeroPadded */ `%f`: `\d{6}`,
   191  	/*utcOffset             */ `%z`: `[+-]\d{4}`,
   192  	/*tzName                */ `%Z`: `[A-Z]{1,4}T`,
   193  	/*dayOfYearZeroPadded   */ `%j`: `(?:0[0-9][1-9]|[1,2][0-9][0-9]|3[0-5][0-9]|36[0-6])`,
   194  	/*milliseconds          */ `%L`: `\.\d{3}`,
   195  }
   196  
   197  func parseLogGroup(info logger.Info, groupTemplate string) (string, error) {
   198  	tmpl, err := templates.NewParse("log-group", groupTemplate)
   199  	if err != nil {
   200  		return "", err
   201  	}
   202  	buf := new(bytes.Buffer)
   203  	if err := tmpl.Execute(buf, &info); err != nil {
   204  		return "", err
   205  	}
   206  
   207  	return buf.String(), nil
   208  }
   209  
   210  // newRegionFinder is a variable such that the implementation
   211  // can be swapped out for unit tests.
   212  var newRegionFinder = func() regionFinder {
   213  	return ec2metadata.New(session.New())
   214  }
   215  
   216  // newAWSLogsClient creates the service client for Amazon CloudWatch Logs.
   217  // Customizations to the default client from the SDK include a Docker-specific
   218  // User-Agent string and automatic region detection using the EC2 Instance
   219  // Metadata Service when region is otherwise unspecified.
   220  func newAWSLogsClient(info logger.Info) (api, error) {
   221  	var region *string
   222  	if os.Getenv(regionEnvKey) != "" {
   223  		region = aws.String(os.Getenv(regionEnvKey))
   224  	}
   225  	if info.Config[regionKey] != "" {
   226  		region = aws.String(info.Config[regionKey])
   227  	}
   228  	if region == nil || *region == "" {
   229  		logrus.Info("Trying to get region from EC2 Metadata")
   230  		ec2MetadataClient := newRegionFinder()
   231  		r, err := ec2MetadataClient.Region()
   232  		if err != nil {
   233  			logrus.WithFields(logrus.Fields{
   234  				"error": err,
   235  			}).Error("Could not get region from EC2 metadata, environment, or log option")
   236  			return nil, errors.New("Cannot determine region for awslogs driver")
   237  		}
   238  		region = &r
   239  	}
   240  	logrus.WithFields(logrus.Fields{
   241  		"region": *region,
   242  	}).Debug("Created awslogs client")
   243  
   244  	client := cloudwatchlogs.New(session.New(), aws.NewConfig().WithRegion(*region))
   245  
   246  	client.Handlers.Build.PushBackNamed(request.NamedHandler{
   247  		Name: "DockerUserAgentHandler",
   248  		Fn: func(r *request.Request) {
   249  			currentAgent := r.HTTPRequest.Header.Get(userAgentHeader)
   250  			r.HTTPRequest.Header.Set(userAgentHeader,
   251  				fmt.Sprintf("Docker %s (%s) %s",
   252  					dockerversion.Version, runtime.GOOS, currentAgent))
   253  		},
   254  	})
   255  	return client, nil
   256  }
   257  
   258  // Name returns the name of the awslogs logging driver
   259  func (l *logStream) Name() string {
   260  	return name
   261  }
   262  
   263  // Log submits messages for logging by an instance of the awslogs logging driver
   264  func (l *logStream) Log(msg *logger.Message) error {
   265  	l.lock.RLock()
   266  	defer l.lock.RUnlock()
   267  	if !l.closed {
   268  		l.messages <- msg
   269  	}
   270  	return nil
   271  }
   272  
   273  // Close closes the instance of the awslogs logging driver
   274  func (l *logStream) Close() error {
   275  	l.lock.Lock()
   276  	defer l.lock.Unlock()
   277  	if !l.closed {
   278  		close(l.messages)
   279  	}
   280  	l.closed = true
   281  	return nil
   282  }
   283  
   284  // create creates log group and log stream for the instance of the awslogs logging driver
   285  func (l *logStream) create() error {
   286  	if err := l.createLogStream(); err != nil {
   287  		if l.logCreateGroup {
   288  			if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == resourceNotFoundCode {
   289  				if err := l.createLogGroup(); err != nil {
   290  					return err
   291  				}
   292  				return l.createLogStream()
   293  			}
   294  		}
   295  		return err
   296  	}
   297  
   298  	return nil
   299  }
   300  
   301  // createLogGroup creates a log group for the instance of the awslogs logging driver
   302  func (l *logStream) createLogGroup() error {
   303  	if _, err := l.client.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{
   304  		LogGroupName: aws.String(l.logGroupName),
   305  	}); err != nil {
   306  		if awsErr, ok := err.(awserr.Error); ok {
   307  			fields := logrus.Fields{
   308  				"errorCode":      awsErr.Code(),
   309  				"message":        awsErr.Message(),
   310  				"origError":      awsErr.OrigErr(),
   311  				"logGroupName":   l.logGroupName,
   312  				"logCreateGroup": l.logCreateGroup,
   313  			}
   314  			if awsErr.Code() == resourceAlreadyExistsCode {
   315  				// Allow creation to succeed
   316  				logrus.WithFields(fields).Info("Log group already exists")
   317  				return nil
   318  			}
   319  			logrus.WithFields(fields).Error("Failed to create log group")
   320  		}
   321  		return err
   322  	}
   323  	return nil
   324  }
   325  
   326  // createLogStream creates a log stream for the instance of the awslogs logging driver
   327  func (l *logStream) createLogStream() error {
   328  	input := &cloudwatchlogs.CreateLogStreamInput{
   329  		LogGroupName:  aws.String(l.logGroupName),
   330  		LogStreamName: aws.String(l.logStreamName),
   331  	}
   332  
   333  	_, err := l.client.CreateLogStream(input)
   334  
   335  	if err != nil {
   336  		if awsErr, ok := err.(awserr.Error); ok {
   337  			fields := logrus.Fields{
   338  				"errorCode":     awsErr.Code(),
   339  				"message":       awsErr.Message(),
   340  				"origError":     awsErr.OrigErr(),
   341  				"logGroupName":  l.logGroupName,
   342  				"logStreamName": l.logStreamName,
   343  			}
   344  			if awsErr.Code() == resourceAlreadyExistsCode {
   345  				// Allow creation to succeed
   346  				logrus.WithFields(fields).Info("Log stream already exists")
   347  				return nil
   348  			}
   349  			logrus.WithFields(fields).Error("Failed to create log stream")
   350  		}
   351  	}
   352  	return err
   353  }
   354  
   355  // newTicker is used for time-based batching.  newTicker is a variable such
   356  // that the implementation can be swapped out for unit tests.
   357  var newTicker = func(freq time.Duration) *time.Ticker {
   358  	return time.NewTicker(freq)
   359  }
   360  
   361  // collectBatch executes as a goroutine to perform batching of log events for
   362  // submission to the log stream.  If the awslogs-multiline-pattern or
   363  // awslogs-datetime-format options have been configured, multiline processing
   364  // is enabled, where log messages are stored in an event buffer until a multiline
   365  // pattern match is found, at which point the messages in the event buffer are
   366  // pushed to CloudWatch logs as a single log event.  Multline messages are processed
   367  // according to the maximumBytesPerPut constraint, and the implementation only
   368  // allows for messages to be buffered for a maximum of 2*batchPublishFrequency
   369  // seconds.  When events are ready to be processed for submission to CloudWatch
   370  // Logs, the processEvents method is called.  If a multiline pattern is not
   371  // configured, log events are submitted to the processEvents method immediately.
   372  func (l *logStream) collectBatch() {
   373  	timer := newTicker(batchPublishFrequency)
   374  	var events []wrappedEvent
   375  	var eventBuffer []byte
   376  	var eventBufferTimestamp int64
   377  	for {
   378  		select {
   379  		case t := <-timer.C:
   380  			// If event buffer is older than batch publish frequency flush the event buffer
   381  			if eventBufferTimestamp > 0 && len(eventBuffer) > 0 {
   382  				eventBufferAge := t.UnixNano()/int64(time.Millisecond) - eventBufferTimestamp
   383  				eventBufferExpired := eventBufferAge > int64(batchPublishFrequency)/int64(time.Millisecond)
   384  				eventBufferNegative := eventBufferAge < 0
   385  				if eventBufferExpired || eventBufferNegative {
   386  					events = l.processEvent(events, eventBuffer, eventBufferTimestamp)
   387  				}
   388  			}
   389  			l.publishBatch(events)
   390  			events = events[:0]
   391  		case msg, more := <-l.messages:
   392  			if !more {
   393  				// Flush event buffer
   394  				events = l.processEvent(events, eventBuffer, eventBufferTimestamp)
   395  				l.publishBatch(events)
   396  				return
   397  			}
   398  			if eventBufferTimestamp == 0 {
   399  				eventBufferTimestamp = msg.Timestamp.UnixNano() / int64(time.Millisecond)
   400  			}
   401  			unprocessedLine := msg.Line
   402  			if l.multilinePattern != nil {
   403  				if l.multilinePattern.Match(unprocessedLine) {
   404  					// This is a new log event so flush the current eventBuffer to events
   405  					events = l.processEvent(events, eventBuffer, eventBufferTimestamp)
   406  					eventBufferTimestamp = msg.Timestamp.UnixNano() / int64(time.Millisecond)
   407  					eventBuffer = eventBuffer[:0]
   408  				}
   409  				// If we will exceed max bytes per event flush the current event buffer before appending
   410  				if len(eventBuffer)+len(unprocessedLine) > maximumBytesPerEvent {
   411  					events = l.processEvent(events, eventBuffer, eventBufferTimestamp)
   412  					eventBuffer = eventBuffer[:0]
   413  				}
   414  				// Append new line
   415  				processedLine := append(unprocessedLine, "\n"...)
   416  				eventBuffer = append(eventBuffer, processedLine...)
   417  				logger.PutMessage(msg)
   418  			} else {
   419  				events = l.processEvent(events, unprocessedLine, msg.Timestamp.UnixNano()/int64(time.Millisecond))
   420  				logger.PutMessage(msg)
   421  			}
   422  		}
   423  	}
   424  }
   425  
   426  // processEvent processes log events that are ready for submission to CloudWatch
   427  // logs.  Batching is performed on time- and size-bases.  Time-based batching
   428  // occurs at a 5 second interval (defined in the batchPublishFrequency const).
   429  // Size-based batching is performed on the maximum number of events per batch
   430  // (defined in maximumLogEventsPerPut) and the maximum number of total bytes in a
   431  // batch (defined in maximumBytesPerPut).  Log messages are split by the maximum
   432  // bytes per event (defined in maximumBytesPerEvent).  There is a fixed per-event
   433  // byte overhead (defined in perEventBytes) which is accounted for in split- and
   434  // batch-calculations.
   435  func (l *logStream) processEvent(events []wrappedEvent, unprocessedLine []byte, timestamp int64) []wrappedEvent {
   436  	bytes := 0
   437  	for len(unprocessedLine) > 0 {
   438  		// Split line length so it does not exceed the maximum
   439  		lineBytes := len(unprocessedLine)
   440  		if lineBytes > maximumBytesPerEvent {
   441  			lineBytes = maximumBytesPerEvent
   442  		}
   443  		line := unprocessedLine[:lineBytes]
   444  		unprocessedLine = unprocessedLine[lineBytes:]
   445  		if (len(events) >= maximumLogEventsPerPut) || (bytes+lineBytes+perEventBytes > maximumBytesPerPut) {
   446  			// Publish an existing batch if it's already over the maximum number of events or if adding this
   447  			// event would push it over the maximum number of total bytes.
   448  			l.publishBatch(events)
   449  			events = events[:0]
   450  			bytes = 0
   451  		}
   452  		events = append(events, wrappedEvent{
   453  			inputLogEvent: &cloudwatchlogs.InputLogEvent{
   454  				Message:   aws.String(string(line)),
   455  				Timestamp: aws.Int64(timestamp),
   456  			},
   457  			insertOrder: len(events),
   458  		})
   459  		bytes += (lineBytes + perEventBytes)
   460  	}
   461  	return events
   462  }
   463  
   464  // publishBatch calls PutLogEvents for a given set of InputLogEvents,
   465  // accounting for sequencing requirements (each request must reference the
   466  // sequence token returned by the previous request).
   467  func (l *logStream) publishBatch(events []wrappedEvent) {
   468  	if len(events) == 0 {
   469  		return
   470  	}
   471  
   472  	// events in a batch must be sorted by timestamp
   473  	// see http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
   474  	sort.Sort(byTimestamp(events))
   475  	cwEvents := unwrapEvents(events)
   476  
   477  	nextSequenceToken, err := l.putLogEvents(cwEvents, l.sequenceToken)
   478  
   479  	if err != nil {
   480  		if awsErr, ok := err.(awserr.Error); ok {
   481  			if awsErr.Code() == dataAlreadyAcceptedCode {
   482  				// already submitted, just grab the correct sequence token
   483  				parts := strings.Split(awsErr.Message(), " ")
   484  				nextSequenceToken = &parts[len(parts)-1]
   485  				logrus.WithFields(logrus.Fields{
   486  					"errorCode":     awsErr.Code(),
   487  					"message":       awsErr.Message(),
   488  					"logGroupName":  l.logGroupName,
   489  					"logStreamName": l.logStreamName,
   490  				}).Info("Data already accepted, ignoring error")
   491  				err = nil
   492  			} else if awsErr.Code() == invalidSequenceTokenCode {
   493  				// sequence code is bad, grab the correct one and retry
   494  				parts := strings.Split(awsErr.Message(), " ")
   495  				token := parts[len(parts)-1]
   496  				nextSequenceToken, err = l.putLogEvents(cwEvents, &token)
   497  			}
   498  		}
   499  	}
   500  	if err != nil {
   501  		logrus.Error(err)
   502  	} else {
   503  		l.sequenceToken = nextSequenceToken
   504  	}
   505  }
   506  
   507  // putLogEvents wraps the PutLogEvents API
   508  func (l *logStream) putLogEvents(events []*cloudwatchlogs.InputLogEvent, sequenceToken *string) (*string, error) {
   509  	input := &cloudwatchlogs.PutLogEventsInput{
   510  		LogEvents:     events,
   511  		SequenceToken: sequenceToken,
   512  		LogGroupName:  aws.String(l.logGroupName),
   513  		LogStreamName: aws.String(l.logStreamName),
   514  	}
   515  	resp, err := l.client.PutLogEvents(input)
   516  	if err != nil {
   517  		if awsErr, ok := err.(awserr.Error); ok {
   518  			logrus.WithFields(logrus.Fields{
   519  				"errorCode":     awsErr.Code(),
   520  				"message":       awsErr.Message(),
   521  				"origError":     awsErr.OrigErr(),
   522  				"logGroupName":  l.logGroupName,
   523  				"logStreamName": l.logStreamName,
   524  			}).Error("Failed to put log events")
   525  		}
   526  		return nil, err
   527  	}
   528  	return resp.NextSequenceToken, nil
   529  }
   530  
   531  // ValidateLogOpt looks for awslogs-specific log options awslogs-region,
   532  // awslogs-group, awslogs-stream, awslogs-create-group, awslogs-datetime-format,
   533  // awslogs-multiline-pattern
   534  func ValidateLogOpt(cfg map[string]string) error {
   535  	for key := range cfg {
   536  		switch key {
   537  		case logGroupKey:
   538  		case logStreamKey:
   539  		case logCreateGroupKey:
   540  		case regionKey:
   541  		case tagKey:
   542  		case datetimeFormatKey:
   543  		case multilinePatternKey:
   544  		default:
   545  			return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name)
   546  		}
   547  	}
   548  	if cfg[logGroupKey] == "" {
   549  		return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey)
   550  	}
   551  	if cfg[logCreateGroupKey] != "" {
   552  		if _, err := strconv.ParseBool(cfg[logCreateGroupKey]); err != nil {
   553  			return fmt.Errorf("must specify valid value for log opt '%s': %v", logCreateGroupKey, err)
   554  		}
   555  	}
   556  	_, datetimeFormatKeyExists := cfg[datetimeFormatKey]
   557  	_, multilinePatternKeyExists := cfg[multilinePatternKey]
   558  	if datetimeFormatKeyExists && multilinePatternKeyExists {
   559  		return fmt.Errorf("you cannot configure log opt '%s' and '%s' at the same time", datetimeFormatKey, multilinePatternKey)
   560  	}
   561  	return nil
   562  }
   563  
   564  // Len returns the length of a byTimestamp slice.  Len is required by the
   565  // sort.Interface interface.
   566  func (slice byTimestamp) Len() int {
   567  	return len(slice)
   568  }
   569  
   570  // Less compares two values in a byTimestamp slice by Timestamp.  Less is
   571  // required by the sort.Interface interface.
   572  func (slice byTimestamp) Less(i, j int) bool {
   573  	iTimestamp, jTimestamp := int64(0), int64(0)
   574  	if slice != nil && slice[i].inputLogEvent.Timestamp != nil {
   575  		iTimestamp = *slice[i].inputLogEvent.Timestamp
   576  	}
   577  	if slice != nil && slice[j].inputLogEvent.Timestamp != nil {
   578  		jTimestamp = *slice[j].inputLogEvent.Timestamp
   579  	}
   580  	if iTimestamp == jTimestamp {
   581  		return slice[i].insertOrder < slice[j].insertOrder
   582  	}
   583  	return iTimestamp < jTimestamp
   584  }
   585  
   586  // Swap swaps two values in a byTimestamp slice with each other.  Swap is
   587  // required by the sort.Interface interface.
   588  func (slice byTimestamp) Swap(i, j int) {
   589  	slice[i], slice[j] = slice[j], slice[i]
   590  }
   591  
   592  func unwrapEvents(events []wrappedEvent) []*cloudwatchlogs.InputLogEvent {
   593  	cwEvents := make([]*cloudwatchlogs.InputLogEvent, len(events))
   594  	for i, input := range events {
   595  		cwEvents[i] = input.inputLogEvent
   596  	}
   597  	return cwEvents
   598  }