github.com/fabiokung/docker@v0.11.2-0.20170222101415-4534dcd49497/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 "errors" 7 "fmt" 8 "os" 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 ) 28 29 const ( 30 name = "awslogs" 31 regionKey = "awslogs-region" 32 regionEnvKey = "AWS_REGION" 33 logGroupKey = "awslogs-group" 34 logStreamKey = "awslogs-stream" 35 logCreateGroupKey = "awslogs-create-group" 36 tagKey = "tag" 37 batchPublishFrequency = 5 * time.Second 38 39 // See: http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html 40 perEventBytes = 26 41 maximumBytesPerPut = 1048576 42 maximumLogEventsPerPut = 10000 43 44 // See: http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_limits.html 45 maximumBytesPerEvent = 262144 - perEventBytes 46 47 resourceAlreadyExistsCode = "ResourceAlreadyExistsException" 48 dataAlreadyAcceptedCode = "DataAlreadyAcceptedException" 49 invalidSequenceTokenCode = "InvalidSequenceTokenException" 50 resourceNotFoundCode = "ResourceNotFoundException" 51 52 userAgentHeader = "User-Agent" 53 ) 54 55 type logStream struct { 56 logStreamName string 57 logGroupName string 58 logCreateGroup bool 59 client api 60 messages chan *logger.Message 61 lock sync.RWMutex 62 closed bool 63 sequenceToken *string 64 } 65 66 type api interface { 67 CreateLogGroup(*cloudwatchlogs.CreateLogGroupInput) (*cloudwatchlogs.CreateLogGroupOutput, error) 68 CreateLogStream(*cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) 69 PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) 70 } 71 72 type regionFinder interface { 73 Region() (string, error) 74 } 75 76 type wrappedEvent struct { 77 inputLogEvent *cloudwatchlogs.InputLogEvent 78 insertOrder int 79 } 80 type byTimestamp []wrappedEvent 81 82 // init registers the awslogs driver 83 func init() { 84 if err := logger.RegisterLogDriver(name, New); err != nil { 85 logrus.Fatal(err) 86 } 87 if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil { 88 logrus.Fatal(err) 89 } 90 } 91 92 // New creates an awslogs logger using the configuration passed in on the 93 // context. Supported context configuration variables are awslogs-region, 94 // awslogs-group, awslogs-stream, and awslogs-create-group. When available, configuration is 95 // also taken from environment variables AWS_REGION, AWS_ACCESS_KEY_ID, 96 // AWS_SECRET_ACCESS_KEY, the shared credentials file (~/.aws/credentials), and 97 // the EC2 Instance Metadata Service. 98 func New(info logger.Info) (logger.Logger, error) { 99 logGroupName := info.Config[logGroupKey] 100 logStreamName, err := loggerutils.ParseLogTag(info, "{{.FullID}}") 101 if err != nil { 102 return nil, err 103 } 104 logCreateGroup := false 105 if info.Config[logCreateGroupKey] != "" { 106 logCreateGroup, err = strconv.ParseBool(info.Config[logCreateGroupKey]) 107 if err != nil { 108 return nil, err 109 } 110 } 111 112 if info.Config[logStreamKey] != "" { 113 logStreamName = info.Config[logStreamKey] 114 } 115 client, err := newAWSLogsClient(info) 116 if err != nil { 117 return nil, err 118 } 119 containerStream := &logStream{ 120 logStreamName: logStreamName, 121 logGroupName: logGroupName, 122 logCreateGroup: logCreateGroup, 123 client: client, 124 messages: make(chan *logger.Message, 4096), 125 } 126 err = containerStream.create() 127 if err != nil { 128 return nil, err 129 } 130 go containerStream.collectBatch() 131 132 return containerStream, nil 133 } 134 135 func parseLogGroup(info logger.Info, groupTemplate string) (string, error) { 136 tmpl, err := templates.NewParse("log-group", groupTemplate) 137 if err != nil { 138 return "", err 139 } 140 buf := new(bytes.Buffer) 141 if err := tmpl.Execute(buf, &info); err != nil { 142 return "", err 143 } 144 145 return buf.String(), nil 146 } 147 148 // newRegionFinder is a variable such that the implementation 149 // can be swapped out for unit tests. 150 var newRegionFinder = func() regionFinder { 151 return ec2metadata.New(session.New()) 152 } 153 154 // newAWSLogsClient creates the service client for Amazon CloudWatch Logs. 155 // Customizations to the default client from the SDK include a Docker-specific 156 // User-Agent string and automatic region detection using the EC2 Instance 157 // Metadata Service when region is otherwise unspecified. 158 func newAWSLogsClient(info logger.Info) (api, error) { 159 var region *string 160 if os.Getenv(regionEnvKey) != "" { 161 region = aws.String(os.Getenv(regionEnvKey)) 162 } 163 if info.Config[regionKey] != "" { 164 region = aws.String(info.Config[regionKey]) 165 } 166 if region == nil || *region == "" { 167 logrus.Info("Trying to get region from EC2 Metadata") 168 ec2MetadataClient := newRegionFinder() 169 r, err := ec2MetadataClient.Region() 170 if err != nil { 171 logrus.WithFields(logrus.Fields{ 172 "error": err, 173 }).Error("Could not get region from EC2 metadata, environment, or log option") 174 return nil, errors.New("Cannot determine region for awslogs driver") 175 } 176 region = &r 177 } 178 logrus.WithFields(logrus.Fields{ 179 "region": *region, 180 }).Debug("Created awslogs client") 181 182 client := cloudwatchlogs.New(session.New(), aws.NewConfig().WithRegion(*region)) 183 184 client.Handlers.Build.PushBackNamed(request.NamedHandler{ 185 Name: "DockerUserAgentHandler", 186 Fn: func(r *request.Request) { 187 currentAgent := r.HTTPRequest.Header.Get(userAgentHeader) 188 r.HTTPRequest.Header.Set(userAgentHeader, 189 fmt.Sprintf("Docker %s (%s) %s", 190 dockerversion.Version, runtime.GOOS, currentAgent)) 191 }, 192 }) 193 return client, nil 194 } 195 196 // Name returns the name of the awslogs logging driver 197 func (l *logStream) Name() string { 198 return name 199 } 200 201 // Log submits messages for logging by an instance of the awslogs logging driver 202 func (l *logStream) Log(msg *logger.Message) error { 203 l.lock.RLock() 204 defer l.lock.RUnlock() 205 if !l.closed { 206 l.messages <- msg 207 } 208 return nil 209 } 210 211 // Close closes the instance of the awslogs logging driver 212 func (l *logStream) Close() error { 213 l.lock.Lock() 214 defer l.lock.Unlock() 215 if !l.closed { 216 close(l.messages) 217 } 218 l.closed = true 219 return nil 220 } 221 222 // create creates log group and log stream for the instance of the awslogs logging driver 223 func (l *logStream) create() error { 224 if err := l.createLogStream(); err != nil { 225 if l.logCreateGroup { 226 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == resourceNotFoundCode { 227 if err := l.createLogGroup(); err != nil { 228 return err 229 } 230 return l.createLogStream() 231 } 232 } 233 return err 234 } 235 236 return nil 237 } 238 239 // createLogGroup creates a log group for the instance of the awslogs logging driver 240 func (l *logStream) createLogGroup() error { 241 if _, err := l.client.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{ 242 LogGroupName: aws.String(l.logGroupName), 243 }); err != nil { 244 if awsErr, ok := err.(awserr.Error); ok { 245 fields := logrus.Fields{ 246 "errorCode": awsErr.Code(), 247 "message": awsErr.Message(), 248 "origError": awsErr.OrigErr(), 249 "logGroupName": l.logGroupName, 250 "logCreateGroup": l.logCreateGroup, 251 } 252 if awsErr.Code() == resourceAlreadyExistsCode { 253 // Allow creation to succeed 254 logrus.WithFields(fields).Info("Log group already exists") 255 return nil 256 } 257 logrus.WithFields(fields).Error("Failed to create log group") 258 } 259 return err 260 } 261 return nil 262 } 263 264 // createLogStream creates a log stream for the instance of the awslogs logging driver 265 func (l *logStream) createLogStream() error { 266 input := &cloudwatchlogs.CreateLogStreamInput{ 267 LogGroupName: aws.String(l.logGroupName), 268 LogStreamName: aws.String(l.logStreamName), 269 } 270 271 _, err := l.client.CreateLogStream(input) 272 273 if err != nil { 274 if awsErr, ok := err.(awserr.Error); ok { 275 fields := logrus.Fields{ 276 "errorCode": awsErr.Code(), 277 "message": awsErr.Message(), 278 "origError": awsErr.OrigErr(), 279 "logGroupName": l.logGroupName, 280 "logStreamName": l.logStreamName, 281 } 282 if awsErr.Code() == resourceAlreadyExistsCode { 283 // Allow creation to succeed 284 logrus.WithFields(fields).Info("Log stream already exists") 285 return nil 286 } 287 logrus.WithFields(fields).Error("Failed to create log stream") 288 } 289 } 290 return err 291 } 292 293 // newTicker is used for time-based batching. newTicker is a variable such 294 // that the implementation can be swapped out for unit tests. 295 var newTicker = func(freq time.Duration) *time.Ticker { 296 return time.NewTicker(freq) 297 } 298 299 // collectBatch executes as a goroutine to perform batching of log events for 300 // submission to the log stream. Batching is performed on time- and size- 301 // bases. Time-based batching occurs at a 5 second interval (defined in the 302 // batchPublishFrequency const). Size-based batching is performed on the 303 // maximum number of events per batch (defined in maximumLogEventsPerPut) and 304 // the maximum number of total bytes in a batch (defined in 305 // maximumBytesPerPut). Log messages are split by the maximum bytes per event 306 // (defined in maximumBytesPerEvent). There is a fixed per-event byte overhead 307 // (defined in perEventBytes) which is accounted for in split- and batch- 308 // calculations. 309 func (l *logStream) collectBatch() { 310 timer := newTicker(batchPublishFrequency) 311 var events []wrappedEvent 312 bytes := 0 313 for { 314 select { 315 case <-timer.C: 316 l.publishBatch(events) 317 events = events[:0] 318 bytes = 0 319 case msg, more := <-l.messages: 320 if !more { 321 l.publishBatch(events) 322 return 323 } 324 unprocessedLine := msg.Line 325 for len(unprocessedLine) > 0 { 326 // Split line length so it does not exceed the maximum 327 lineBytes := len(unprocessedLine) 328 if lineBytes > maximumBytesPerEvent { 329 lineBytes = maximumBytesPerEvent 330 } 331 line := unprocessedLine[:lineBytes] 332 unprocessedLine = unprocessedLine[lineBytes:] 333 if (len(events) >= maximumLogEventsPerPut) || (bytes+lineBytes+perEventBytes > maximumBytesPerPut) { 334 // Publish an existing batch if it's already over the maximum number of events or if adding this 335 // event would push it over the maximum number of total bytes. 336 l.publishBatch(events) 337 events = events[:0] 338 bytes = 0 339 } 340 events = append(events, wrappedEvent{ 341 inputLogEvent: &cloudwatchlogs.InputLogEvent{ 342 Message: aws.String(string(line)), 343 Timestamp: aws.Int64(msg.Timestamp.UnixNano() / int64(time.Millisecond)), 344 }, 345 insertOrder: len(events), 346 }) 347 bytes += (lineBytes + perEventBytes) 348 } 349 logger.PutMessage(msg) 350 } 351 } 352 } 353 354 // publishBatch calls PutLogEvents for a given set of InputLogEvents, 355 // accounting for sequencing requirements (each request must reference the 356 // sequence token returned by the previous request). 357 func (l *logStream) publishBatch(events []wrappedEvent) { 358 if len(events) == 0 { 359 return 360 } 361 362 // events in a batch must be sorted by timestamp 363 // see http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html 364 sort.Sort(byTimestamp(events)) 365 cwEvents := unwrapEvents(events) 366 367 nextSequenceToken, err := l.putLogEvents(cwEvents, l.sequenceToken) 368 369 if err != nil { 370 if awsErr, ok := err.(awserr.Error); ok { 371 if awsErr.Code() == dataAlreadyAcceptedCode { 372 // already submitted, just grab the correct sequence token 373 parts := strings.Split(awsErr.Message(), " ") 374 nextSequenceToken = &parts[len(parts)-1] 375 logrus.WithFields(logrus.Fields{ 376 "errorCode": awsErr.Code(), 377 "message": awsErr.Message(), 378 "logGroupName": l.logGroupName, 379 "logStreamName": l.logStreamName, 380 }).Info("Data already accepted, ignoring error") 381 err = nil 382 } else if awsErr.Code() == invalidSequenceTokenCode { 383 // sequence code is bad, grab the correct one and retry 384 parts := strings.Split(awsErr.Message(), " ") 385 token := parts[len(parts)-1] 386 nextSequenceToken, err = l.putLogEvents(cwEvents, &token) 387 } 388 } 389 } 390 if err != nil { 391 logrus.Error(err) 392 } else { 393 l.sequenceToken = nextSequenceToken 394 } 395 } 396 397 // putLogEvents wraps the PutLogEvents API 398 func (l *logStream) putLogEvents(events []*cloudwatchlogs.InputLogEvent, sequenceToken *string) (*string, error) { 399 input := &cloudwatchlogs.PutLogEventsInput{ 400 LogEvents: events, 401 SequenceToken: sequenceToken, 402 LogGroupName: aws.String(l.logGroupName), 403 LogStreamName: aws.String(l.logStreamName), 404 } 405 resp, err := l.client.PutLogEvents(input) 406 if err != nil { 407 if awsErr, ok := err.(awserr.Error); ok { 408 logrus.WithFields(logrus.Fields{ 409 "errorCode": awsErr.Code(), 410 "message": awsErr.Message(), 411 "origError": awsErr.OrigErr(), 412 "logGroupName": l.logGroupName, 413 "logStreamName": l.logStreamName, 414 }).Error("Failed to put log events") 415 } 416 return nil, err 417 } 418 return resp.NextSequenceToken, nil 419 } 420 421 // ValidateLogOpt looks for awslogs-specific log options awslogs-region, 422 // awslogs-group, awslogs-stream, awslogs-create-group 423 func ValidateLogOpt(cfg map[string]string) error { 424 for key := range cfg { 425 switch key { 426 case logGroupKey: 427 case logStreamKey: 428 case logCreateGroupKey: 429 case regionKey: 430 case tagKey: 431 default: 432 return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name) 433 } 434 } 435 if cfg[logGroupKey] == "" { 436 return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey) 437 } 438 if cfg[logCreateGroupKey] != "" { 439 if _, err := strconv.ParseBool(cfg[logCreateGroupKey]); err != nil { 440 return fmt.Errorf("must specify valid value for log opt '%s': %v", logCreateGroupKey, err) 441 } 442 } 443 return nil 444 } 445 446 // Len returns the length of a byTimestamp slice. Len is required by the 447 // sort.Interface interface. 448 func (slice byTimestamp) Len() int { 449 return len(slice) 450 } 451 452 // Less compares two values in a byTimestamp slice by Timestamp. Less is 453 // required by the sort.Interface interface. 454 func (slice byTimestamp) Less(i, j int) bool { 455 iTimestamp, jTimestamp := int64(0), int64(0) 456 if slice != nil && slice[i].inputLogEvent.Timestamp != nil { 457 iTimestamp = *slice[i].inputLogEvent.Timestamp 458 } 459 if slice != nil && slice[j].inputLogEvent.Timestamp != nil { 460 jTimestamp = *slice[j].inputLogEvent.Timestamp 461 } 462 if iTimestamp == jTimestamp { 463 return slice[i].insertOrder < slice[j].insertOrder 464 } 465 return iTimestamp < jTimestamp 466 } 467 468 // Swap swaps two values in a byTimestamp slice with each other. Swap is 469 // required by the sort.Interface interface. 470 func (slice byTimestamp) Swap(i, j int) { 471 slice[i], slice[j] = slice[j], slice[i] 472 } 473 474 func unwrapEvents(events []wrappedEvent) []*cloudwatchlogs.InputLogEvent { 475 cwEvents := []*cloudwatchlogs.InputLogEvent{} 476 for _, input := range events { 477 cwEvents = append(cwEvents, input.inputLogEvent) 478 } 479 return cwEvents 480 }