diff --git a/daemon/logger/awslogs/cloudwatchlogs.go b/daemon/logger/awslogs/cloudwatchlogs.go index 55065c0f96..5b06969bd3 100644 --- a/daemon/logger/awslogs/cloudwatchlogs.go +++ b/daemon/logger/awslogs/cloudwatchlogs.go @@ -2,6 +2,7 @@ package awslogs import ( + "errors" "fmt" "os" "runtime" @@ -14,6 +15,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/docker/docker/daemon/logger" @@ -58,6 +60,10 @@ type api interface { PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) } +type regionFinder interface { + Region() (string, error) +} + type byTimestamp []*cloudwatchlogs.InputLogEvent // init registers the awslogs driver and sets the default region, if provided @@ -85,13 +91,17 @@ func New(ctx logger.Context) (logger.Logger, error) { if ctx.Config[logStreamKey] != "" { logStreamName = ctx.Config[logStreamKey] } + client, err := newAWSLogsClient(ctx) + if err != nil { + return nil, err + } containerStream := &logStream{ logStreamName: logStreamName, logGroupName: logGroupName, - client: newAWSLogsClient(ctx), + client: client, messages: make(chan *logger.Message, 4096), } - err := containerStream.create() + err = containerStream.create() if err != nil { return nil, err } @@ -100,13 +110,38 @@ func New(ctx logger.Context) (logger.Logger, error) { return containerStream, nil } -func newAWSLogsClient(ctx logger.Context) api { +// newRegionFinder is a variable such that the implementation +// can be swapped out for unit tests. +var newRegionFinder = func() regionFinder { + return ec2metadata.New(nil) +} + +// newAWSLogsClient creates the service client for Amazon CloudWatch Logs. +// Customizations to the default client from the SDK include a Docker-specific +// User-Agent string and automatic region detection using the EC2 Instance +// Metadata Service when region is otherwise unspecified. +func newAWSLogsClient(ctx logger.Context) (api, error) { config := defaults.DefaultConfig if ctx.Config[regionKey] != "" { config = defaults.DefaultConfig.Merge(&aws.Config{ Region: aws.String(ctx.Config[regionKey]), }) } + if config.Region == nil || *config.Region == "" { + logrus.Info("Trying to get region from EC2 Metadata") + ec2MetadataClient := newRegionFinder() + region, err := ec2MetadataClient.Region() + if err != nil { + logrus.WithFields(logrus.Fields{ + "error": err, + }).Error("Could not get region from EC2 metadata, environment, or log option") + return nil, errors.New("Cannot determine region for awslogs driver") + } + config.Region = ®ion + } + logrus.WithFields(logrus.Fields{ + "region": *config.Region, + }).Debug("Created awslogs client") client := cloudwatchlogs.New(config) client.Handlers.Build.PushBackNamed(request.NamedHandler{ @@ -118,7 +153,7 @@ func newAWSLogsClient(ctx logger.Context) api { version.VERSION, runtime.GOOS, currentAgent)) }, }) - return client + return client, nil } // Name returns the name of the awslogs logging driver @@ -312,12 +347,6 @@ func ValidateLogOpt(cfg map[string]string) error { if cfg[logGroupKey] == "" { return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey) } - if cfg[regionKey] == "" && os.Getenv(regionEnvKey) == "" { - return fmt.Errorf( - "must specify a value for environment variable '%s' or log opt '%s'", - regionEnvKey, - regionKey) - } return nil } diff --git a/daemon/logger/awslogs/cloudwatchlogs_test.go b/daemon/logger/awslogs/cloudwatchlogs_test.go index fdae93e448..806851d0ce 100644 --- a/daemon/logger/awslogs/cloudwatchlogs_test.go +++ b/daemon/logger/awslogs/cloudwatchlogs_test.go @@ -32,7 +32,10 @@ func TestNewAWSLogsClientUserAgentHandler(t *testing.T) { }, } - client := newAWSLogsClient(ctx) + client, err := newAWSLogsClient(ctx) + if err != nil { + t.Fatal(err) + } realClient, ok := client.(*cloudwatchlogs.CloudWatchLogs) if !ok { t.Fatal("Could not cast client to cloudwatchlogs.CloudWatchLogs") @@ -53,6 +56,25 @@ func TestNewAWSLogsClientUserAgentHandler(t *testing.T) { } } +func TestNewAWSLogsClientRegionDetect(t *testing.T) { + ctx := logger.Context{ + Config: map[string]string{}, + } + + mockMetadata := newMockMetadataClient() + newRegionFinder = func() regionFinder { + return mockMetadata + } + mockMetadata.regionResult <- ®ionResult{ + successResult: "us-east-1", + } + + _, err := newAWSLogsClient(ctx) + if err != nil { + t.Fatal(err) + } +} + func TestCreateSuccess(t *testing.T) { mockClient := newMockClient() stream := &logStream{ diff --git a/daemon/logger/awslogs/cwlogsiface_mock_test.go b/daemon/logger/awslogs/cwlogsiface_mock_test.go index 64ea1bc829..bfe6d74f72 100644 --- a/daemon/logger/awslogs/cwlogsiface_mock_test.go +++ b/daemon/logger/awslogs/cwlogsiface_mock_test.go @@ -49,6 +49,26 @@ func (m *mockcwlogsclient) PutLogEvents(input *cloudwatchlogs.PutLogEventsInput) return output.successResult, output.errorResult } +type mockmetadataclient struct { + regionResult chan *regionResult +} + +type regionResult struct { + successResult string + errorResult error +} + +func newMockMetadataClient() *mockmetadataclient { + return &mockmetadataclient{ + regionResult: make(chan *regionResult, 1), + } +} + +func (m *mockmetadataclient) Region() (string, error) { + output := <-m.regionResult + return output.successResult, output.errorResult +} + func test() { _ = &logStream{ client: newMockClient(), diff --git a/docs/reference/logging/awslogs.md b/docs/reference/logging/awslogs.md index 8e52288b7d..99f7db3da1 100644 --- a/docs/reference/logging/awslogs.md +++ b/docs/reference/logging/awslogs.md @@ -34,9 +34,10 @@ You can use the `--log-opt NAME=VALUE` flag to specify Amazon CloudWatch Logs lo ### awslogs-region -You must specify a region for the `awslogs` logging driver. You can specify the -region with either the `awslogs-region` log option or `AWS_REGION` environment -variable: +The `awslogs` logging driver sends your Docker logs to a specific region. Use +the `awslogs-region` log option or the `AWS_REGION` environment variable to set +the region. By default, if your Docker daemon is running on an EC2 instance +and no region is set, the driver uses the instance's region. docker run --log-driver=awslogs --log-opt awslogs-region=us-east-1 ...