Browse code

Add awslogs driver for Amazon CloudWatch Logs

Signed-off-by: Samuel Karp <skarp@amazon.com>

Samuel Karp authored on 2015/08/05 09:35:06
Showing 15 changed files
... ...
@@ -279,6 +279,7 @@ __docker_log_driver_options() {
279 279
 	local gelf_options="gelf-address gelf-tag"
280 280
 	local json_file_options="max-file max-size"
281 281
 	local syslog_options="syslog-address syslog-facility syslog-tag"
282
+	local awslogs_options="awslogs-region awslogs-group awslogs-stream"
282 283
 
283 284
 	case $(__docker_value_of_option --log-driver) in
284 285
 		'')
... ...
@@ -296,6 +297,9 @@ __docker_log_driver_options() {
296 296
 		syslog)
297 297
 			COMPREPLY=( $( compgen -W "$syslog_options" -S = -- "$cur" ) )
298 298
 			;;
299
+		awslogs)
300
+			COMPREPLY=( $( compgen -W "$awslogs_options" -S = -- "$cur" ) )
301
+			;;
299 302
 		*)
300 303
 			return
301 304
 			;;
... ...
@@ -238,7 +238,7 @@ __docker_subcommand() {
238 238
         "($help)--ipc=-[IPC namespace to use]:IPC namespace: "
239 239
         "($help)*--link=-[Add link to another container]:link:->link"
240 240
         "($help)*"{-l,--label=-}"[Set meta data on a container]:label: "
241
-        "($help)--log-driver=-[Default driver for container logs]:Logging driver:(json-file syslog journald gelf fluentd none)"
241
+        "($help)--log-driver=-[Default driver for container logs]:Logging driver:(json-file syslog journald gelf fluentd awslogs none)"
242 242
         "($help)*--log-opt=-[Log driver specific options]:log driver options: "
243 243
         "($help)*--lxc-conf=-[Add custom lxc options]:lxc options: "
244 244
         "($help)--mac-address=-[Container MAC address]:MAC address: "
... ...
@@ -617,7 +617,7 @@ _docker() {
617 617
         "($help)--ipv6[Enable IPv6 networking]" \
618 618
         "($help -l --log-level)"{-l,--log-level=-}"[Set the logging level]:level:(debug info warn error fatal)" \
619 619
         "($help)*--label=-[Set key=value labels to the daemon]:label: " \
620
-        "($help)--log-driver=-[Default driver for container logs]:Logging driver:(json-file syslog journald gelf fluentd none)" \
620
+        "($help)--log-driver=-[Default driver for container logs]:Logging driver:(json-file syslog journald gelf fluentd awslogs none)" \
621 621
         "($help)*--log-opt=-[Log driver specific options]:log driver options: " \
622 622
         "($help)--mtu=-[Set the containers network MTU]:mtu:(0 576 1420 1500 9000)" \
623 623
         "($help -p --pidfile)"{-p,--pidfile=-}"[Path to use for daemon PID file]:PID file:_files" \
... ...
@@ -3,6 +3,7 @@ package daemon
3 3
 import (
4 4
 	// Importing packages here only to make sure their init gets called and
5 5
 	// therefore they register themselves to the logdriver factory.
6
+	_ "github.com/docker/docker/daemon/logger/awslogs"
6 7
 	_ "github.com/docker/docker/daemon/logger/fluentd"
7 8
 	_ "github.com/docker/docker/daemon/logger/gelf"
8 9
 	_ "github.com/docker/docker/daemon/logger/journald"
... ...
@@ -3,5 +3,6 @@ package daemon
3 3
 import (
4 4
 	// Importing packages here only to make sure their init gets called and
5 5
 	// therefore they register themselves to the logdriver factory.
6
+	_ "github.com/docker/docker/daemon/logger/awslogs"
6 7
 	_ "github.com/docker/docker/daemon/logger/jsonfilelog"
7 8
 )
8 9
new file mode 100644
... ...
@@ -0,0 +1,326 @@
0
+// Package awslogs provides the logdriver for forwarding container logs to Amazon CloudWatch Logs
1
+package awslogs
2
+
3
+import (
4
+	"fmt"
5
+	"os"
6
+	"sort"
7
+	"strings"
8
+	"sync"
9
+	"time"
10
+
11
+	"github.com/aws/aws-sdk-go/aws"
12
+	"github.com/aws/aws-sdk-go/aws/awserr"
13
+	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
14
+	"github.com/docker/docker/daemon/logger"
15
+	"github.com/docker/docker/vendor/src/github.com/Sirupsen/logrus"
16
+)
17
+
18
+const (
19
+	name                  = "awslogs"
20
+	regionKey             = "awslogs-region"
21
+	regionEnvKey          = "AWS_REGION"
22
+	logGroupKey           = "awslogs-group"
23
+	logStreamKey          = "awslogs-stream"
24
+	batchPublishFrequency = 5 * time.Second
25
+
26
+	// See: http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
27
+	perEventBytes          = 26
28
+	maximumBytesPerPut     = 1048576
29
+	maximumLogEventsPerPut = 10000
30
+
31
+	// See: http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_limits.html
32
+	maximumBytesPerEvent = 262144 - perEventBytes
33
+
34
+	resourceAlreadyExistsCode = "ResourceAlreadyExistsException"
35
+	dataAlreadyAcceptedCode   = "DataAlreadyAcceptedException"
36
+	invalidSequenceTokenCode  = "InvalidSequenceTokenException"
37
+)
38
+
39
+type logStream struct {
40
+	logStreamName string
41
+	logGroupName  string
42
+	client        api
43
+	messages      chan *logger.Message
44
+	lock          sync.RWMutex
45
+	closed        bool
46
+	sequenceToken *string
47
+}
48
+
49
+type api interface {
50
+	CreateLogStream(*cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error)
51
+	PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error)
52
+}
53
+
54
+type byTimestamp []*cloudwatchlogs.InputLogEvent
55
+
56
+// init registers the awslogs driver and sets the default region, if provided
57
+func init() {
58
+	if os.Getenv(regionEnvKey) != "" {
59
+		aws.DefaultConfig.Region = aws.String(os.Getenv(regionEnvKey))
60
+	}
61
+	if err := logger.RegisterLogDriver(name, New); err != nil {
62
+		logrus.Fatal(err)
63
+	}
64
+	if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil {
65
+		logrus.Fatal(err)
66
+	}
67
+}
68
+
69
+// New creates an awslogs logger using the configuration passed in on the
70
+// context.  Supported context configuration variables are awslogs-region,
71
+// awslogs-group, and awslogs-stream.  When available, configuration is
72
+// also taken from environment variables AWS_REGION, AWS_ACCESS_KEY_ID,
73
+// AWS_SECRET_ACCESS_KEY, the shared credentials file (~/.aws/credentials), and
74
+// the EC2 Instance Metadata Service.
75
+func New(ctx logger.Context) (logger.Logger, error) {
76
+	logGroupName := ctx.Config[logGroupKey]
77
+	logStreamName := ctx.ContainerID
78
+	if ctx.Config[logStreamKey] != "" {
79
+		logStreamName = ctx.Config[logStreamKey]
80
+	}
81
+	config := aws.DefaultConfig
82
+	if ctx.Config[regionKey] != "" {
83
+		config = aws.DefaultConfig.Merge(&aws.Config{
84
+			Region: aws.String(ctx.Config[regionKey]),
85
+		})
86
+	}
87
+	containerStream := &logStream{
88
+		logStreamName: logStreamName,
89
+		logGroupName:  logGroupName,
90
+		client:        cloudwatchlogs.New(config),
91
+		messages:      make(chan *logger.Message, 4096),
92
+	}
93
+	err := containerStream.create()
94
+	if err != nil {
95
+		return nil, err
96
+	}
97
+	go containerStream.collectBatch()
98
+
99
+	return containerStream, nil
100
+}
101
+
102
+// Name returns the name of the awslogs logging driver
103
+func (l *logStream) Name() string {
104
+	return name
105
+}
106
+
107
+// Log submits messages for logging by an instance of the awslogs logging driver
108
+func (l *logStream) Log(msg *logger.Message) error {
109
+	l.lock.RLock()
110
+	defer l.lock.RUnlock()
111
+	if !l.closed {
112
+		l.messages <- msg
113
+	}
114
+	return nil
115
+}
116
+
117
+// Close closes the instance of the awslogs logging driver
118
+func (l *logStream) Close() error {
119
+	l.lock.Lock()
120
+	defer l.lock.Unlock()
121
+	if !l.closed {
122
+		close(l.messages)
123
+	}
124
+	l.closed = true
125
+	return nil
126
+}
127
+
128
+// create creates a log stream for the instance of the awslogs logging driver
129
+func (l *logStream) create() error {
130
+	input := &cloudwatchlogs.CreateLogStreamInput{
131
+		LogGroupName:  aws.String(l.logGroupName),
132
+		LogStreamName: aws.String(l.logStreamName),
133
+	}
134
+
135
+	_, err := l.client.CreateLogStream(input)
136
+
137
+	if err != nil {
138
+		if awsErr, ok := err.(awserr.Error); ok {
139
+			fields := logrus.Fields{
140
+				"errorCode":     awsErr.Code(),
141
+				"message":       awsErr.Message(),
142
+				"origError":     awsErr.OrigErr(),
143
+				"logGroupName":  l.logGroupName,
144
+				"logStreamName": l.logStreamName,
145
+			}
146
+			if awsErr.Code() == resourceAlreadyExistsCode {
147
+				// Allow creation to succeed
148
+				logrus.WithFields(fields).Info("Log stream already exists")
149
+				return nil
150
+			}
151
+			logrus.WithFields(fields).Error("Failed to create log stream")
152
+		}
153
+	}
154
+	return err
155
+}
156
+
157
+// newTicker is used for time-based batching.  newTicker is a variable such
158
+// that the implementation can be swapped out for unit tests.
159
+var newTicker = func(freq time.Duration) *time.Ticker {
160
+	return time.NewTicker(freq)
161
+}
162
+
163
+// collectBatch executes as a goroutine to perform batching of log events for
164
+// submission to the log stream.  Batching is performed on time- and size-
165
+// bases.  Time-based batching occurs at a 5 second interval (defined in the
166
+// batchPublishFrequency const).  Size-based batching is performed on the
167
+// maximum number of events per batch (defined in maximumLogEventsPerPut) and
168
+// the maximum number of total bytes in a batch (defined in
169
+// maximumBytesPerPut).  Log messages are split by the maximum bytes per event
170
+// (defined in maximumBytesPerEvent).  There is a fixed per-event byte overhead
171
+// (defined in perEventBytes) which is accounted for in split- and batch-
172
+// calculations.
173
+func (l *logStream) collectBatch() {
174
+	timer := newTicker(batchPublishFrequency)
175
+	var events []*cloudwatchlogs.InputLogEvent
176
+	bytes := 0
177
+	for {
178
+		select {
179
+		case <-timer.C:
180
+			l.publishBatch(events)
181
+			events = events[:0]
182
+			bytes = 0
183
+		case msg, more := <-l.messages:
184
+			if !more {
185
+				l.publishBatch(events)
186
+				return
187
+			}
188
+			unprocessedLine := msg.Line
189
+			for len(unprocessedLine) > 0 {
190
+				// Split line length so it does not exceed the maximum
191
+				lineBytes := len(unprocessedLine)
192
+				if lineBytes > maximumBytesPerEvent {
193
+					lineBytes = maximumBytesPerEvent
194
+				}
195
+				line := unprocessedLine[:lineBytes]
196
+				unprocessedLine = unprocessedLine[lineBytes:]
197
+				if (len(events) >= maximumLogEventsPerPut) || (bytes+lineBytes+perEventBytes > maximumBytesPerPut) {
198
+					// Publish an existing batch if it's already over the maximum number of events or if adding this
199
+					// event would push it over the maximum number of total bytes.
200
+					l.publishBatch(events)
201
+					events = events[:0]
202
+					bytes = 0
203
+				}
204
+				events = append(events, &cloudwatchlogs.InputLogEvent{
205
+					Message:   aws.String(string(line)),
206
+					Timestamp: aws.Int64(msg.Timestamp.UnixNano() / int64(time.Millisecond)),
207
+				})
208
+				bytes += (lineBytes + perEventBytes)
209
+			}
210
+		}
211
+	}
212
+}
213
+
214
+// publishBatch calls PutLogEvents for a given set of InputLogEvents,
215
+// accounting for sequencing requirements (each request must reference the
216
+// sequence token returned by the previous request).
217
+func (l *logStream) publishBatch(events []*cloudwatchlogs.InputLogEvent) {
218
+	if len(events) == 0 {
219
+		return
220
+	}
221
+
222
+	sort.Sort(byTimestamp(events))
223
+
224
+	nextSequenceToken, err := l.putLogEvents(events, l.sequenceToken)
225
+
226
+	if err != nil {
227
+		if awsErr, ok := err.(awserr.Error); ok {
228
+			if awsErr.Code() == dataAlreadyAcceptedCode {
229
+				// already submitted, just grab the correct sequence token
230
+				parts := strings.Split(awsErr.Message(), " ")
231
+				nextSequenceToken = &parts[len(parts)-1]
232
+				logrus.WithFields(logrus.Fields{
233
+					"errorCode":     awsErr.Code(),
234
+					"message":       awsErr.Message(),
235
+					"logGroupName":  l.logGroupName,
236
+					"logStreamName": l.logStreamName,
237
+				}).Info("Data already accepted, ignoring error")
238
+				err = nil
239
+			} else if awsErr.Code() == invalidSequenceTokenCode {
240
+				// sequence code is bad, grab the correct one and retry
241
+				parts := strings.Split(awsErr.Message(), " ")
242
+				token := parts[len(parts)-1]
243
+				nextSequenceToken, err = l.putLogEvents(events, &token)
244
+			}
245
+		}
246
+	}
247
+	if err != nil {
248
+		logrus.Error(err)
249
+	} else {
250
+		l.sequenceToken = nextSequenceToken
251
+	}
252
+}
253
+
254
+// putLogEvents wraps the PutLogEvents API
255
+func (l *logStream) putLogEvents(events []*cloudwatchlogs.InputLogEvent, sequenceToken *string) (*string, error) {
256
+	input := &cloudwatchlogs.PutLogEventsInput{
257
+		LogEvents:     events,
258
+		SequenceToken: sequenceToken,
259
+		LogGroupName:  aws.String(l.logGroupName),
260
+		LogStreamName: aws.String(l.logStreamName),
261
+	}
262
+	resp, err := l.client.PutLogEvents(input)
263
+	if err != nil {
264
+		if awsErr, ok := err.(awserr.Error); ok {
265
+			logrus.WithFields(logrus.Fields{
266
+				"errorCode":     awsErr.Code(),
267
+				"message":       awsErr.Message(),
268
+				"origError":     awsErr.OrigErr(),
269
+				"logGroupName":  l.logGroupName,
270
+				"logStreamName": l.logStreamName,
271
+			}).Error("Failed to put log events")
272
+		}
273
+		return nil, err
274
+	}
275
+	return resp.NextSequenceToken, nil
276
+}
277
+
278
+// ValidateLogOpt looks for awslogs-specific log options awslogs-region,
279
+// awslogs-group, and awslogs-stream
280
+func ValidateLogOpt(cfg map[string]string) error {
281
+	for key := range cfg {
282
+		switch key {
283
+		case logGroupKey:
284
+		case logStreamKey:
285
+		case regionKey:
286
+		default:
287
+			return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name)
288
+		}
289
+	}
290
+	if cfg[logGroupKey] == "" {
291
+		return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey)
292
+	}
293
+	if cfg[regionKey] == "" && os.Getenv(regionEnvKey) == "" {
294
+		return fmt.Errorf(
295
+			"must specify a value for environment variable '%s' or log opt '%s'",
296
+			regionEnvKey,
297
+			regionKey)
298
+	}
299
+	return nil
300
+}
301
+
302
+// Len returns the length of a byTimestamp slice.  Len is required by the
303
+// sort.Interface interface.
304
+func (slice byTimestamp) Len() int {
305
+	return len(slice)
306
+}
307
+
308
+// Less compares two values in a byTimestamp slice by Timestamp.  Less is
309
+// required by the sort.Interface interface.
310
+func (slice byTimestamp) Less(i, j int) bool {
311
+	iTimestamp, jTimestamp := int64(0), int64(0)
312
+	if slice != nil && slice[i].Timestamp != nil {
313
+		iTimestamp = *slice[i].Timestamp
314
+	}
315
+	if slice != nil && slice[j].Timestamp != nil {
316
+		jTimestamp = *slice[j].Timestamp
317
+	}
318
+	return iTimestamp < jTimestamp
319
+}
320
+
321
+// Swap swaps two values in a byTimestamp slice with each other.  Swap is
322
+// required by the sort.Interface interface.
323
+func (slice byTimestamp) Swap(i, j int) {
324
+	slice[i], slice[j] = slice[j], slice[i]
325
+}
0 326
new file mode 100644
... ...
@@ -0,0 +1,572 @@
0
+package awslogs
1
+
2
+import (
3
+	"errors"
4
+	"strings"
5
+	"testing"
6
+	"time"
7
+
8
+	"github.com/aws/aws-sdk-go/aws"
9
+	"github.com/aws/aws-sdk-go/aws/awserr"
10
+	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
11
+	"github.com/docker/docker/daemon/logger"
12
+)
13
+
14
+const (
15
+	groupName         = "groupName"
16
+	streamName        = "streamName"
17
+	sequenceToken     = "sequenceToken"
18
+	nextSequenceToken = "nextSequenceToken"
19
+	logline           = "this is a log line"
20
+)
21
+
22
+func TestCreateSuccess(t *testing.T) {
23
+	mockClient := newMockClient()
24
+	stream := &logStream{
25
+		client:        mockClient,
26
+		logGroupName:  groupName,
27
+		logStreamName: streamName,
28
+	}
29
+	mockClient.createLogStreamResult <- &createLogStreamResult{}
30
+
31
+	err := stream.create()
32
+
33
+	if err != nil {
34
+		t.Errorf("Received unexpected err: %v\n", err)
35
+	}
36
+	argument := <-mockClient.createLogStreamArgument
37
+	if argument.LogGroupName == nil {
38
+		t.Fatal("Expected non-nil LogGroupName")
39
+	}
40
+	if *argument.LogGroupName != groupName {
41
+		t.Errorf("Expected LogGroupName to be %s", groupName)
42
+	}
43
+	if argument.LogStreamName == nil {
44
+		t.Fatal("Expected non-nil LogGroupName")
45
+	}
46
+	if *argument.LogStreamName != streamName {
47
+		t.Errorf("Expected LogStreamName to be %s", streamName)
48
+	}
49
+}
50
+
51
+func TestCreateError(t *testing.T) {
52
+	mockClient := newMockClient()
53
+	stream := &logStream{
54
+		client: mockClient,
55
+	}
56
+	mockClient.createLogStreamResult <- &createLogStreamResult{
57
+		errorResult: errors.New("Error!"),
58
+	}
59
+
60
+	err := stream.create()
61
+
62
+	if err == nil {
63
+		t.Fatal("Expected non-nil err")
64
+	}
65
+}
66
+
67
+func TestCreateAlreadyExists(t *testing.T) {
68
+	mockClient := newMockClient()
69
+	stream := &logStream{
70
+		client: mockClient,
71
+	}
72
+	mockClient.createLogStreamResult <- &createLogStreamResult{
73
+		errorResult: awserr.New(resourceAlreadyExistsCode, "", nil),
74
+	}
75
+
76
+	err := stream.create()
77
+
78
+	if err != nil {
79
+		t.Fatal("Expected nil err")
80
+	}
81
+}
82
+
83
+func TestPublishBatchSuccess(t *testing.T) {
84
+	mockClient := newMockClient()
85
+	stream := &logStream{
86
+		client:        mockClient,
87
+		logGroupName:  groupName,
88
+		logStreamName: streamName,
89
+		sequenceToken: aws.String(sequenceToken),
90
+	}
91
+	mockClient.putLogEventsResult <- &putLogEventsResult{
92
+		successResult: &cloudwatchlogs.PutLogEventsOutput{
93
+			NextSequenceToken: aws.String(nextSequenceToken),
94
+		},
95
+	}
96
+
97
+	events := []*cloudwatchlogs.InputLogEvent{
98
+		{
99
+			Message: aws.String(logline),
100
+		},
101
+	}
102
+
103
+	stream.publishBatch(events)
104
+	if stream.sequenceToken == nil {
105
+		t.Fatal("Expected non-nil sequenceToken")
106
+	}
107
+	if *stream.sequenceToken != nextSequenceToken {
108
+		t.Errorf("Expected sequenceToken to be %s, but was %s", nextSequenceToken, *stream.sequenceToken)
109
+	}
110
+	argument := <-mockClient.putLogEventsArgument
111
+	if argument == nil {
112
+		t.Fatal("Expected non-nil PutLogEventsInput")
113
+	}
114
+	if argument.SequenceToken == nil {
115
+		t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken")
116
+	}
117
+	if *argument.SequenceToken != sequenceToken {
118
+		t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken)
119
+	}
120
+	if len(argument.LogEvents) != 1 {
121
+		t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
122
+	}
123
+	if argument.LogEvents[0] != events[0] {
124
+		t.Error("Expected event to equal input")
125
+	}
126
+}
127
+
128
+func TestPublishBatchError(t *testing.T) {
129
+	mockClient := newMockClient()
130
+	stream := &logStream{
131
+		client:        mockClient,
132
+		logGroupName:  groupName,
133
+		logStreamName: streamName,
134
+		sequenceToken: aws.String(sequenceToken),
135
+	}
136
+	mockClient.putLogEventsResult <- &putLogEventsResult{
137
+		errorResult: errors.New("Error!"),
138
+	}
139
+
140
+	events := []*cloudwatchlogs.InputLogEvent{
141
+		{
142
+			Message: aws.String(logline),
143
+		},
144
+	}
145
+
146
+	stream.publishBatch(events)
147
+	if stream.sequenceToken == nil {
148
+		t.Fatal("Expected non-nil sequenceToken")
149
+	}
150
+	if *stream.sequenceToken != sequenceToken {
151
+		t.Errorf("Expected sequenceToken to be %s, but was %s", sequenceToken, *stream.sequenceToken)
152
+	}
153
+}
154
+
155
+func TestPublishBatchInvalidSeqSuccess(t *testing.T) {
156
+	mockClient := newMockClientBuffered(2)
157
+	stream := &logStream{
158
+		client:        mockClient,
159
+		logGroupName:  groupName,
160
+		logStreamName: streamName,
161
+		sequenceToken: aws.String(sequenceToken),
162
+	}
163
+	mockClient.putLogEventsResult <- &putLogEventsResult{
164
+		errorResult: awserr.New(invalidSequenceTokenCode, "use token token", nil),
165
+	}
166
+	mockClient.putLogEventsResult <- &putLogEventsResult{
167
+		successResult: &cloudwatchlogs.PutLogEventsOutput{
168
+			NextSequenceToken: aws.String(nextSequenceToken),
169
+		},
170
+	}
171
+
172
+	events := []*cloudwatchlogs.InputLogEvent{
173
+		{
174
+			Message: aws.String(logline),
175
+		},
176
+	}
177
+
178
+	stream.publishBatch(events)
179
+	if stream.sequenceToken == nil {
180
+		t.Fatal("Expected non-nil sequenceToken")
181
+	}
182
+	if *stream.sequenceToken != nextSequenceToken {
183
+		t.Errorf("Expected sequenceToken to be %s, but was %s", nextSequenceToken, *stream.sequenceToken)
184
+	}
185
+
186
+	argument := <-mockClient.putLogEventsArgument
187
+	if argument == nil {
188
+		t.Fatal("Expected non-nil PutLogEventsInput")
189
+	}
190
+	if argument.SequenceToken == nil {
191
+		t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken")
192
+	}
193
+	if *argument.SequenceToken != sequenceToken {
194
+		t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken)
195
+	}
196
+	if len(argument.LogEvents) != 1 {
197
+		t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
198
+	}
199
+	if argument.LogEvents[0] != events[0] {
200
+		t.Error("Expected event to equal input")
201
+	}
202
+
203
+	argument = <-mockClient.putLogEventsArgument
204
+	if argument == nil {
205
+		t.Fatal("Expected non-nil PutLogEventsInput")
206
+	}
207
+	if argument.SequenceToken == nil {
208
+		t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken")
209
+	}
210
+	if *argument.SequenceToken != "token" {
211
+		t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", "token", *argument.SequenceToken)
212
+	}
213
+	if len(argument.LogEvents) != 1 {
214
+		t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
215
+	}
216
+	if argument.LogEvents[0] != events[0] {
217
+		t.Error("Expected event to equal input")
218
+	}
219
+}
220
+
221
+func TestPublishBatchAlreadyAccepted(t *testing.T) {
222
+	mockClient := newMockClient()
223
+	stream := &logStream{
224
+		client:        mockClient,
225
+		logGroupName:  groupName,
226
+		logStreamName: streamName,
227
+		sequenceToken: aws.String(sequenceToken),
228
+	}
229
+	mockClient.putLogEventsResult <- &putLogEventsResult{
230
+		errorResult: awserr.New(dataAlreadyAcceptedCode, "use token token", nil),
231
+	}
232
+
233
+	events := []*cloudwatchlogs.InputLogEvent{
234
+		{
235
+			Message: aws.String(logline),
236
+		},
237
+	}
238
+
239
+	stream.publishBatch(events)
240
+	if stream.sequenceToken == nil {
241
+		t.Fatal("Expected non-nil sequenceToken")
242
+	}
243
+	if *stream.sequenceToken != "token" {
244
+		t.Errorf("Expected sequenceToken to be %s, but was %s", "token", *stream.sequenceToken)
245
+	}
246
+
247
+	argument := <-mockClient.putLogEventsArgument
248
+	if argument == nil {
249
+		t.Fatal("Expected non-nil PutLogEventsInput")
250
+	}
251
+	if argument.SequenceToken == nil {
252
+		t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken")
253
+	}
254
+	if *argument.SequenceToken != sequenceToken {
255
+		t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken)
256
+	}
257
+	if len(argument.LogEvents) != 1 {
258
+		t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
259
+	}
260
+	if argument.LogEvents[0] != events[0] {
261
+		t.Error("Expected event to equal input")
262
+	}
263
+}
264
+
265
+func TestCollectBatchSimple(t *testing.T) {
266
+	mockClient := newMockClient()
267
+	stream := &logStream{
268
+		client:        mockClient,
269
+		logGroupName:  groupName,
270
+		logStreamName: streamName,
271
+		sequenceToken: aws.String(sequenceToken),
272
+		messages:      make(chan *logger.Message),
273
+	}
274
+	mockClient.putLogEventsResult <- &putLogEventsResult{
275
+		successResult: &cloudwatchlogs.PutLogEventsOutput{
276
+			NextSequenceToken: aws.String(nextSequenceToken),
277
+		},
278
+	}
279
+	ticks := make(chan time.Time)
280
+	newTicker = func(_ time.Duration) *time.Ticker {
281
+		return &time.Ticker{
282
+			C: ticks,
283
+		}
284
+	}
285
+
286
+	go stream.collectBatch()
287
+
288
+	stream.Log(&logger.Message{
289
+		Line:      []byte(logline),
290
+		Timestamp: time.Time{},
291
+	})
292
+
293
+	ticks <- time.Time{}
294
+	stream.Close()
295
+
296
+	argument := <-mockClient.putLogEventsArgument
297
+	if argument == nil {
298
+		t.Fatal("Expected non-nil PutLogEventsInput")
299
+	}
300
+	if len(argument.LogEvents) != 1 {
301
+		t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
302
+	}
303
+	if *argument.LogEvents[0].Message != logline {
304
+		t.Errorf("Expected message to be %s but was %s", logline, *argument.LogEvents[0].Message)
305
+	}
306
+}
307
+
308
+func TestCollectBatchTicker(t *testing.T) {
309
+	mockClient := newMockClient()
310
+	stream := &logStream{
311
+		client:        mockClient,
312
+		logGroupName:  groupName,
313
+		logStreamName: streamName,
314
+		sequenceToken: aws.String(sequenceToken),
315
+		messages:      make(chan *logger.Message),
316
+	}
317
+	mockClient.putLogEventsResult <- &putLogEventsResult{
318
+		successResult: &cloudwatchlogs.PutLogEventsOutput{
319
+			NextSequenceToken: aws.String(nextSequenceToken),
320
+		},
321
+	}
322
+	ticks := make(chan time.Time)
323
+	newTicker = func(_ time.Duration) *time.Ticker {
324
+		return &time.Ticker{
325
+			C: ticks,
326
+		}
327
+	}
328
+
329
+	go stream.collectBatch()
330
+
331
+	stream.Log(&logger.Message{
332
+		Line:      []byte(logline + " 1"),
333
+		Timestamp: time.Time{},
334
+	})
335
+	stream.Log(&logger.Message{
336
+		Line:      []byte(logline + " 2"),
337
+		Timestamp: time.Time{},
338
+	})
339
+
340
+	ticks <- time.Time{}
341
+
342
+	// Verify first batch
343
+	argument := <-mockClient.putLogEventsArgument
344
+	if argument == nil {
345
+		t.Fatal("Expected non-nil PutLogEventsInput")
346
+	}
347
+	if len(argument.LogEvents) != 2 {
348
+		t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents))
349
+	}
350
+	if *argument.LogEvents[0].Message != logline+" 1" {
351
+		t.Errorf("Expected message to be %s but was %s", logline+" 1", *argument.LogEvents[0].Message)
352
+	}
353
+	if *argument.LogEvents[1].Message != logline+" 2" {
354
+		t.Errorf("Expected message to be %s but was %s", logline+" 2", *argument.LogEvents[0].Message)
355
+	}
356
+
357
+	stream.Log(&logger.Message{
358
+		Line:      []byte(logline + " 3"),
359
+		Timestamp: time.Time{},
360
+	})
361
+
362
+	ticks <- time.Time{}
363
+	argument = <-mockClient.putLogEventsArgument
364
+	if argument == nil {
365
+		t.Fatal("Expected non-nil PutLogEventsInput")
366
+	}
367
+	if len(argument.LogEvents) != 1 {
368
+		t.Errorf("Expected LogEvents to contain 1 elements, but contains %d", len(argument.LogEvents))
369
+	}
370
+	if *argument.LogEvents[0].Message != logline+" 3" {
371
+		t.Errorf("Expected message to be %s but was %s", logline+" 3", *argument.LogEvents[0].Message)
372
+	}
373
+
374
+	stream.Close()
375
+
376
+}
377
+
378
+func TestCollectBatchClose(t *testing.T) {
379
+	mockClient := newMockClient()
380
+	stream := &logStream{
381
+		client:        mockClient,
382
+		logGroupName:  groupName,
383
+		logStreamName: streamName,
384
+		sequenceToken: aws.String(sequenceToken),
385
+		messages:      make(chan *logger.Message),
386
+	}
387
+	mockClient.putLogEventsResult <- &putLogEventsResult{
388
+		successResult: &cloudwatchlogs.PutLogEventsOutput{
389
+			NextSequenceToken: aws.String(nextSequenceToken),
390
+		},
391
+	}
392
+	var ticks = make(chan time.Time)
393
+	newTicker = func(_ time.Duration) *time.Ticker {
394
+		return &time.Ticker{
395
+			C: ticks,
396
+		}
397
+	}
398
+
399
+	go stream.collectBatch()
400
+
401
+	stream.Log(&logger.Message{
402
+		Line:      []byte(logline),
403
+		Timestamp: time.Time{},
404
+	})
405
+
406
+	// no ticks
407
+	stream.Close()
408
+
409
+	argument := <-mockClient.putLogEventsArgument
410
+	if argument == nil {
411
+		t.Fatal("Expected non-nil PutLogEventsInput")
412
+	}
413
+	if len(argument.LogEvents) != 1 {
414
+		t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
415
+	}
416
+	if *argument.LogEvents[0].Message != logline {
417
+		t.Errorf("Expected message to be %s but was %s", logline, *argument.LogEvents[0].Message)
418
+	}
419
+}
420
+
421
+func TestCollectBatchLineSplit(t *testing.T) {
422
+	mockClient := newMockClient()
423
+	stream := &logStream{
424
+		client:        mockClient,
425
+		logGroupName:  groupName,
426
+		logStreamName: streamName,
427
+		sequenceToken: aws.String(sequenceToken),
428
+		messages:      make(chan *logger.Message),
429
+	}
430
+	mockClient.putLogEventsResult <- &putLogEventsResult{
431
+		successResult: &cloudwatchlogs.PutLogEventsOutput{
432
+			NextSequenceToken: aws.String(nextSequenceToken),
433
+		},
434
+	}
435
+	var ticks = make(chan time.Time)
436
+	newTicker = func(_ time.Duration) *time.Ticker {
437
+		return &time.Ticker{
438
+			C: ticks,
439
+		}
440
+	}
441
+
442
+	go stream.collectBatch()
443
+
444
+	longline := strings.Repeat("A", maximumBytesPerEvent)
445
+	stream.Log(&logger.Message{
446
+		Line:      []byte(longline + "B"),
447
+		Timestamp: time.Time{},
448
+	})
449
+
450
+	// no ticks
451
+	stream.Close()
452
+
453
+	argument := <-mockClient.putLogEventsArgument
454
+	if argument == nil {
455
+		t.Fatal("Expected non-nil PutLogEventsInput")
456
+	}
457
+	if len(argument.LogEvents) != 2 {
458
+		t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents))
459
+	}
460
+	if *argument.LogEvents[0].Message != longline {
461
+		t.Errorf("Expected message to be %s but was %s", longline, *argument.LogEvents[0].Message)
462
+	}
463
+	if *argument.LogEvents[1].Message != "B" {
464
+		t.Errorf("Expected message to be %s but was %s", "B", *argument.LogEvents[1].Message)
465
+	}
466
+}
467
+
468
+func TestCollectBatchMaxEvents(t *testing.T) {
469
+	mockClient := newMockClientBuffered(1)
470
+	stream := &logStream{
471
+		client:        mockClient,
472
+		logGroupName:  groupName,
473
+		logStreamName: streamName,
474
+		sequenceToken: aws.String(sequenceToken),
475
+		messages:      make(chan *logger.Message),
476
+	}
477
+	mockClient.putLogEventsResult <- &putLogEventsResult{
478
+		successResult: &cloudwatchlogs.PutLogEventsOutput{
479
+			NextSequenceToken: aws.String(nextSequenceToken),
480
+		},
481
+	}
482
+	var ticks = make(chan time.Time)
483
+	newTicker = func(_ time.Duration) *time.Ticker {
484
+		return &time.Ticker{
485
+			C: ticks,
486
+		}
487
+	}
488
+
489
+	go stream.collectBatch()
490
+
491
+	line := "A"
492
+	for i := 0; i <= maximumLogEventsPerPut; i++ {
493
+		stream.Log(&logger.Message{
494
+			Line:      []byte(line),
495
+			Timestamp: time.Time{},
496
+		})
497
+	}
498
+
499
+	// no ticks
500
+	stream.Close()
501
+
502
+	argument := <-mockClient.putLogEventsArgument
503
+	if argument == nil {
504
+		t.Fatal("Expected non-nil PutLogEventsInput")
505
+	}
506
+	if len(argument.LogEvents) != maximumLogEventsPerPut {
507
+		t.Errorf("Expected LogEvents to contain %d elements, but contains %d", maximumLogEventsPerPut, len(argument.LogEvents))
508
+	}
509
+
510
+	argument = <-mockClient.putLogEventsArgument
511
+	if argument == nil {
512
+		t.Fatal("Expected non-nil PutLogEventsInput")
513
+	}
514
+	if len(argument.LogEvents) != 1 {
515
+		t.Errorf("Expected LogEvents to contain %d elements, but contains %d", 1, len(argument.LogEvents))
516
+	}
517
+}
518
+
519
+func TestCollectBatchMaxTotalBytes(t *testing.T) {
520
+	mockClient := newMockClientBuffered(1)
521
+	stream := &logStream{
522
+		client:        mockClient,
523
+		logGroupName:  groupName,
524
+		logStreamName: streamName,
525
+		sequenceToken: aws.String(sequenceToken),
526
+		messages:      make(chan *logger.Message),
527
+	}
528
+	mockClient.putLogEventsResult <- &putLogEventsResult{
529
+		successResult: &cloudwatchlogs.PutLogEventsOutput{
530
+			NextSequenceToken: aws.String(nextSequenceToken),
531
+		},
532
+	}
533
+	var ticks = make(chan time.Time)
534
+	newTicker = func(_ time.Duration) *time.Ticker {
535
+		return &time.Ticker{
536
+			C: ticks,
537
+		}
538
+	}
539
+
540
+	go stream.collectBatch()
541
+
542
+	longline := strings.Repeat("A", maximumBytesPerPut)
543
+	stream.Log(&logger.Message{
544
+		Line:      []byte(longline + "B"),
545
+		Timestamp: time.Time{},
546
+	})
547
+
548
+	// no ticks
549
+	stream.Close()
550
+
551
+	argument := <-mockClient.putLogEventsArgument
552
+	if argument == nil {
553
+		t.Fatal("Expected non-nil PutLogEventsInput")
554
+	}
555
+	bytes := 0
556
+	for _, event := range argument.LogEvents {
557
+		bytes += len(*event.Message)
558
+	}
559
+	if bytes > maximumBytesPerPut {
560
+		t.Errorf("Expected <= %d bytes but was %d", maximumBytesPerPut, bytes)
561
+	}
562
+
563
+	argument = <-mockClient.putLogEventsArgument
564
+	if len(argument.LogEvents) != 1 {
565
+		t.Errorf("Expected LogEvents to contain 1 elements, but contains %d", len(argument.LogEvents))
566
+	}
567
+	message := *argument.LogEvents[0].Message
568
+	if message[len(message)-1:] != "B" {
569
+		t.Errorf("Expected message to be %s but was %s", "B", message[len(message)-1:])
570
+	}
571
+}
0 572
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package awslogs
1
+
2
+import "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
3
+
4
+type mockcwlogsclient struct {
5
+	createLogStreamArgument chan *cloudwatchlogs.CreateLogStreamInput
6
+	createLogStreamResult   chan *createLogStreamResult
7
+	putLogEventsArgument    chan *cloudwatchlogs.PutLogEventsInput
8
+	putLogEventsResult      chan *putLogEventsResult
9
+}
10
+
11
+type createLogStreamResult struct {
12
+	successResult *cloudwatchlogs.CreateLogStreamOutput
13
+	errorResult   error
14
+}
15
+
16
+type putLogEventsResult struct {
17
+	successResult *cloudwatchlogs.PutLogEventsOutput
18
+	errorResult   error
19
+}
20
+
21
+func newMockClient() *mockcwlogsclient {
22
+	return &mockcwlogsclient{
23
+		createLogStreamArgument: make(chan *cloudwatchlogs.CreateLogStreamInput, 1),
24
+		createLogStreamResult:   make(chan *createLogStreamResult, 1),
25
+		putLogEventsArgument:    make(chan *cloudwatchlogs.PutLogEventsInput, 1),
26
+		putLogEventsResult:      make(chan *putLogEventsResult, 1),
27
+	}
28
+}
29
+
30
+func newMockClientBuffered(buflen int) *mockcwlogsclient {
31
+	return &mockcwlogsclient{
32
+		createLogStreamArgument: make(chan *cloudwatchlogs.CreateLogStreamInput, buflen),
33
+		createLogStreamResult:   make(chan *createLogStreamResult, buflen),
34
+		putLogEventsArgument:    make(chan *cloudwatchlogs.PutLogEventsInput, buflen),
35
+		putLogEventsResult:      make(chan *putLogEventsResult, buflen),
36
+	}
37
+}
38
+
39
+func (m *mockcwlogsclient) CreateLogStream(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) {
40
+	m.createLogStreamArgument <- input
41
+	output := <-m.createLogStreamResult
42
+	return output.successResult, output.errorResult
43
+}
44
+
45
+func (m *mockcwlogsclient) PutLogEvents(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) {
46
+	m.putLogEventsArgument <- input
47
+	output := <-m.putLogEventsResult
48
+	return output.successResult, output.errorResult
49
+}
50
+
51
+func test() {
52
+	_ = &logStream{
53
+		client: newMockClient(),
54
+	}
55
+}
... ...
@@ -298,7 +298,7 @@ Json Parameters:
298 298
         systems, such as SELinux.
