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