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 }