299 299
     -   **LogConfig** - Log configuration for the container, specified as a JSON object in the form
300 300
           `{ "Type": "<driver_name>", "Config": {"key1": "val1"}}`.
301
-          Available types: `json-file`, `syslog`, `journald`, `gelf`, `none`.
301
+          Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `none`.
302 302
           `json-file` logging driver.
303 303
     -   **CgroupParent** - Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist.
304 304
     -   **VolumeDriver** - Driver that this container users to mount volumes.
305 305
new file mode 100644
... ...
@@ -0,0 +1,90 @@
0
+<!--[metadata]>
1
+title = "Amazon CloudWatch Logs logging driver"
2
+description = "Describes how to use the Amazon CloudWatch Logs logging driver."
3
+keywords = ["AWS, Amazon, CloudWatch, logging, driver"]
4
+[menu.main]
5
+parent = "smn_logging"
6
+<![end-metadata]-->
7
+
8
+# Amazon CloudWatch Logs logging driver
9
+
10
+The `awslogs` logging driver sends container logs to
11
+[Amazon CloudWatch Logs](https://aws.amazon.com/cloudwatch/details/#log-monitoring).
12
+Log entries can be retrieved through the [AWS Management
13
+Console](https://console.aws.amazon.com/cloudwatch/home#logs:) or the [AWS SDKs
14
+and Command Line Tools](http://docs.aws.amazon.com/cli/latest/reference/logs/index.html).
15
+
16
+## Usage
17
+
18
+You can configure the default logging driver by passing the `--log-driver`
19
+option to the Docker daemon:
20
+
21
+    docker --log-driver=awslogs
22
+
23
+You can set the logging driver for a specific container by using the
24
+`--log-driver` option to `docker run`:
25
+
26
+    docker run --log-driver=awslogs ...
27
+
28
+## Amazon CloudWatch Logs options
29
+
30
+You can use the `--log-opt NAME=VALUE` flag to specify Amazon CloudWatch Logs logging driver options.
31
+
32
+### awslogs-region
33
+
34
+You must specify a region for the `awslogs` logging driver. You can specify the
35
+region with either the `awslogs-region` log option or `AWS_REGION` environment
36
+variable:
37
+
38
+    docker run --log-driver=awslogs --log-opt awslogs-region=us-east-1 ...
39
+
40
+### awslogs-group
41
+
42
+You must specify a
43
+[log group](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/WhatIsCloudWatchLogs.html)
44
+for the `awslogs` logging driver.  You can specify the log group with the
45
+`awslogs-group` log option:
46
+
47
+    docker run --log-driver=awslogs --log-opt awslogs-region=us-east-1 --log-opt awslogs-group=myLogGroup ...
48
+
49
+### awslogs-stream
50
+
51
+To configure which
52
+[log stream](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/WhatIsCloudWatchLogs.html)
53
+should be used, you can specify the `awslogs-stream` log option.  If not
54
+specified, the container ID is used as the log stream.
55
+
56
+> **Note:**
57
+> Log streams within a given log group should only be used by one container
58
+> at a time.  Using the same log stream for multiple containers concurrently
59
+> can cause reduced logging performance.
60
+
61
+## Credentials
62
+
63
+You must provide AWS credentials to the Docker daemon to use the `awslogs`
64
+logging driver. You can provide these credentials with the `AWS_ACCESS_KEY_ID`,
65
+`AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` environment variables, the
66
+default AWS shared credentials file (`~/.aws/credentials` of the root user), or
67
+(if you are running the Docker daemon on an Amazon EC2 instance) the Amazon EC2
68
+instance profile.
69
+
70
+Credentials must have a policy applied that allows the `logs:CreateLogStream`
71
+and `logs:PutLogEvents` actions, as shown in the following example.
72
+
73
+    {
74
+      "Version": "2012-10-17",
75
+      "Statement": [
76
+        {
77
+          "Action": [
78
+            "logs:CreateLogStream",
79
+            "logs:PutLogEvents"
80
+          ],
81
+          "Effect": "Allow",
82
+          "Resource": "*"
83
+        }
84
+      ]
85
+    }
86
+
87
+
... ...
@@ -15,4 +15,5 @@ weight=8
15 15
 
16 16
 * [Configuring logging drivers](overview)
17 17
 * [Fluentd logging driver](fluentd)
18
-* [Journald logging driver](journald)
19 18
\ No newline at end of file
19
+* [Journald logging driver](journald)
20
+* [Amazon CloudWatch Logs logging driver](awslogs)
... ...
@@ -23,6 +23,7 @@ container's logging driver. The following options are supported:
23 23
 | `journald`  | Journald logging driver for Docker. Writes log messages to `journald`.                                                        |
24 24
 | `gelf`      | Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint likeGraylog or Logstash. |
25 25
 | `fluentd`   | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input).                                          |
26
+| `awslogs`   | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs.                              |
26 27
 
27 28
 The `docker logs`command is available only for the `json-file` logging driver.  
28 29
 
... ...
@@ -128,3 +129,15 @@ For example, to specify both additional options:
128 128
 If container cannot connect to the Fluentd daemon on the specified address,
129 129
 the container stops immediately. For detailed information on working with this
130 130
 logging driver, see [the fluentd logging driver](/reference/logging/fluentd/)
131
+
132
+## Specify Amazon CloudWatch Logs options
133
+
134
+The Amazon CloudWatch Logs logging driver supports the following options:
135
+
136
+    --log-opt awslogs-region=<aws_region>
137
+    --log-opt awslogs-group=<log_group_name>
138
+    --log-opt awslogs-stream=<log_stream_name>
139
+
140
+
141
+For detailed information on working with this logging driver, see [the awslogs logging driver](/reference/logging/awslogs/)
142
+reference documentation.
... ...
@@ -1011,6 +1011,7 @@ container's logging driver. The following options are supported:
1011 1011
 | `journald`  | Journald logging driver for Docker. Writes log messages to `journald`.                                                        |
1012 1012
 | `gelf`      | Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint likeGraylog or Logstash. |
1013 1013
 | `fluentd`   | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input).                                          |
