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  }