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