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