github.com/jiasir/docker@v1.3.3-0.20170609024000-252e610103e7/daemon/logger/awslogs/cloudwatchlogs.go (about) 1 // Package awslogs provides the logdriver for forwarding container logs to Amazon CloudWatch Logs 2 package awslogs 3 4 import ( 5 "bytes" 6 "fmt" 7 "os" 8 "regexp" 9 "runtime" 10 "sort" 11 "strconv" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/Sirupsen/logrus" 17 "github.com/aws/aws-sdk-go/aws" 18 "github.com/aws/aws-sdk-go/aws/awserr" 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/docker/docker/pkg/templates" 27 "github.com/pkg/errors" 28 ) 29 30 const ( 31 name = "awslogs" 32 regionKey = "awslogs-region" 33 regionEnvKey = "AWS_REGION" 34 logGroupKey = "awslogs-group" 35 logStreamKey = "awslogs-stream" 36 logCreateGroupKey = "awslogs-create-group" 37 tagKey = "tag" 38 datetimeFormatKey = "awslogs-datetime-format" 39 multilinePatternKey = "awslogs-multiline-pattern" 40 batchPublishFrequency = 5 * time.Second 41 42 // See: http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html 43 perEventBytes = 26 44 maximumBytesPerPut = 1048576 45 maximumLogEventsPerPut = 10000 46 47 // See: http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_limits.html 48 maximumBytesPerEvent = 262144 - perEventBytes 49 50 resourceAlreadyExistsCode = "ResourceAlreadyExistsException" 51 dataAlreadyAcceptedCode = "DataAlreadyAcceptedException" 52 invalidSequenceTokenCode = "InvalidSequenceTokenException" 53 resourceNotFoundCode = "ResourceNotFoundException" 54 55 userAgentHeader = "User-Agent" 56 ) 57 58 type logStream struct { 59 logStreamName string 60 logGroupName string 61 logCreateGroup bool 62 multilinePattern *regexp.Regexp 63 client api 64 messages chan *logger.Message 65 lock sync.RWMutex 66 closed bool 67 sequenceToken *string 68 } 69 70 type api interface { 71 CreateLogGroup(*cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) 72 CreateLogStream(*cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) 73 PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) 74 } 75 76 type regionFinder interface { 77 Region() (string, error) 78 } 79 80 type wrappedEvent struct { 81 inputLogEvent *cloudwatchlogs.InputLogEvent 82 insertOrder int 83 } 84 type byTimestamp []wrappedEvent 85 86 // init registers the awslogs driver 87 func init() { 88 if err := logger.RegisterLogDriver(name, New); err != nil { 89 logrus.Fatal(err) 90 } 91 if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil { 92 logrus.Fatal(err) 93 } 94 } 95 96 // New creates an awslogs logger using the configuration passed in on the 97 // context. Supported context configuration variables are awslogs-region, 98 // awslogs-group, awslogs-stream, awslogs-create-group, awslogs-multiline-pattern 99 // and awslogs-datetime-format. When available, configuration is 100 // also taken from environment variables AWS_REGION, AWS_ACCESS_KEY_ID, 101 // AWS_SECRET_ACCESS_KEY, the shared credentials file (~/.aws/credentials), and 102 // the EC2 Instance Metadata Service. 103 func New(info logger.Info) (logger.Logger, error) { 104 logGroupName := info.Config[logGroupKey] 105 logStreamName, err := loggerutils.ParseLogTag(info, "{{.FullID}}") 106 if err != nil { 107 return nil, err 108 } 109 logCreateGroup := false 110 if info.Config[logCreateGroupKey] != "" { 111 logCreateGroup, err = strconv.ParseBool(info.Config[logCreateGroupKey]) 112 if err != nil { 113 return nil, err 114 } 115 } 116 117 if info.Config[logStreamKey] != "" { 118 logStreamName = info.Config[logStreamKey] 119 } 120 121 multilinePattern, err := parseMultilineOptions(info) 122 if err != nil { 123 return nil, err 124 } 125 126 client, err := newAWSLogsClient(info) 127 if err != nil { 128 return nil, err 129 } 130 containerStream := &logStream{ 131 logStreamName: logStreamName, 132 logGroupName: logGroupName, 133 logCreateGroup: logCreateGroup, 134 multilinePattern: multilinePattern, 135 client: client, 136 messages: make(chan *logger.Message, 4096), 137 } 138 err = containerStream.create() 139 if err != nil { 140 return nil, err 141 } 142 go containerStream.collectBatch() 143 144 return containerStream, nil 145 } 146 147 // Parses awslogs-multiline-pattern and awslogs-datetime-format options 148 // If awslogs-datetime-format is present, convert the format from strftime 149 // to regexp and return. 150 // If awslogs-multiline-pattern is present, compile regexp and return 151 func parseMultilineOptions(info logger.Info) (*regexp.Regexp, error) { 152 dateTimeFormat := info.Config[datetimeFormatKey] 153 multilinePatternKey := info.Config[multilinePatternKey] 154 // strftime input is parsed into a regular expression 155 if dateTimeFormat != "" { 156 // %. matches each strftime format sequence and ReplaceAllStringFunc 157 // looks up each format sequence in the conversion table strftimeToRegex 158 // to replace with a defined regular expression 159 r := regexp.MustCompile("%.") 160 multilinePatternKey = r.ReplaceAllStringFunc(dateTimeFormat, func(s string) string { 161 return strftimeToRegex[s] 162 }) 163 } 164 if multilinePatternKey != "" { 165 multilinePattern, err := regexp.Compile(multilinePatternKey) 166 if err != nil { 167 return nil, errors.Wrapf(err, "awslogs could not parse multiline pattern key %q", multilinePatternKey) 168 } 169 return multilinePattern, nil 170 } 171 return nil, nil 172 } 173 174 // Maps strftime format strings to regex 175 var strftimeToRegex = map[string]string{ 176 /*weekdayShort */ `%a`: `(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)`, 177 /*weekdayFull */ `%A`: `(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)`, 178 /*weekdayZeroIndex */ `%w`: `[0-6]`, 179 /*dayZeroPadded */ `%d`: `(?:0[1-9]|[1,2][0-9]|3[0,1])`, 180 /*monthShort */ `%b`: `(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)`, 181 /*monthFull */ `%B`: `(?:January|February|March|April|May|June|July|August|September|October|November|December)`, 182 /*monthZeroPadded */ `%m`: `(?:0[1-9]|1[0-2])`, 183 /*yearCentury */ `%Y`: `\d{4}`, 184 /*yearZeroPadded */ `%y`: `\d{2}`, 185 /*hour24ZeroPadded */ `%H`: `(?:[0,1][0-9]|2[0-3])`, 186 /*hour12ZeroPadded */ `%I`: `(?:0[0-9]|1[0-2])`, 187 /*AM or PM */ `%p`: "[A,P]M", 188 /*minuteZeroPadded */ `%M`: `[0-5][0-9]`, 189 /*secondZeroPadded */ `%S`: `[0-5][0-9]`, 190 /*microsecondZeroPadded */ `%f`: `\d{6}`, 191 /*utcOffset */ `%z`: `[+-]\d{4}`, 192 /*tzName */ `%Z`: `[A-Z]{1,4}T`, 193 /*dayOfYearZeroPadded */ `%j`: `(?:0[0-9][1-9]|[1,2][0-9][0-9]|3[0-5][0-9]|36[0-6])`, 194 /*milliseconds */ `%L`: `\.\d{3}`, 195 } 196 197 func parseLogGroup(info logger.Info, groupTemplate string) (string, error) { 198 tmpl, err := templates.NewParse("log-group", groupTemplate) 199 if err != nil { 200 return "", err 201 } 202 buf := new(bytes.Buffer) 203 if err := tmpl.Execute(buf, &info); err != nil { 204 return "", err 205 } 206 207 return buf.String(), nil 208 } 209 210 // newRegionFinder is a variable such that the implementation 211 // can be swapped out for unit tests. 212 var newRegionFinder = func() regionFinder { 213 return ec2metadata.New(session.New()) 214 } 215 216 // newAWSLogsClient creates the service client for Amazon CloudWatch Logs. 217 // Customizations to the default client from the SDK include a Docker-specific 218 // User-Agent string and automatic region detection using the EC2 Instance 219 // Metadata Service when region is otherwise unspecified. 220 func newAWSLogsClient(info logger.Info) (api, error) { 221 var region *string 222 if os.Getenv(regionEnvKey) != "" { 223 region = aws.String(os.Getenv(regionEnvKey)) 224 } 225 if info.Config[regionKey] != "" { 226 region = aws.String(info.Config[regionKey]) 227 } 228 if region == nil || *region == "" { 229 logrus.Info("Trying to get region from EC2 Metadata") 230 ec2MetadataClient := newRegionFinder() 231 r, err := ec2MetadataClient.Region() 232 if err != nil { 233 logrus.WithFields(logrus.Fields{ 234 "error": err, 235 }).Error("Could not get region from EC2 metadata, environment, or log option") 236 return nil, errors.New("Cannot determine region for awslogs driver") 237 } 238 region = &r 239 } 240 logrus.WithFields(logrus.Fields{ 241 "region": *region, 242 }).Debug("Created awslogs client") 243 244 client := cloudwatchlogs.New(session.New(), aws.NewConfig().WithRegion(*region)) 245 246 client.Handlers.Build.PushBackNamed(request.NamedHandler{ 247 Name: "DockerUserAgentHandler", 248 Fn: func(r *request.Request) { 249 currentAgent := r.HTTPRequest.Header.Get(userAgentHeader) 250 r.HTTPRequest.Header.Set(userAgentHeader, 251 fmt.Sprintf("Docker %s (%s) %s", 252 dockerversion.Version, runtime.GOOS, currentAgent)) 253 }, 254 }) 255 return client, nil 256 } 257 258 // Name returns the name of the awslogs logging driver 259 func (l *logStream) Name() string { 260 return name 261 } 262 263 // Log submits messages for logging by an instance of the awslogs logging driver 264 func (l *logStream) Log(msg *logger.Message) error { 265 l.lock.RLock() 266 defer l.lock.RUnlock() 267 if !l.closed { 268 l.messages <- msg 269 } 270 return nil 271 } 272 273 // Close closes the instance of the awslogs logging driver 274 func (l *logStream) Close() error { 275 l.lock.Lock() 276 defer l.lock.Unlock() 277 if !l.closed { 278 close(l.messages) 279 } 280 l.closed = true 281 return nil 282 } 283 284 // create creates log group and log stream for the instance of the awslogs logging driver 285 func (l *logStream) create() error { 286 if err := l.createLogStream(); err != nil { 287 if l.logCreateGroup { 288 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == resourceNotFoundCode { 289 if err := l.createLogGroup(); err != nil { 290 return err 291 } 292 return l.createLogStream() 293 } 294 } 295 return err 296 } 297 298 return nil 299 } 300 301 // createLogGroup creates a log group for the instance of the awslogs logging driver 302 func (l *logStream) createLogGroup() error { 303 if _, err := l.client.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{ 304 LogGroupName: aws.String(l.logGroupName), 305 }); err != nil { 306 if awsErr, ok := err.(awserr.Error); ok { 307 fields := logrus.Fields{ 308 "errorCode": awsErr.Code(), 309 "message": awsErr.Message(), 310 "origError": awsErr.OrigErr(), 311 "logGroupName": l.logGroupName, 312 "logCreateGroup": l.logCreateGroup, 313 } 314 if awsErr.Code() == resourceAlreadyExistsCode { 315 // Allow creation to succeed 316 logrus.WithFields(fields).Info("Log group already exists") 317 return nil 318 } 319 logrus.WithFields(fields).Error("Failed to create log group") 320 } 321 return err 322 } 323 return nil 324 } 325 326 // createLogStream creates a log stream for the instance of the awslogs logging driver 327 func (l *logStream) createLogStream() error { 328 input := &cloudwatchlogs.CreateLogStreamInput{ 329 LogGroupName: aws.String(l.logGroupName), 330 LogStreamName: aws.String(l.logStreamName), 331 } 332 333 _, err := l.client.CreateLogStream(input) 334 335 if err != nil { 336 if awsErr, ok := err.(awserr.Error); ok { 337 fields := logrus.Fields{ 338 "errorCode": awsErr.Code(), 339 "message": awsErr.Message(), 340 "origError": awsErr.OrigErr(), 341 "logGroupName": l.logGroupName, 342 "logStreamName": l.logStreamName, 343 } 344 if awsErr.Code() == resourceAlreadyExistsCode { 345 // Allow creation to succeed 346 logrus.WithFields(fields).Info("Log stream already exists") 347 return nil 348 } 349 logrus.WithFields(fields).Error("Failed to create log stream") 350 } 351 } 352 return err 353 } 354 355 // newTicker is used for time-based batching. newTicker is a variable such 356 // that the implementation can be swapped out for unit tests. 357 var newTicker = func(freq time.Duration) *time.Ticker { 358 return time.NewTicker(freq) 359 } 360 361 // collectBatch executes as a goroutine to perform batching of log events for 362 // submission to the log stream. If the awslogs-multiline-pattern or 363 // awslogs-datetime-format options have been configured, multiline processing 364 // is enabled, where log messages are stored in an event buffer until a multiline 365 // pattern match is found, at which point the messages in the event buffer are 366 // pushed to CloudWatch logs as a single log event. Multline messages are processed 367 // according to the maximumBytesPerPut constraint, and the implementation only 368 // allows for messages to be buffered for a maximum of 2*batchPublishFrequency 369 // seconds. When events are ready to be processed for submission to CloudWatch 370 // Logs, the processEvents method is called. If a multiline pattern is not 371 // configured, log events are submitted to the processEvents method immediately. 372 func (l *logStream) collectBatch() { 373 timer := newTicker(batchPublishFrequency) 374 var events []wrappedEvent 375 var eventBuffer []byte 376 var eventBufferTimestamp int64 377 for { 378 select { 379 case t := <-timer.C: 380 // If event buffer is older than batch publish frequency flush the event buffer 381 if eventBufferTimestamp > 0 && len(eventBuffer) > 0 { 382 eventBufferAge := t.UnixNano()/int64(time.Millisecond) - eventBufferTimestamp 383 eventBufferExpired := eventBufferAge > int64(batchPublishFrequency)/int64(time.Millisecond) 384 eventBufferNegative := eventBufferAge < 0 385 if eventBufferExpired || eventBufferNegative { 386 events = l.processEvent(events, eventBuffer, eventBufferTimestamp) 387 } 388 } 389 l.publishBatch(events) 390 events = events[:0] 391 case msg, more := <-l.messages: 392 if !more { 393 // Flush event buffer 394 events = l.processEvent(events, eventBuffer, eventBufferTimestamp) 395 l.publishBatch(events) 396 return 397 } 398 if eventBufferTimestamp == 0 { 399 eventBufferTimestamp = msg.Timestamp.UnixNano() / int64(time.Millisecond) 400 } 401 unprocessedLine := msg.Line 402 if l.multilinePattern != nil { 403 if l.multilinePattern.Match(unprocessedLine) { 404 // This is a new log event so flush the current eventBuffer to events 405 events = l.processEvent(events, eventBuffer, eventBufferTimestamp) 406 eventBufferTimestamp = msg.Timestamp.UnixNano() / int64(time.Millisecond) 407 eventBuffer = eventBuffer[:0] 408 } 409 // If we will exceed max bytes per event flush the current event buffer before appending 410 if len(eventBuffer)+len(unprocessedLine) > maximumBytesPerEvent { 411 events = l.processEvent(events, eventBuffer, eventBufferTimestamp) 412 eventBuffer = eventBuffer[:0] 413 } 414 // Append new line 415 processedLine := append(unprocessedLine, "\n"...) 416 eventBuffer = append(eventBuffer, processedLine...) 417 logger.PutMessage(msg) 418 } else { 419 events = l.processEvent(events, unprocessedLine, msg.Timestamp.UnixNano()/int64(time.Millisecond)) 420 logger.PutMessage(msg) 421 } 422 } 423 } 424 } 425 426 // processEvent processes log events that are ready for submission to CloudWatch 427 // logs. Batching is performed on time- and size-bases. Time-based batching 428 // occurs at a 5 second interval (defined in the batchPublishFrequency const). 429 // Size-based batching is performed on the maximum number of events per batch 430 // (defined in maximumLogEventsPerPut) and the maximum number of total bytes in a 431 // batch (defined in maximumBytesPerPut). Log messages are split by the maximum 432 // bytes per event (defined in maximumBytesPerEvent). There is a fixed per-event 433 // byte overhead (defined in perEventBytes) which is accounted for in split- and 434 // batch-calculations. 435 func (l *logStream) processEvent(events []wrappedEvent, unprocessedLine []byte, timestamp int64) []wrappedEvent { 436 bytes := 0 437 for len(unprocessedLine) > 0 { 438 // Split line length so it does not exceed the maximum 439 lineBytes := len(unprocessedLine) 440 if lineBytes > maximumBytesPerEvent { 441 lineBytes = maximumBytesPerEvent 442 } 443 line := unprocessedLine[:lineBytes] 444 unprocessedLine = unprocessedLine[lineBytes:] 445 if (len(events) >= maximumLogEventsPerPut) || (bytes+lineBytes+perEventBytes > maximumBytesPerPut) { 446 // Publish an existing batch if it's already over the maximum number of events or if adding this 447 // event would push it over the maximum number of total bytes. 448 l.publishBatch(events) 449 events = events[:0] 450 bytes = 0 451 } 452 events = append(events, wrappedEvent{ 453 inputLogEvent: &cloudwatchlogs.InputLogEvent{ 454 Message: aws.String(string(line)), 455 Timestamp: aws.Int64(timestamp), 456 }, 457 insertOrder: len(events), 458 }) 459 bytes += (lineBytes + perEventBytes) 460 } 461 return events 462 } 463 464 // publishBatch calls PutLogEvents for a given set of InputLogEvents, 465 // accounting for sequencing requirements (each request must reference the 466 // sequence token returned by the previous request). 467 func (l *logStream) publishBatch(events []wrappedEvent) { 468 if len(events) == 0 { 469 return 470 } 471 472 // events in a batch must be sorted by timestamp 473 // see http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html 474 sort.Sort(byTimestamp(events)) 475 cwEvents := unwrapEvents(events) 476 477 nextSequenceToken, err := l.putLogEvents(cwEvents, l.sequenceToken) 478 479 if err != nil { 480 if awsErr, ok := err.(awserr.Error); ok { 481 if awsErr.Code() == dataAlreadyAcceptedCode { 482 // already submitted, just grab the correct sequence token 483 parts := strings.Split(awsErr.Message(), " ") 484 nextSequenceToken = &parts[len(parts)-1] 485 logrus.WithFields(logrus.Fields{ 486 "errorCode": awsErr.Code(), 487 "message": awsErr.Message(), 488 "logGroupName": l.logGroupName, 489 "logStreamName": l.logStreamName, 490 }).Info("Data already accepted, ignoring error") 491 err = nil 492 } else if awsErr.Code() == invalidSequenceTokenCode { 493 // sequence code is bad, grab the correct one and retry 494 parts := strings.Split(awsErr.Message(), " ") 495 token := parts[len(parts)-1] 496 nextSequenceToken, err = l.putLogEvents(cwEvents, &token) 497 } 498 } 499 } 500 if err != nil { 501 logrus.Error(err) 502 } else { 503 l.sequenceToken = nextSequenceToken 504 } 505 } 506 507 // putLogEvents wraps the PutLogEvents API 508 func (l *logStream) putLogEvents(events []*cloudwatchlogs.InputLogEvent, sequenceToken *string) (*string, error) { 509 input := &cloudwatchlogs.PutLogEventsInput{ 510 LogEvents: events, 511 SequenceToken: sequenceToken, 512 LogGroupName: aws.String(l.logGroupName), 513 LogStreamName: aws.String(l.logStreamName), 514 } 515 resp, err := l.client.PutLogEvents(input) 516 if err != nil { 517 if awsErr, ok := err.(awserr.Error); ok { 518 logrus.WithFields(logrus.Fields{ 519 "errorCode": awsErr.Code(), 520 "message": awsErr.Message(), 521 "origError": awsErr.OrigErr(), 522 "logGroupName": l.logGroupName, 523 "logStreamName": l.logStreamName, 524 }).Error("Failed to put log events") 525 } 526 return nil, err 527 } 528 return resp.NextSequenceToken, nil 529 } 530 531 // ValidateLogOpt looks for awslogs-specific log options awslogs-region, 532 // awslogs-group, awslogs-stream, awslogs-create-group, awslogs-datetime-format, 533 // awslogs-multiline-pattern 534 func ValidateLogOpt(cfg map[string]string) error { 535 for key := range cfg { 536 switch key { 537 case logGroupKey: 538 case logStreamKey: 539 case logCreateGroupKey: 540 case regionKey: 541 case tagKey: 542 case datetimeFormatKey: 543 case multilinePatternKey: 544 default: 545 return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name) 546 } 547 } 548 if cfg[logGroupKey] == "" { 549 return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey) 550 } 551 if cfg[logCreateGroupKey] != "" { 552 if _, err := strconv.ParseBool(cfg[logCreateGroupKey]); err != nil { 553 return fmt.Errorf("must specify valid value for log opt '%s': %v", logCreateGroupKey, err) 554 } 555 } 556 _, datetimeFormatKeyExists := cfg[datetimeFormatKey] 557 _, multilinePatternKeyExists := cfg[multilinePatternKey] 558 if datetimeFormatKeyExists && multilinePatternKeyExists { 559 return fmt.Errorf("you cannot configure log opt '%s' and '%s' at the same time", datetimeFormatKey, multilinePatternKey) 560 } 561 return nil 562 } 563 564 // Len returns the length of a byTimestamp slice. Len is required by the 565 // sort.Interface interface. 566 func (slice byTimestamp) Len() int { 567 return len(slice) 568 } 569 570 // Less compares two values in a byTimestamp slice by Timestamp. Less is 571 // required by the sort.Interface interface. 572 func (slice byTimestamp) Less(i, j int) bool { 573 iTimestamp, jTimestamp := int64(0), int64(0) 574 if slice != nil && slice[i].inputLogEvent.Timestamp != nil { 575 iTimestamp = *slice[i].inputLogEvent.Timestamp 576 } 577 if slice != nil && slice[j].inputLogEvent.Timestamp != nil { 578 jTimestamp = *slice[j].inputLogEvent.Timestamp 579 } 580 if iTimestamp == jTimestamp { 581 return slice[i].insertOrder < slice[j].insertOrder 582 } 583 return iTimestamp < jTimestamp 584 } 585 586 // Swap swaps two values in a byTimestamp slice with each other. Swap is 587 // required by the sort.Interface interface. 588 func (slice byTimestamp) Swap(i, j int) { 589 slice[i], slice[j] = slice[j], slice[i] 590 } 591 592 func unwrapEvents(events []wrappedEvent) []*cloudwatchlogs.InputLogEvent { 593 cwEvents := make([]*cloudwatchlogs.InputLogEvent, len(events)) 594 for i, input := range events { 595 cwEvents[i] = input.inputLogEvent 596 } 597 return cwEvents 598 }