github.com/moby/docker@v26.1.3+incompatible/daemon/logger/awslogs/cloudwatchlogs.go (about)

     1  // Package awslogs provides the logdriver for forwarding container logs to Amazon CloudWatch Logs
     2  package awslogs // import "github.com/docker/docker/daemon/logger/awslogs"
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"regexp"
     9  	"sort"
    10  	"strconv"
    11  	"sync"
    12  	"time"
    13  	"unicode/utf8"
    14  
    15  	"github.com/aws/aws-sdk-go-v2/aws"
    16  	"github.com/aws/aws-sdk-go-v2/aws/middleware"
    17  	"github.com/aws/aws-sdk-go-v2/config"
    18  	"github.com/aws/aws-sdk-go-v2/credentials/endpointcreds"
    19  	"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
    20  	"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
    21  	"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
    22  	"github.com/aws/smithy-go"
    23  	smithymiddleware "github.com/aws/smithy-go/middleware"
    24  	smithyhttp "github.com/aws/smithy-go/transport/http"
    25  	"github.com/containerd/log"
    26  	"github.com/docker/docker/daemon/logger"
    27  	"github.com/docker/docker/daemon/logger/loggerutils"
    28  	"github.com/docker/docker/dockerversion"
    29  	"github.com/pkg/errors"
    30  )
    31  
    32  const (
    33  	name                   = "awslogs"
    34  	regionKey              = "awslogs-region"
    35  	endpointKey            = "awslogs-endpoint"
    36  	regionEnvKey           = "AWS_REGION"
    37  	logGroupKey            = "awslogs-group"
    38  	logStreamKey           = "awslogs-stream"
    39  	logCreateGroupKey      = "awslogs-create-group"
    40  	logCreateStreamKey     = "awslogs-create-stream"
    41  	tagKey                 = "tag"
    42  	datetimeFormatKey      = "awslogs-datetime-format"
    43  	multilinePatternKey    = "awslogs-multiline-pattern"
    44  	credentialsEndpointKey = "awslogs-credentials-endpoint" //nolint:gosec // G101: Potential hardcoded credentials
    45  	forceFlushIntervalKey  = "awslogs-force-flush-interval-seconds"
    46  	maxBufferedEventsKey   = "awslogs-max-buffered-events"
    47  	logFormatKey           = "awslogs-format"
    48  
    49  	defaultForceFlushInterval = 5 * time.Second
    50  	defaultMaxBufferedEvents  = 4096
    51  
    52  	// See: http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
    53  	perEventBytes          = 26
    54  	maximumBytesPerPut     = 1048576
    55  	maximumLogEventsPerPut = 10000
    56  
    57  	// See: http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_limits.html
    58  	// Because the events are interpreted as UTF-8 encoded Unicode, invalid UTF-8 byte sequences are replaced with the
    59  	// Unicode replacement character (U+FFFD), which is a 3-byte sequence in UTF-8.  To compensate for that and to avoid
    60  	// splitting valid UTF-8 characters into invalid byte sequences, we calculate the length of each event assuming that
    61  	// this replacement happens.
    62  	maximumBytesPerEvent = 262144 - perEventBytes
    63  
    64  	credentialsEndpoint = "http://169.254.170.2" //nolint:gosec // G101: Potential hardcoded credentials
    65  
    66  	// See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
    67  	logsFormatHeader = "x-amzn-logs-format"
    68  	jsonEmfLogFormat = "json/emf"
    69  )
    70  
    71  type logStream struct {
    72  	logStreamName      string
    73  	logGroupName       string
    74  	logCreateGroup     bool
    75  	logCreateStream    bool
    76  	forceFlushInterval time.Duration
    77  	multilinePattern   *regexp.Regexp
    78  	client             api
    79  	messages           chan *logger.Message
    80  	lock               sync.RWMutex
    81  	closed             bool
    82  	sequenceToken      *string
    83  }
    84  
    85  type logStreamConfig struct {
    86  	logStreamName      string
    87  	logGroupName       string
    88  	logCreateGroup     bool
    89  	logCreateStream    bool
    90  	forceFlushInterval time.Duration
    91  	maxBufferedEvents  int
    92  	multilinePattern   *regexp.Regexp
    93  }
    94  
    95  var _ logger.SizedLogger = &logStream{}
    96  
    97  type api interface {
    98  	CreateLogGroup(context.Context, *cloudwatchlogs.CreateLogGroupInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogGroupOutput, error)
    99  	CreateLogStream(context.Context, *cloudwatchlogs.CreateLogStreamInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.CreateLogStreamOutput, error)
   100  	PutLogEvents(context.Context, *cloudwatchlogs.PutLogEventsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.PutLogEventsOutput, error)
   101  }
   102  
   103  type regionFinder interface {
   104  	GetRegion(context.Context, *imds.GetRegionInput, ...func(*imds.Options)) (*imds.GetRegionOutput, error)
   105  }
   106  
   107  type wrappedEvent struct {
   108  	inputLogEvent types.InputLogEvent
   109  	insertOrder   int
   110  }
   111  type byTimestamp []wrappedEvent
   112  
   113  // init registers the awslogs driver
   114  func init() {
   115  	if err := logger.RegisterLogDriver(name, New); err != nil {
   116  		panic(err)
   117  	}
   118  	if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil {
   119  		panic(err)
   120  	}
   121  }
   122  
   123  // eventBatch holds the events that are batched for submission and the
   124  // associated data about it.
   125  //
   126  // Warning: this type is not threadsafe and must not be used
   127  // concurrently. This type is expected to be consumed in a single go
   128  // routine and never concurrently.
   129  type eventBatch struct {
   130  	batch []wrappedEvent
   131  	bytes int
   132  }
   133  
   134  // New creates an awslogs logger using the configuration passed in on the
   135  // context.  Supported context configuration variables are awslogs-region,
   136  // awslogs-endpoint, awslogs-group, awslogs-stream, awslogs-create-group,
   137  // awslogs-multiline-pattern and awslogs-datetime-format.
   138  // When available, configuration is also taken from environment variables
   139  // AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, the shared credentials
   140  // file (~/.aws/credentials), and the EC2 Instance Metadata Service.
   141  func New(info logger.Info) (logger.Logger, error) {
   142  	containerStreamConfig, err := newStreamConfig(info)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	client, err := newAWSLogsClient(info)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	logNonBlocking := info.Config["mode"] == "non-blocking"
   152  
   153  	containerStream := &logStream{
   154  		logStreamName:      containerStreamConfig.logStreamName,
   155  		logGroupName:       containerStreamConfig.logGroupName,
   156  		logCreateGroup:     containerStreamConfig.logCreateGroup,
   157  		logCreateStream:    containerStreamConfig.logCreateStream,
   158  		forceFlushInterval: containerStreamConfig.forceFlushInterval,
   159  		multilinePattern:   containerStreamConfig.multilinePattern,
   160  		client:             client,
   161  		messages:           make(chan *logger.Message, containerStreamConfig.maxBufferedEvents),
   162  	}
   163  
   164  	creationDone := make(chan bool)
   165  	if logNonBlocking {
   166  		go func() {
   167  			backoff := 1
   168  			maxBackoff := 32
   169  			for {
   170  				// If logger is closed we are done
   171  				containerStream.lock.RLock()
   172  				if containerStream.closed {
   173  					containerStream.lock.RUnlock()
   174  					break
   175  				}
   176  				containerStream.lock.RUnlock()
   177  				err := containerStream.create()
   178  				if err == nil {
   179  					break
   180  				}
   181  
   182  				time.Sleep(time.Duration(backoff) * time.Second)
   183  				if backoff < maxBackoff {
   184  					backoff *= 2
   185  				}
   186  				log.G(context.TODO()).
   187  					WithError(err).
   188  					WithField("container-id", info.ContainerID).
   189  					WithField("container-name", info.ContainerName).
   190  					Error("Error while trying to initialize awslogs. Retrying in: ", backoff, " seconds")
   191  			}
   192  			close(creationDone)
   193  		}()
   194  	} else {
   195  		if err = containerStream.create(); err != nil {
   196  			return nil, err
   197  		}
   198  		close(creationDone)
   199  	}
   200  	go containerStream.collectBatch(creationDone)
   201  
   202  	return containerStream, nil
   203  }
   204  
   205  // Parses most of the awslogs- options and prepares a config object to be used for newing the actual stream
   206  // It has been formed out to ease Utest of the New above
   207  func newStreamConfig(info logger.Info) (*logStreamConfig, error) {
   208  	logGroupName := info.Config[logGroupKey]
   209  	logStreamName, err := loggerutils.ParseLogTag(info, "{{.FullID}}")
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	logCreateGroup := false
   214  	if info.Config[logCreateGroupKey] != "" {
   215  		logCreateGroup, err = strconv.ParseBool(info.Config[logCreateGroupKey])
   216  		if err != nil {
   217  			return nil, err
   218  		}
   219  	}
   220  
   221  	forceFlushInterval := defaultForceFlushInterval
   222  	if info.Config[forceFlushIntervalKey] != "" {
   223  		forceFlushIntervalAsInt, err := strconv.Atoi(info.Config[forceFlushIntervalKey])
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  		forceFlushInterval = time.Duration(forceFlushIntervalAsInt) * time.Second
   228  	}
   229  
   230  	maxBufferedEvents := int(defaultMaxBufferedEvents)
   231  	if info.Config[maxBufferedEventsKey] != "" {
   232  		maxBufferedEvents, err = strconv.Atoi(info.Config[maxBufferedEventsKey])
   233  		if err != nil {
   234  			return nil, err
   235  		}
   236  	}
   237  
   238  	if info.Config[logStreamKey] != "" {
   239  		logStreamName = info.Config[logStreamKey]
   240  	}
   241  	logCreateStream := true
   242  	if info.Config[logCreateStreamKey] != "" {
   243  		logCreateStream, err = strconv.ParseBool(info.Config[logCreateStreamKey])
   244  		if err != nil {
   245  			return nil, err
   246  		}
   247  	}
   248  
   249  	multilinePattern, err := parseMultilineOptions(info)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	containerStreamConfig := &logStreamConfig{
   255  		logStreamName:      logStreamName,
   256  		logGroupName:       logGroupName,
   257  		logCreateGroup:     logCreateGroup,
   258  		logCreateStream:    logCreateStream,
   259  		forceFlushInterval: forceFlushInterval,
   260  		maxBufferedEvents:  maxBufferedEvents,
   261  		multilinePattern:   multilinePattern,
   262  	}
   263  
   264  	return containerStreamConfig, nil
   265  }
   266  
   267  // Parses awslogs-multiline-pattern and awslogs-datetime-format options
   268  // If awslogs-datetime-format is present, convert the format from strftime
   269  // to regexp and return.
   270  // If awslogs-multiline-pattern is present, compile regexp and return
   271  func parseMultilineOptions(info logger.Info) (*regexp.Regexp, error) {
   272  	dateTimeFormat := info.Config[datetimeFormatKey]
   273  	multilinePatternKey := info.Config[multilinePatternKey]
   274  	// strftime input is parsed into a regular expression
   275  	if dateTimeFormat != "" {
   276  		// %. matches each strftime format sequence and ReplaceAllStringFunc
   277  		// looks up each format sequence in the conversion table strftimeToRegex
   278  		// to replace with a defined regular expression
   279  		r := regexp.MustCompile("%.")
   280  		multilinePatternKey = r.ReplaceAllStringFunc(dateTimeFormat, func(s string) string {
   281  			return strftimeToRegex[s]
   282  		})
   283  	}
   284  	if multilinePatternKey != "" {
   285  		multilinePattern, err := regexp.Compile(multilinePatternKey)
   286  		if err != nil {
   287  			return nil, errors.Wrapf(err, "awslogs could not parse multiline pattern key %q", multilinePatternKey)
   288  		}
   289  		return multilinePattern, nil
   290  	}
   291  	return nil, nil
   292  }
   293  
   294  // Maps strftime format strings to regex
   295  var strftimeToRegex = map[string]string{
   296  	/*weekdayShort          */ `%a`: `(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)`,
   297  	/*weekdayFull           */ `%A`: `(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)`,
   298  	/*weekdayZeroIndex      */ `%w`: `[0-6]`,
   299  	/*dayZeroPadded         */ `%d`: `(?:0[1-9]|[1,2][0-9]|3[0,1])`,
   300  	/*monthShort            */ `%b`: `(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)`,
   301  	/*monthFull             */ `%B`: `(?:January|February|March|April|May|June|July|August|September|October|November|December)`,
   302  	/*monthZeroPadded       */ `%m`: `(?:0[1-9]|1[0-2])`,
   303  	/*yearCentury           */ `%Y`: `\d{4}`,
   304  	/*yearZeroPadded        */ `%y`: `\d{2}`,
   305  	/*hour24ZeroPadded      */ `%H`: `(?:[0,1][0-9]|2[0-3])`,
   306  	/*hour12ZeroPadded      */ `%I`: `(?:0[0-9]|1[0-2])`,
   307  	/*AM or PM              */ `%p`: "[A,P]M",
   308  	/*minuteZeroPadded      */ `%M`: `[0-5][0-9]`,
   309  	/*secondZeroPadded      */ `%S`: `[0-5][0-9]`,
   310  	/*microsecondZeroPadded */ `%f`: `\d{6}`,
   311  	/*utcOffset             */ `%z`: `[+-]\d{4}`,
   312  	/*tzName                */ `%Z`: `[A-Z]{1,4}T`,
   313  	/*dayOfYearZeroPadded   */ `%j`: `(?:0[0-9][1-9]|[1,2][0-9][0-9]|3[0-5][0-9]|36[0-6])`,
   314  	/*milliseconds          */ `%L`: `\.\d{3}`,
   315  }
   316  
   317  // newRegionFinder is a variable such that the implementation
   318  // can be swapped out for unit tests.
   319  var newRegionFinder = func(ctx context.Context) (regionFinder, error) {
   320  	cfg, err := config.LoadDefaultConfig(ctx) // default config, because we don't yet know the region
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  
   325  	client := imds.NewFromConfig(cfg)
   326  	return client, nil
   327  }
   328  
   329  // newSDKEndpoint is a variable such that the implementation
   330  // can be swapped out for unit tests.
   331  var newSDKEndpoint = credentialsEndpoint
   332  
   333  // newAWSLogsClient creates the service client for Amazon CloudWatch Logs.
   334  // Customizations to the default client from the SDK include a Docker-specific
   335  // User-Agent string and automatic region detection using the EC2 Instance
   336  // Metadata Service when region is otherwise unspecified.
   337  func newAWSLogsClient(info logger.Info, configOpts ...func(*config.LoadOptions) error) (*cloudwatchlogs.Client, error) {
   338  	ctx := context.TODO()
   339  	var region, endpoint *string
   340  	if os.Getenv(regionEnvKey) != "" {
   341  		region = aws.String(os.Getenv(regionEnvKey))
   342  	}
   343  	if info.Config[regionKey] != "" {
   344  		region = aws.String(info.Config[regionKey])
   345  	}
   346  	if info.Config[endpointKey] != "" {
   347  		endpoint = aws.String(info.Config[endpointKey])
   348  	}
   349  	if region == nil || *region == "" {
   350  		log.G(ctx).Info("Trying to get region from IMDS")
   351  		regFinder, err := newRegionFinder(context.TODO())
   352  		if err != nil {
   353  			log.G(ctx).WithError(err).Error("could not create regionFinder")
   354  			return nil, errors.Wrap(err, "could not create regionFinder")
   355  		}
   356  
   357  		r, err := regFinder.GetRegion(context.TODO(), &imds.GetRegionInput{})
   358  		if err != nil {
   359  			log.G(ctx).WithError(err).Error("Could not get region from IMDS, environment, or log option")
   360  			return nil, errors.Wrap(err, "cannot determine region for awslogs driver")
   361  		}
   362  		region = &r.Region
   363  	}
   364  
   365  	configOpts = append(configOpts, config.WithRegion(*region))
   366  
   367  	if uri, ok := info.Config[credentialsEndpointKey]; ok {
   368  		log.G(ctx).Debugf("Trying to get credentials from awslogs-credentials-endpoint")
   369  
   370  		endpoint := fmt.Sprintf("%s%s", newSDKEndpoint, uri)
   371  		configOpts = append(configOpts, config.WithCredentialsProvider(endpointcreds.New(endpoint)))
   372  	}
   373  
   374  	cfg, err := config.LoadDefaultConfig(context.TODO(), configOpts...)
   375  	if err != nil {
   376  		log.G(ctx).WithError(err).Error("Could not initialize AWS SDK config")
   377  		return nil, errors.Wrap(err, "could not initialize AWS SDK config")
   378  	}
   379  
   380  	log.G(ctx).WithFields(log.Fields{
   381  		"region": *region,
   382  	}).Debug("Created awslogs client")
   383  
   384  	var clientOpts []func(*cloudwatchlogs.Options)
   385  
   386  	if info.Config[logFormatKey] != "" {
   387  		logFormatMiddleware := smithymiddleware.BuildMiddlewareFunc("logFormat", func(
   388  			ctx context.Context, in smithymiddleware.BuildInput, next smithymiddleware.BuildHandler,
   389  		) (
   390  			out smithymiddleware.BuildOutput, metadata smithymiddleware.Metadata, err error,
   391  		) {
   392  			switch v := in.Request.(type) {
   393  			case *smithyhttp.Request:
   394  				v.Header.Add(logsFormatHeader, jsonEmfLogFormat)
   395  			}
   396  			return next.HandleBuild(ctx, in)
   397  		})
   398  		clientOpts = append(
   399  			clientOpts,
   400  			cloudwatchlogs.WithAPIOptions(func(stack *smithymiddleware.Stack) error {
   401  				return stack.Build.Add(logFormatMiddleware, smithymiddleware.Before)
   402  			}),
   403  		)
   404  	}
   405  
   406  	clientOpts = append(
   407  		clientOpts,
   408  		cloudwatchlogs.WithAPIOptions(middleware.AddUserAgentKeyValue("Docker", dockerversion.Version)),
   409  		func(o *cloudwatchlogs.Options) {
   410  			o.BaseEndpoint = endpoint
   411  		},
   412  	)
   413  
   414  	client := cloudwatchlogs.NewFromConfig(cfg, clientOpts...)
   415  
   416  	return client, nil
   417  }
   418  
   419  // Name returns the name of the awslogs logging driver
   420  func (l *logStream) Name() string {
   421  	return name
   422  }
   423  
   424  // BufSize returns the maximum bytes CloudWatch can handle.
   425  func (l *logStream) BufSize() int {
   426  	return maximumBytesPerEvent
   427  }
   428  
   429  // Log submits messages for logging by an instance of the awslogs logging driver
   430  func (l *logStream) Log(msg *logger.Message) error {
   431  	l.lock.RLock()
   432  	defer l.lock.RUnlock()
   433  	if l.closed {
   434  		return errors.New("awslogs is closed")
   435  	}
   436  	l.messages <- msg
   437  	return nil
   438  }
   439  
   440  // Close closes the instance of the awslogs logging driver
   441  func (l *logStream) Close() error {
   442  	l.lock.Lock()
   443  	defer l.lock.Unlock()
   444  	if !l.closed {
   445  		close(l.messages)
   446  	}
   447  	l.closed = true
   448  	return nil
   449  }
   450  
   451  // create creates log group and log stream for the instance of the awslogs logging driver
   452  func (l *logStream) create() error {
   453  	err := l.createLogStream()
   454  	if err == nil {
   455  		return nil
   456  	}
   457  
   458  	var apiErr *types.ResourceNotFoundException
   459  	if errors.As(err, &apiErr) && l.logCreateGroup {
   460  		if err := l.createLogGroup(); err != nil {
   461  			return errors.Wrap(err, "failed to create Cloudwatch log group")
   462  		}
   463  		err = l.createLogStream()
   464  		if err == nil {
   465  			return nil
   466  		}
   467  	}
   468  	return errors.Wrap(err, "failed to create Cloudwatch log stream")
   469  }
   470  
   471  // createLogGroup creates a log group for the instance of the awslogs logging driver
   472  func (l *logStream) createLogGroup() error {
   473  	if _, err := l.client.CreateLogGroup(context.TODO(), &cloudwatchlogs.CreateLogGroupInput{
   474  		LogGroupName: aws.String(l.logGroupName),
   475  	}); err != nil {
   476  		var apiErr smithy.APIError
   477  		if errors.As(err, &apiErr) {
   478  			fields := log.Fields{
   479  				"errorCode":      apiErr.ErrorCode(),
   480  				"message":        apiErr.ErrorMessage(),
   481  				"logGroupName":   l.logGroupName,
   482  				"logCreateGroup": l.logCreateGroup,
   483  			}
   484  			if _, ok := apiErr.(*types.ResourceAlreadyExistsException); ok {
   485  				// Allow creation to succeed
   486  				log.G(context.TODO()).WithFields(fields).Info("Log group already exists")
   487  				return nil
   488  			}
   489  			log.G(context.TODO()).WithFields(fields).Error("Failed to create log group")
   490  		}
   491  		return err
   492  	}
   493  	return nil
   494  }
   495  
   496  // createLogStream creates a log stream for the instance of the awslogs logging driver
   497  func (l *logStream) createLogStream() error {
   498  	// Directly return if we do not want to create log stream.
   499  	if !l.logCreateStream {
   500  		log.G(context.TODO()).WithFields(log.Fields{
   501  			"logGroupName":    l.logGroupName,
   502  			"logStreamName":   l.logStreamName,
   503  			"logCreateStream": l.logCreateStream,
   504  		}).Info("Skipping creating log stream")
   505  		return nil
   506  	}
   507  
   508  	input := &cloudwatchlogs.CreateLogStreamInput{
   509  		LogGroupName:  aws.String(l.logGroupName),
   510  		LogStreamName: aws.String(l.logStreamName),
   511  	}
   512  
   513  	_, err := l.client.CreateLogStream(context.TODO(), input)
   514  	if err != nil {
   515  		var apiErr smithy.APIError
   516  		if errors.As(err, &apiErr) {
   517  			fields := log.Fields{
   518  				"errorCode":     apiErr.ErrorCode(),
   519  				"message":       apiErr.ErrorMessage(),
   520  				"logGroupName":  l.logGroupName,
   521  				"logStreamName": l.logStreamName,
   522  			}
   523  			if _, ok := apiErr.(*types.ResourceAlreadyExistsException); ok {
   524  				// Allow creation to succeed
   525  				log.G(context.TODO()).WithFields(fields).Info("Log stream already exists")
   526  				return nil
   527  			}
   528  			log.G(context.TODO()).WithFields(fields).Error("Failed to create log stream")
   529  		}
   530  	}
   531  	return err
   532  }
   533  
   534  // newTicker is used for time-based batching.  newTicker is a variable such
   535  // that the implementation can be swapped out for unit tests.
   536  var newTicker = func(freq time.Duration) *time.Ticker {
   537  	return time.NewTicker(freq)
   538  }
   539  
   540  // collectBatch executes as a goroutine to perform batching of log events for
   541  // submission to the log stream.  If the awslogs-multiline-pattern or
   542  // awslogs-datetime-format options have been configured, multiline processing
   543  // is enabled, where log messages are stored in an event buffer until a multiline
   544  // pattern match is found, at which point the messages in the event buffer are
   545  // pushed to CloudWatch logs as a single log event.  Multiline messages are processed
   546  // according to the maximumBytesPerPut constraint, and the implementation only
   547  // allows for messages to be buffered for a maximum of 2*l.forceFlushInterval
   548  // seconds.  If no forceFlushInterval is specified for the log stream, then the default
   549  // of 5 seconds will be used resulting in a maximum of 10 seconds buffer time for multiline
   550  // messages. When events are ready to be processed for submission to CloudWatch
   551  // Logs, the processEvents method is called.  If a multiline pattern is not
   552  // configured, log events are submitted to the processEvents method immediately.
   553  func (l *logStream) collectBatch(created chan bool) {
   554  	// Wait for the logstream/group to be created
   555  	<-created
   556  	flushInterval := l.forceFlushInterval
   557  	if flushInterval <= 0 {
   558  		flushInterval = defaultForceFlushInterval
   559  	}
   560  	ticker := newTicker(flushInterval)
   561  	var eventBuffer []byte
   562  	var eventBufferTimestamp int64
   563  	batch := newEventBatch()
   564  	for {
   565  		select {
   566  		case t := <-ticker.C:
   567  			// If event buffer is older than batch publish frequency flush the event buffer
   568  			if eventBufferTimestamp > 0 && len(eventBuffer) > 0 {
   569  				eventBufferAge := t.UnixNano()/int64(time.Millisecond) - eventBufferTimestamp
   570  				eventBufferExpired := eventBufferAge >= int64(flushInterval)/int64(time.Millisecond)
   571  				eventBufferNegative := eventBufferAge < 0
   572  				if eventBufferExpired || eventBufferNegative {
   573  					l.processEvent(batch, eventBuffer, eventBufferTimestamp)
   574  					eventBuffer = eventBuffer[:0]
   575  				}
   576  			}
   577  			l.publishBatch(batch)
   578  			batch.reset()
   579  		case msg, more := <-l.messages:
   580  			if !more {
   581  				// Flush event buffer and release resources
   582  				l.processEvent(batch, eventBuffer, eventBufferTimestamp)
   583  				l.publishBatch(batch)
   584  				batch.reset()
   585  				return
   586  			}
   587  			if eventBufferTimestamp == 0 {
   588  				eventBufferTimestamp = msg.Timestamp.UnixNano() / int64(time.Millisecond)
   589  			}
   590  			line := msg.Line
   591  			if l.multilinePattern != nil {
   592  				lineEffectiveLen := effectiveLen(string(line))
   593  				if l.multilinePattern.Match(line) || effectiveLen(string(eventBuffer))+lineEffectiveLen > maximumBytesPerEvent {
   594  					// This is a new log event or we will exceed max bytes per event
   595  					// so flush the current eventBuffer to events and reset timestamp
   596  					l.processEvent(batch, eventBuffer, eventBufferTimestamp)
   597  					eventBufferTimestamp = msg.Timestamp.UnixNano() / int64(time.Millisecond)
   598  					eventBuffer = eventBuffer[:0]
   599  				}
   600  				// Append newline if event is less than max event size
   601  				if lineEffectiveLen < maximumBytesPerEvent {
   602  					line = append(line, "\n"...)
   603  				}
   604  				eventBuffer = append(eventBuffer, line...)
   605  				logger.PutMessage(msg)
   606  			} else {
   607  				l.processEvent(batch, line, msg.Timestamp.UnixNano()/int64(time.Millisecond))
   608  				logger.PutMessage(msg)
   609  			}
   610  		}
   611  	}
   612  }
   613  
   614  // processEvent processes log events that are ready for submission to CloudWatch
   615  // logs.  Batching is performed on time- and size-bases.  Time-based batching occurs
   616  // at the interval defined by awslogs-force-flush-interval-seconds (defaults to 5 seconds).
   617  // Size-based batching is performed on the maximum number of events per batch
   618  // (defined in maximumLogEventsPerPut) and the maximum number of total bytes in a
   619  // batch (defined in maximumBytesPerPut).  Log messages are split by the maximum
   620  // bytes per event (defined in maximumBytesPerEvent).  There is a fixed per-event
   621  // byte overhead (defined in perEventBytes) which is accounted for in split- and
   622  // batch-calculations.  Because the events are interpreted as UTF-8 encoded
   623  // Unicode, invalid UTF-8 byte sequences are replaced with the Unicode
   624  // replacement character (U+FFFD), which is a 3-byte sequence in UTF-8.  To
   625  // compensate for that and to avoid splitting valid UTF-8 characters into
   626  // invalid byte sequences, we calculate the length of each event assuming that
   627  // this replacement happens.
   628  func (l *logStream) processEvent(batch *eventBatch, bytes []byte, timestamp int64) {
   629  	for len(bytes) > 0 {
   630  		// Split line length so it does not exceed the maximum
   631  		splitOffset, lineBytes := findValidSplit(string(bytes), maximumBytesPerEvent)
   632  		line := bytes[:splitOffset]
   633  		event := wrappedEvent{
   634  			inputLogEvent: types.InputLogEvent{
   635  				Message:   aws.String(string(line)),
   636  				Timestamp: aws.Int64(timestamp),
   637  			},
   638  			insertOrder: batch.count(),
   639  		}
   640  
   641  		added := batch.add(event, lineBytes)
   642  		if added {
   643  			bytes = bytes[splitOffset:]
   644  		} else {
   645  			l.publishBatch(batch)
   646  			batch.reset()
   647  		}
   648  	}
   649  }
   650  
   651  // effectiveLen counts the effective number of bytes in the string, after
   652  // UTF-8 normalization.  UTF-8 normalization includes replacing bytes that do
   653  // not constitute valid UTF-8 encoded Unicode codepoints with the Unicode
   654  // replacement codepoint U+FFFD (a 3-byte UTF-8 sequence, represented in Go as
   655  // utf8.RuneError)
   656  func effectiveLen(line string) int {
   657  	effectiveBytes := 0
   658  	for _, rune := range line {
   659  		effectiveBytes += utf8.RuneLen(rune)
   660  	}
   661  	return effectiveBytes
   662  }
   663  
   664  // findValidSplit finds the byte offset to split a string without breaking valid
   665  // Unicode codepoints given a maximum number of total bytes.  findValidSplit
   666  // returns the byte offset for splitting a string or []byte, as well as the
   667  // effective number of bytes if the string were normalized to replace invalid
   668  // UTF-8 encoded bytes with the Unicode replacement character (a 3-byte UTF-8
   669  // sequence, represented in Go as utf8.RuneError)
   670  func findValidSplit(line string, maxBytes int) (splitOffset, effectiveBytes int) {
   671  	for offset, rune := range line {
   672  		splitOffset = offset
   673  		if effectiveBytes+utf8.RuneLen(rune) > maxBytes {
   674  			return splitOffset, effectiveBytes
   675  		}
   676  		effectiveBytes += utf8.RuneLen(rune)
   677  	}
   678  	splitOffset = len(line)
   679  	return
   680  }
   681  
   682  // publishBatch calls PutLogEvents for a given set of InputLogEvents,
   683  // accounting for sequencing requirements (each request must reference the
   684  // sequence token returned by the previous request).
   685  func (l *logStream) publishBatch(batch *eventBatch) {
   686  	if batch.isEmpty() {
   687  		return
   688  	}
   689  	cwEvents := unwrapEvents(batch.events())
   690  
   691  	nextSequenceToken, err := l.putLogEvents(cwEvents, l.sequenceToken)
   692  	if err != nil {
   693  		if apiErr := (*types.DataAlreadyAcceptedException)(nil); errors.As(err, &apiErr) {
   694  			// already submitted, just grab the correct sequence token
   695  			nextSequenceToken = apiErr.ExpectedSequenceToken
   696  			log.G(context.TODO()).WithFields(log.Fields{
   697  				"errorCode":     apiErr.ErrorCode(),
   698  				"message":       apiErr.ErrorMessage(),
   699  				"logGroupName":  l.logGroupName,
   700  				"logStreamName": l.logStreamName,
   701  			}).Info("Data already accepted, ignoring error")
   702  			err = nil
   703  		} else if apiErr := (*types.InvalidSequenceTokenException)(nil); errors.As(err, &apiErr) {
   704  			nextSequenceToken, err = l.putLogEvents(cwEvents, apiErr.ExpectedSequenceToken)
   705  		}
   706  	}
   707  	if err != nil {
   708  		log.G(context.TODO()).Error(err)
   709  	} else {
   710  		l.sequenceToken = nextSequenceToken
   711  	}
   712  }
   713  
   714  // putLogEvents wraps the PutLogEvents API
   715  func (l *logStream) putLogEvents(events []types.InputLogEvent, sequenceToken *string) (*string, error) {
   716  	input := &cloudwatchlogs.PutLogEventsInput{
   717  		LogEvents:     events,
   718  		SequenceToken: sequenceToken,
   719  		LogGroupName:  aws.String(l.logGroupName),
   720  		LogStreamName: aws.String(l.logStreamName),
   721  	}
   722  	resp, err := l.client.PutLogEvents(context.TODO(), input)
   723  	if err != nil {
   724  		var apiErr smithy.APIError
   725  		if errors.As(err, &apiErr) {
   726  			log.G(context.TODO()).WithFields(log.Fields{
   727  				"errorCode":     apiErr.ErrorCode(),
   728  				"message":       apiErr.ErrorMessage(),
   729  				"logGroupName":  l.logGroupName,
   730  				"logStreamName": l.logStreamName,
   731  			}).Error("Failed to put log events")
   732  		}
   733  		return nil, err
   734  	}
   735  	return resp.NextSequenceToken, nil
   736  }
   737  
   738  // ValidateLogOpt looks for awslogs-specific log options awslogs-region, awslogs-endpoint
   739  // awslogs-group, awslogs-stream, awslogs-create-group, awslogs-datetime-format,
   740  // awslogs-multiline-pattern
   741  func ValidateLogOpt(cfg map[string]string) error {
   742  	for key := range cfg {
   743  		switch key {
   744  		case logGroupKey:
   745  		case logStreamKey:
   746  		case logCreateGroupKey:
   747  		case regionKey:
   748  		case endpointKey:
   749  		case tagKey:
   750  		case datetimeFormatKey:
   751  		case multilinePatternKey:
   752  		case credentialsEndpointKey:
   753  		case forceFlushIntervalKey:
   754  		case maxBufferedEventsKey:
   755  		case logFormatKey:
   756  		default:
   757  			return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name)
   758  		}
   759  	}
   760  	if cfg[logGroupKey] == "" {
   761  		return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey)
   762  	}
   763  	if cfg[logCreateGroupKey] != "" {
   764  		if _, err := strconv.ParseBool(cfg[logCreateGroupKey]); err != nil {
   765  			return fmt.Errorf("must specify valid value for log opt '%s': %v", logCreateGroupKey, err)
   766  		}
   767  	}
   768  	if cfg[forceFlushIntervalKey] != "" {
   769  		if value, err := strconv.Atoi(cfg[forceFlushIntervalKey]); err != nil || value <= 0 {
   770  			return fmt.Errorf("must specify a positive integer for log opt '%s': %v", forceFlushIntervalKey, cfg[forceFlushIntervalKey])
   771  		}
   772  	}
   773  	if cfg[maxBufferedEventsKey] != "" {
   774  		if value, err := strconv.Atoi(cfg[maxBufferedEventsKey]); err != nil || value <= 0 {
   775  			return fmt.Errorf("must specify a positive integer for log opt '%s': %v", maxBufferedEventsKey, cfg[maxBufferedEventsKey])
   776  		}
   777  	}
   778  	_, datetimeFormatKeyExists := cfg[datetimeFormatKey]
   779  	_, multilinePatternKeyExists := cfg[multilinePatternKey]
   780  	if datetimeFormatKeyExists && multilinePatternKeyExists {
   781  		return fmt.Errorf("you cannot configure log opt '%s' and '%s' at the same time", datetimeFormatKey, multilinePatternKey)
   782  	}
   783  
   784  	if cfg[logFormatKey] != "" {
   785  		// For now, only the "json/emf" log format is supported
   786  		if cfg[logFormatKey] != jsonEmfLogFormat {
   787  			return fmt.Errorf("unsupported log format '%s'", cfg[logFormatKey])
   788  		}
   789  		if datetimeFormatKeyExists || multilinePatternKeyExists {
   790  			return fmt.Errorf("you cannot configure log opt '%s' or '%s' when log opt '%s' is set to '%s'", datetimeFormatKey, multilinePatternKey, logFormatKey, jsonEmfLogFormat)
   791  		}
   792  	}
   793  
   794  	return nil
   795  }
   796  
   797  // Len returns the length of a byTimestamp slice.  Len is required by the
   798  // sort.Interface interface.
   799  func (slice byTimestamp) Len() int {
   800  	return len(slice)
   801  }
   802  
   803  // Less compares two values in a byTimestamp slice by Timestamp.  Less is
   804  // required by the sort.Interface interface.
   805  func (slice byTimestamp) Less(i, j int) bool {
   806  	iTimestamp, jTimestamp := int64(0), int64(0)
   807  	if slice != nil && slice[i].inputLogEvent.Timestamp != nil {
   808  		iTimestamp = *slice[i].inputLogEvent.Timestamp
   809  	}
   810  	if slice != nil && slice[j].inputLogEvent.Timestamp != nil {
   811  		jTimestamp = *slice[j].inputLogEvent.Timestamp
   812  	}
   813  	if iTimestamp == jTimestamp {
   814  		return slice[i].insertOrder < slice[j].insertOrder
   815  	}
   816  	return iTimestamp < jTimestamp
   817  }
   818  
   819  // Swap swaps two values in a byTimestamp slice with each other.  Swap is
   820  // required by the sort.Interface interface.
   821  func (slice byTimestamp) Swap(i, j int) {
   822  	slice[i], slice[j] = slice[j], slice[i]
   823  }
   824  
   825  func unwrapEvents(events []wrappedEvent) []types.InputLogEvent {
   826  	cwEvents := make([]types.InputLogEvent, len(events))
   827  	for i, input := range events {
   828  		cwEvents[i] = input.inputLogEvent
   829  	}
   830  	return cwEvents
   831  }
   832  
   833  func newEventBatch() *eventBatch {
   834  	return &eventBatch{
   835  		batch: make([]wrappedEvent, 0),
   836  		bytes: 0,
   837  	}
   838  }
   839  
   840  // events returns a slice of wrappedEvents sorted in order of their
   841  // timestamps and then by their insertion order (see `byTimestamp`).
   842  //
   843  // Warning: this method is not threadsafe and must not be used
   844  // concurrently.
   845  func (b *eventBatch) events() []wrappedEvent {
   846  	sort.Sort(byTimestamp(b.batch))
   847  	return b.batch
   848  }
   849  
   850  // add adds an event to the batch of events accounting for the
   851  // necessary overhead for an event to be logged. An error will be
   852  // returned if the event cannot be added to the batch due to service
   853  // limits.
   854  //
   855  // Warning: this method is not threadsafe and must not be used
   856  // concurrently.
   857  func (b *eventBatch) add(event wrappedEvent, size int) bool {
   858  	addBytes := size + perEventBytes
   859  
   860  	// verify we are still within service limits
   861  	switch {
   862  	case len(b.batch)+1 > maximumLogEventsPerPut:
   863  		return false
   864  	case b.bytes+addBytes > maximumBytesPerPut:
   865  		return false
   866  	}
   867  
   868  	b.bytes += addBytes
   869  	b.batch = append(b.batch, event)
   870  
   871  	return true
   872  }
   873  
   874  // count is the number of batched events.  Warning: this method
   875  // is not threadsafe and must not be used concurrently.
   876  func (b *eventBatch) count() int {
   877  	return len(b.batch)
   878  }
   879  
   880  // size is the total number of bytes that the batch represents.
   881  //
   882  // Warning: this method is not threadsafe and must not be used
   883  // concurrently.
   884  func (b *eventBatch) size() int {
   885  	return b.bytes
   886  }
   887  
   888  func (b *eventBatch) isEmpty() bool {
   889  	zeroEvents := b.count() == 0
   890  	zeroSize := b.size() == 0
   891  	return zeroEvents && zeroSize
   892  }
   893  
   894  // reset prepares the batch for reuse.
   895  func (b *eventBatch) reset() {
   896  	b.bytes = 0
   897  	b.batch = b.batch[:0]
   898  }