github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/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/Prakhar-Agarwal-byte/moby/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/Prakhar-Agarwal-byte/moby/daemon/logger"
    27  	"github.com/Prakhar-Agarwal-byte/moby/daemon/logger/loggerutils"
    28  	"github.com/Prakhar-Agarwal-byte/moby/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  	)
   410  
   411  	if endpoint != nil {
   412  		clientOpts = append(clientOpts, cloudwatchlogs.WithEndpointResolver(cloudwatchlogs.EndpointResolverFromURL(*endpoint)))
   413  	}
   414  
   415  	client := cloudwatchlogs.NewFromConfig(cfg, clientOpts...)
   416  
   417  	return client, nil
   418  }
   419  
   420  // Name returns the name of the awslogs logging driver
   421  func (l *logStream) Name() string {
   422  	return name
   423  }
   424  
   425  // BufSize returns the maximum bytes CloudWatch can handle.
   426  func (l *logStream) BufSize() int {
   427  	return maximumBytesPerEvent
   428  }
   429  
   430  // Log submits messages for logging by an instance of the awslogs logging driver
   431  func (l *logStream) Log(msg *logger.Message) error {
   432  	l.lock.RLock()
   433  	defer l.lock.RUnlock()
   434  	if l.closed {
   435  		return errors.New("awslogs is closed")
   436  	}
   437  	l.messages <- msg
   438  	return nil
   439  }
   440  
   441  // Close closes the instance of the awslogs logging driver
   442  func (l *logStream) Close() error {
   443  	l.lock.Lock()
   444  	defer l.lock.Unlock()
   445  	if !l.closed {
   446  		close(l.messages)
   447  	}
   448  	l.closed = true
   449  	return nil
   450  }
   451  
   452  // create creates log group and log stream for the instance of the awslogs logging driver
   453  func (l *logStream) create() error {
   454  	err := l.createLogStream()
   455  	if err == nil {
   456  		return nil
   457  	}
   458  
   459  	var apiErr *types.ResourceNotFoundException
   460  	if errors.As(err, &apiErr) && l.logCreateGroup {
   461  		if err := l.createLogGroup(); err != nil {
   462  			return errors.Wrap(err, "failed to create Cloudwatch log group")
   463  		}
   464  		err = l.createLogStream()
   465  		if err == nil {
   466  			return nil
   467  		}
   468  	}
   469  	return errors.Wrap(err, "failed to create Cloudwatch log stream")
   470  }
   471  
   472  // createLogGroup creates a log group for the instance of the awslogs logging driver
   473  func (l *logStream) createLogGroup() error {
   474  	if _, err := l.client.CreateLogGroup(context.TODO(), &cloudwatchlogs.CreateLogGroupInput{
   475  		LogGroupName: aws.String(l.logGroupName),
   476  	}); err != nil {
   477  		var apiErr smithy.APIError
   478  		if errors.As(err, &apiErr) {
   479  			fields := log.Fields{
   480  				"errorCode":      apiErr.ErrorCode(),
   481  				"message":        apiErr.ErrorMessage(),
   482  				"logGroupName":   l.logGroupName,
   483  				"logCreateGroup": l.logCreateGroup,
   484  			}
   485  			if _, ok := apiErr.(*types.ResourceAlreadyExistsException); ok {
   486  				// Allow creation to succeed
   487  				log.G(context.TODO()).WithFields(fields).Info("Log group already exists")
   488  				return nil
   489  			}
   490  			log.G(context.TODO()).WithFields(fields).Error("Failed to create log group")
   491  		}
   492  		return err
   493  	}
   494  	return nil
   495  }
   496  
   497  // createLogStream creates a log stream for the instance of the awslogs logging driver
   498  func (l *logStream) createLogStream() error {
   499  	// Directly return if we do not want to create log stream.
   500  	if !l.logCreateStream {
   501  		log.G(context.TODO()).WithFields(log.Fields{
   502  			"logGroupName":    l.logGroupName,
   503  			"logStreamName":   l.logStreamName,
   504  			"logCreateStream": l.logCreateStream,
   505  		}).Info("Skipping creating log stream")
   506  		return nil
   507  	}
   508  
   509  	input := &cloudwatchlogs.CreateLogStreamInput{
   510  		LogGroupName:  aws.String(l.logGroupName),
   511  		LogStreamName: aws.String(l.logStreamName),
   512  	}
   513  
   514  	_, err := l.client.CreateLogStream(context.TODO(), input)
   515  	if err != nil {
   516  		var apiErr smithy.APIError
   517  		if errors.As(err, &apiErr) {
   518  			fields := log.Fields{
   519  				"errorCode":     apiErr.ErrorCode(),
   520  				"message":       apiErr.ErrorMessage(),
   521  				"logGroupName":  l.logGroupName,
   522  				"logStreamName": l.logStreamName,
   523  			}
   524  			if _, ok := apiErr.(*types.ResourceAlreadyExistsException); ok {
   525  				// Allow creation to succeed
   526  				log.G(context.TODO()).WithFields(fields).Info("Log stream already exists")
   527  				return nil
   528  			}
   529  			log.G(context.TODO()).WithFields(fields).Error("Failed to create log stream")
   530  		}
   531  	}
   532  	return err
   533  }
   534  
   535  // newTicker is used for time-based batching.  newTicker is a variable such
   536  // that the implementation can be swapped out for unit tests.
   537  var newTicker = func(freq time.Duration) *time.Ticker {
   538  	return time.NewTicker(freq)
   539  }
   540  
   541  // collectBatch executes as a goroutine to perform batching of log events for
   542  // submission to the log stream.  If the awslogs-multiline-pattern or
   543  // awslogs-datetime-format options have been configured, multiline processing
   544  // is enabled, where log messages are stored in an event buffer until a multiline
   545  // pattern match is found, at which point the messages in the event buffer are
   546  // pushed to CloudWatch logs as a single log event.  Multiline messages are processed
   547  // according to the maximumBytesPerPut constraint, and the implementation only
   548  // allows for messages to be buffered for a maximum of 2*l.forceFlushInterval
   549  // seconds.  If no forceFlushInterval is specified for the log stream, then the default
   550  // of 5 seconds will be used resulting in a maximum of 10 seconds buffer time for multiline
   551  // messages. When events are ready to be processed for submission to CloudWatch
   552  // Logs, the processEvents method is called.  If a multiline pattern is not
   553  // configured, log events are submitted to the processEvents method immediately.
   554  func (l *logStream) collectBatch(created chan bool) {
   555  	// Wait for the logstream/group to be created
   556  	<-created
   557  	flushInterval := l.forceFlushInterval
   558  	if flushInterval <= 0 {
   559  		flushInterval = defaultForceFlushInterval
   560  	}
   561  	ticker := newTicker(flushInterval)
   562  	var eventBuffer []byte
   563  	var eventBufferTimestamp int64
   564  	batch := newEventBatch()
   565  	for {
   566  		select {
   567  		case t := <-ticker.C:
   568  			// If event buffer is older than batch publish frequency flush the event buffer
   569  			if eventBufferTimestamp > 0 && len(eventBuffer) > 0 {
   570  				eventBufferAge := t.UnixNano()/int64(time.Millisecond) - eventBufferTimestamp
   571  				eventBufferExpired := eventBufferAge >= int64(flushInterval)/int64(time.Millisecond)
   572  				eventBufferNegative := eventBufferAge < 0
   573  				if eventBufferExpired || eventBufferNegative {
   574  					l.processEvent(batch, eventBuffer, eventBufferTimestamp)
   575  					eventBuffer = eventBuffer[:0]
   576  				}
   577  			}
   578  			l.publishBatch(batch)
   579  			batch.reset()
   580  		case msg, more := <-l.messages:
   581  			if !more {
   582  				// Flush event buffer and release resources
   583  				l.processEvent(batch, eventBuffer, eventBufferTimestamp)
   584  				l.publishBatch(batch)
   585  				batch.reset()
   586  				return
   587  			}
   588  			if eventBufferTimestamp == 0 {
   589  				eventBufferTimestamp = msg.Timestamp.UnixNano() / int64(time.Millisecond)
   590  			}
   591  			line := msg.Line
   592  			if l.multilinePattern != nil {
   593  				lineEffectiveLen := effectiveLen(string(line))
   594  				if l.multilinePattern.Match(line) || effectiveLen(string(eventBuffer))+lineEffectiveLen > maximumBytesPerEvent {
   595  					// This is a new log event or we will exceed max bytes per event
   596  					// so flush the current eventBuffer to events and reset timestamp
   597  					l.processEvent(batch, eventBuffer, eventBufferTimestamp)
   598  					eventBufferTimestamp = msg.Timestamp.UnixNano() / int64(time.Millisecond)
   599  					eventBuffer = eventBuffer[:0]
   600  				}
   601  				// Append newline if event is less than max event size
   602  				if lineEffectiveLen < maximumBytesPerEvent {
   603  					line = append(line, "\n"...)
   604  				}
   605  				eventBuffer = append(eventBuffer, line...)
   606  				logger.PutMessage(msg)
   607  			} else {
   608  				l.processEvent(batch, line, msg.Timestamp.UnixNano()/int64(time.Millisecond))
   609  				logger.PutMessage(msg)
   610  			}
   611  		}
   612  	}
   613  }
   614  
   615  // processEvent processes log events that are ready for submission to CloudWatch
   616  // logs.  Batching is performed on time- and size-bases.  Time-based batching occurs
   617  // at the interval defined by awslogs-force-flush-interval-seconds (defaults to 5 seconds).
   618  // Size-based batching is performed on the maximum number of events per batch
   619  // (defined in maximumLogEventsPerPut) and the maximum number of total bytes in a
   620  // batch (defined in maximumBytesPerPut).  Log messages are split by the maximum
   621  // bytes per event (defined in maximumBytesPerEvent).  There is a fixed per-event
   622  // byte overhead (defined in perEventBytes) which is accounted for in split- and
   623  // batch-calculations.  Because the events are interpreted as UTF-8 encoded
   624  // Unicode, invalid UTF-8 byte sequences are replaced with the Unicode
   625  // replacement character (U+FFFD), which is a 3-byte sequence in UTF-8.  To
   626  // compensate for that and to avoid splitting valid UTF-8 characters into
   627  // invalid byte sequences, we calculate the length of each event assuming that
   628  // this replacement happens.
   629  func (l *logStream) processEvent(batch *eventBatch, bytes []byte, timestamp int64) {
   630  	for len(bytes) > 0 {
   631  		// Split line length so it does not exceed the maximum
   632  		splitOffset, lineBytes := findValidSplit(string(bytes), maximumBytesPerEvent)
   633  		line := bytes[:splitOffset]
   634  		event := wrappedEvent{
   635  			inputLogEvent: types.InputLogEvent{
   636  				Message:   aws.String(string(line)),
   637  				Timestamp: aws.Int64(timestamp),
   638  			},
   639  			insertOrder: batch.count(),
   640  		}
   641  
   642  		added := batch.add(event, lineBytes)
   643  		if added {
   644  			bytes = bytes[splitOffset:]
   645  		} else {
   646  			l.publishBatch(batch)
   647  			batch.reset()
   648  		}
   649  	}
   650  }
   651  
   652  // effectiveLen counts the effective number of bytes in the string, after
   653  // UTF-8 normalization.  UTF-8 normalization includes replacing bytes that do
   654  // not constitute valid UTF-8 encoded Unicode codepoints with the Unicode
   655  // replacement codepoint U+FFFD (a 3-byte UTF-8 sequence, represented in Go as
   656  // utf8.RuneError)
   657  func effectiveLen(line string) int {
   658  	effectiveBytes := 0
   659  	for _, rune := range line {
   660  		effectiveBytes += utf8.RuneLen(rune)
   661  	}
   662  	return effectiveBytes
   663  }
   664  
   665  // findValidSplit finds the byte offset to split a string without breaking valid
   666  // Unicode codepoints given a maximum number of total bytes.  findValidSplit
   667  // returns the byte offset for splitting a string or []byte, as well as the
   668  // effective number of bytes if the string were normalized to replace invalid
   669  // UTF-8 encoded bytes with the Unicode replacement character (a 3-byte UTF-8
   670  // sequence, represented in Go as utf8.RuneError)
   671  func findValidSplit(line string, maxBytes int) (splitOffset, effectiveBytes int) {
   672  	for offset, rune := range line {
   673  		splitOffset = offset
   674  		if effectiveBytes+utf8.RuneLen(rune) > maxBytes {
   675  			return splitOffset, effectiveBytes
   676  		}
   677  		effectiveBytes += utf8.RuneLen(rune)
   678  	}
   679  	splitOffset = len(line)
   680  	return
   681  }
   682  
   683  // publishBatch calls PutLogEvents for a given set of InputLogEvents,
   684  // accounting for sequencing requirements (each request must reference the
   685  // sequence token returned by the previous request).
   686  func (l *logStream) publishBatch(batch *eventBatch) {
   687  	if batch.isEmpty() {
   688  		return
   689  	}
   690  	cwEvents := unwrapEvents(batch.events())
   691  
   692  	nextSequenceToken, err := l.putLogEvents(cwEvents, l.sequenceToken)
   693  	if err != nil {
   694  		if apiErr := (*types.DataAlreadyAcceptedException)(nil); errors.As(err, &apiErr) {
   695  			// already submitted, just grab the correct sequence token
   696  			nextSequenceToken = apiErr.ExpectedSequenceToken
   697  			log.G(context.TODO()).WithFields(log.Fields{
   698  				"errorCode":     apiErr.ErrorCode(),
   699  				"message":       apiErr.ErrorMessage(),
   700  				"logGroupName":  l.logGroupName,
   701  				"logStreamName": l.logStreamName,
   702  			}).Info("Data already accepted, ignoring error")
   703  			err = nil
   704  		} else if apiErr := (*types.InvalidSequenceTokenException)(nil); errors.As(err, &apiErr) {
   705  			nextSequenceToken, err = l.putLogEvents(cwEvents, apiErr.ExpectedSequenceToken)
   706  		}
   707  	}
   708  	if err != nil {
   709  		log.G(context.TODO()).Error(err)
   710  	} else {
   711  		l.sequenceToken = nextSequenceToken
   712  	}
   713  }
   714  
   715  // putLogEvents wraps the PutLogEvents API
   716  func (l *logStream) putLogEvents(events []types.InputLogEvent, sequenceToken *string) (*string, error) {
   717  	input := &cloudwatchlogs.PutLogEventsInput{
   718  		LogEvents:     events,
   719  		SequenceToken: sequenceToken,
   720  		LogGroupName:  aws.String(l.logGroupName),
   721  		LogStreamName: aws.String(l.logStreamName),
   722  	}
   723  	resp, err := l.client.PutLogEvents(context.TODO(), input)
   724  	if err != nil {
   725  		var apiErr smithy.APIError
   726  		if errors.As(err, &apiErr) {
   727  			log.G(context.TODO()).WithFields(log.Fields{
   728  				"errorCode":     apiErr.ErrorCode(),
   729  				"message":       apiErr.ErrorMessage(),
   730  				"logGroupName":  l.logGroupName,
   731  				"logStreamName": l.logStreamName,
   732  			}).Error("Failed to put log events")
   733  		}
   734  		return nil, err
   735  	}
   736  	return resp.NextSequenceToken, nil
   737  }
   738  
   739  // ValidateLogOpt looks for awslogs-specific log options awslogs-region, awslogs-endpoint
   740  // awslogs-group, awslogs-stream, awslogs-create-group, awslogs-datetime-format,
   741  // awslogs-multiline-pattern
   742  func ValidateLogOpt(cfg map[string]string) error {
   743  	for key := range cfg {
   744  		switch key {
   745  		case logGroupKey:
   746  		case logStreamKey:
   747  		case logCreateGroupKey:
   748  		case regionKey:
   749  		case endpointKey:
   750  		case tagKey:
   751  		case datetimeFormatKey:
   752  		case multilinePatternKey:
   753  		case credentialsEndpointKey:
   754  		case forceFlushIntervalKey:
   755  		case maxBufferedEventsKey:
   756  		case logFormatKey:
   757  		default:
   758  			return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name)
   759  		}
   760  	}
   761  	if cfg[logGroupKey] == "" {
   762  		return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey)
   763  	}
   764  	if cfg[logCreateGroupKey] != "" {
   765  		if _, err := strconv.ParseBool(cfg[logCreateGroupKey]); err != nil {
   766  			return fmt.Errorf("must specify valid value for log opt '%s': %v", logCreateGroupKey, err)
   767  		}
   768  	}
   769  	if cfg[forceFlushIntervalKey] != "" {
   770  		if value, err := strconv.Atoi(cfg[forceFlushIntervalKey]); err != nil || value <= 0 {
   771  			return fmt.Errorf("must specify a positive integer for log opt '%s': %v", forceFlushIntervalKey, cfg[forceFlushIntervalKey])
   772  		}
   773  	}
   774  	if cfg[maxBufferedEventsKey] != "" {
   775  		if value, err := strconv.Atoi(cfg[maxBufferedEventsKey]); err != nil || value <= 0 {
   776  			return fmt.Errorf("must specify a positive integer for log opt '%s': %v", maxBufferedEventsKey, cfg[maxBufferedEventsKey])
   777  		}
   778  	}
   779  	_, datetimeFormatKeyExists := cfg[datetimeFormatKey]
   780  	_, multilinePatternKeyExists := cfg[multilinePatternKey]
   781  	if datetimeFormatKeyExists && multilinePatternKeyExists {
   782  		return fmt.Errorf("you cannot configure log opt '%s' and '%s' at the same time", datetimeFormatKey, multilinePatternKey)
   783  	}
   784  
   785  	if cfg[logFormatKey] != "" {
   786  		// For now, only the "json/emf" log format is supported
   787  		if cfg[logFormatKey] != jsonEmfLogFormat {
   788  			return fmt.Errorf("unsupported log format '%s'", cfg[logFormatKey])
   789  		}
   790  		if datetimeFormatKeyExists || multilinePatternKeyExists {
   791  			return fmt.Errorf("you cannot configure log opt '%s' or '%s' when log opt '%s' is set to '%s'", datetimeFormatKey, multilinePatternKey, logFormatKey, jsonEmfLogFormat)
   792  		}
   793  	}
   794  
   795  	return nil
   796  }
   797  
   798  // Len returns the length of a byTimestamp slice.  Len is required by the
   799  // sort.Interface interface.
   800  func (slice byTimestamp) Len() int {
   801  	return len(slice)
   802  }
   803  
   804  // Less compares two values in a byTimestamp slice by Timestamp.  Less is
   805  // required by the sort.Interface interface.
   806  func (slice byTimestamp) Less(i, j int) bool {
   807  	iTimestamp, jTimestamp := int64(0), int64(0)
   808  	if slice != nil && slice[i].inputLogEvent.Timestamp != nil {
   809  		iTimestamp = *slice[i].inputLogEvent.Timestamp
   810  	}
   811  	if slice != nil && slice[j].inputLogEvent.Timestamp != nil {
   812  		jTimestamp = *slice[j].inputLogEvent.Timestamp
   813  	}
   814  	if iTimestamp == jTimestamp {
   815  		return slice[i].insertOrder < slice[j].insertOrder
   816  	}
   817  	return iTimestamp < jTimestamp
   818  }
   819  
   820  // Swap swaps two values in a byTimestamp slice with each other.  Swap is
   821  // required by the sort.Interface interface.
   822  func (slice byTimestamp) Swap(i, j int) {
   823  	slice[i], slice[j] = slice[j], slice[i]
   824  }
   825  
   826  func unwrapEvents(events []wrappedEvent) []types.InputLogEvent {
   827  	cwEvents := make([]types.InputLogEvent, len(events))
   828  	for i, input := range events {
   829  		cwEvents[i] = input.inputLogEvent
   830  	}
   831  	return cwEvents
   832  }
   833  
   834  func newEventBatch() *eventBatch {
   835  	return &eventBatch{
   836  		batch: make([]wrappedEvent, 0),
   837  		bytes: 0,
   838  	}
   839  }
   840  
   841  // events returns a slice of wrappedEvents sorted in order of their
   842  // timestamps and then by their insertion order (see `byTimestamp`).
   843  //
   844  // Warning: this method is not threadsafe and must not be used
   845  // concurrently.
   846  func (b *eventBatch) events() []wrappedEvent {
   847  	sort.Sort(byTimestamp(b.batch))
   848  	return b.batch
   849  }
   850  
   851  // add adds an event to the batch of events accounting for the
   852  // necessary overhead for an event to be logged. An error will be
   853  // returned if the event cannot be added to the batch due to service
   854  // limits.
   855  //
   856  // Warning: this method is not threadsafe and must not be used
   857  // concurrently.
   858  func (b *eventBatch) add(event wrappedEvent, size int) bool {
   859  	addBytes := size + perEventBytes
   860  
   861  	// verify we are still within service limits
   862  	switch {
   863  	case len(b.batch)+1 > maximumLogEventsPerPut:
   864  		return false
   865  	case b.bytes+addBytes > maximumBytesPerPut:
   866  		return false
   867  	}
   868  
   869  	b.bytes += addBytes
   870  	b.batch = append(b.batch, event)
   871  
   872  	return true
   873  }
   874  
   875  // count is the number of batched events.  Warning: this method
   876  // is not threadsafe and must not be used concurrently.
   877  func (b *eventBatch) count() int {
   878  	return len(b.batch)
   879  }
   880  
   881  // size is the total number of bytes that the batch represents.
   882  //
   883  // Warning: this method is not threadsafe and must not be used
   884  // concurrently.
   885  func (b *eventBatch) size() int {
   886  	return b.bytes
   887  }
   888  
   889  func (b *eventBatch) isEmpty() bool {
   890  	zeroEvents := b.count() == 0
   891  	zeroSize := b.size() == 0
   892  	return zeroEvents && zeroSize
   893  }
   894  
   895  // reset prepares the batch for reuse.
   896  func (b *eventBatch) reset() {
   897  	b.bytes = 0
   898  	b.batch = b.batch[:0]
   899  }