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 }