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