Signed-off-by: Samuel Karp <skarp@amazon.com>
| ... | ... |
@@ -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 |
|