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