github.com/walkingsparrow/docker@v1.4.2-0.20151218153551-b708a2249bfa/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 = &region
   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  }