1014
+| `awslogs`   | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs                               |
1014 1015
 
1015 1016
 	The `docker logs`command is available only for the `json-file` logging
1016 1017
 driver.  For detailed information on working with logging drivers, see
... ...
@@ -168,7 +168,7 @@ millions of trillions.
168 168
    Add link to another container in the form of <name or id>:alias or just
169 169
    <name or id> in which case the alias will match the name.
170 170
 
171
-**--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*none*"
171
+**--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*"
172 172
   Logging driver for container. Default is defined by daemon `--log-driver` flag.
173 173
   **Warning**: `docker logs` command works only for `json-file` logging driver.
174 174
 
... ...
@@ -268,7 +268,7 @@ which interface and port to use.
268 268
 **--lxc-conf**=[]
269 269
    (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"
270 270
 
271
-**--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*none*"
271
+**--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*"
272 272
   Logging driver for container. Default is defined by daemon `--log-driver` flag.
273 273
   **Warning**: `docker logs` command works only for `json-file` logging driver.
274 274
 
... ...
@@ -119,7 +119,7 @@ unix://[/path/to/socket] to use.
119 119
 **--label**="[]"
120 120
   Set key=value labels to the daemon (displayed in `docker info`)
121 121
 
122
-**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*none*"
122
+**--log-driver**="*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*"
123 123
   Default driver for container logs. Default is `json-file`.
124 124
   **Warning**: `docker logs` command works only for `json-file` logging driver.
125 125