Merge pull request #42838 from sanjams2/42731-development

Add an option to specify log format for awslogs driver
diff --git a/daemon/logger/awslogs/cloudwatchlogs.go b/daemon/logger/awslogs/cloudwatchlogs.go
index dce0940..0ed42e6 100644
--- a/daemon/logger/awslogs/cloudwatchlogs.go
+++ b/daemon/logger/awslogs/cloudwatchlogs.go
@@ -42,6 +42,7 @@
 	credentialsEndpointKey = "awslogs-credentials-endpoint"
 	forceFlushIntervalKey  = "awslogs-force-flush-interval-seconds"
 	maxBufferedEventsKey   = "awslogs-max-buffered-events"
+	logFormatKey           = "awslogs-format"
 
 	defaultForceFlushInterval = 5 * time.Second
 	defaultMaxBufferedEvents  = 4096
@@ -66,6 +67,10 @@
 	credentialsEndpoint = "http://169.254.170.2"
 
 	userAgentHeader = "User-Agent"
+
+	// See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
+	logsFormatHeader = "x-amzn-logs-format"
+	jsonEmfLogFormat = "json/emf"
 )
 
 type logStream struct {
@@ -404,6 +409,16 @@
 					dockerversion.Version, runtime.GOOS, currentAgent))
 		},
 	})
+
+	if info.Config[logFormatKey] != "" {
+		client.Handlers.Build.PushBackNamed(request.NamedHandler{
+			Name: "LogFormatHeaderHandler",
+			Fn: func(req *request.Request) {
+				req.HTTPRequest.Header.Set(logsFormatHeader, info.Config[logFormatKey])
+			},
+		})
+	}
+
 	return client, nil
 }
 
@@ -755,6 +770,7 @@
 		case credentialsEndpointKey:
 		case forceFlushIntervalKey:
 		case maxBufferedEventsKey:
+		case logFormatKey:
 		default:
 			return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name)
 		}
@@ -782,6 +798,17 @@
 	if datetimeFormatKeyExists && multilinePatternKeyExists {
 		return fmt.Errorf("you cannot configure log opt '%s' and '%s' at the same time", datetimeFormatKey, multilinePatternKey)
 	}
+
+	if cfg[logFormatKey] != "" {
+		// For now, only the "json/emf" log format is supported
+		if cfg[logFormatKey] != jsonEmfLogFormat {
+			return fmt.Errorf("unsupported log format '%s'", cfg[logFormatKey])
+		}
+		if datetimeFormatKeyExists || multilinePatternKeyExists {
+			return fmt.Errorf("you cannot configure log opt '%s' or '%s' when log opt '%s' is set to '%s'", datetimeFormatKey, multilinePatternKey, logFormatKey, jsonEmfLogFormat)
+		}
+	}
+
 	return nil
 }
 
diff --git a/daemon/logger/awslogs/cloudwatchlogs_test.go b/daemon/logger/awslogs/cloudwatchlogs_test.go
index ed1465b..28b521d 100644
--- a/daemon/logger/awslogs/cloudwatchlogs_test.go
+++ b/daemon/logger/awslogs/cloudwatchlogs_test.go
@@ -147,6 +147,48 @@
 	}
 }
 
+func TestNewAWSLogsClientLogFormatHeaderHandler(t *testing.T) {
+	tests := []struct {
+		logFormat           string
+		expectedHeaderValue string
+	}{
+		{
+			logFormat:           jsonEmfLogFormat,
+			expectedHeaderValue: "json/emf",
+		},
+		{
+			logFormat:           "",
+			expectedHeaderValue: "",
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.logFormat, func(t *testing.T) {
+			info := logger.Info{
+				Config: map[string]string{
+					regionKey:    "us-east-1",
+					logFormatKey: tc.logFormat,
+				},
+			}
+
+			client, err := newAWSLogsClient(info)
+			assert.NilError(t, err)
+
+			realClient, ok := client.(*cloudwatchlogs.CloudWatchLogs)
+			assert.Check(t, ok, "Could not cast client to cloudwatchlogs.CloudWatchLogs")
+
+			buildHandlerList := realClient.Handlers.Build
+			request := &request.Request{
+				HTTPRequest: &http.Request{
+					Header: http.Header{},
+				},
+			}
+			buildHandlerList.Run(request)
+			logFormatHeaderVal := request.HTTPRequest.Header.Get("x-amzn-logs-format")
+			assert.Equal(t, tc.expectedHeaderValue, logFormatHeaderVal)
+		})
+	}
+}
+
 func TestNewAWSLogsClientAWSLogsEndpoint(t *testing.T) {
 	endpoint := "mock-endpoint"
 	info := logger.Info{
@@ -1559,6 +1601,43 @@
 	}
 }
 
+func TestValidateLogOptionsFormat(t *testing.T) {
+	tests := []struct {
+		format           string
+		multiLinePattern string
+		datetimeFormat   string
+		expErrMsg        string
+	}{
+		{"json/emf", "", "", ""},
+		{"random", "", "", "unsupported log format 'random'"},
+		{"", "", "", ""},
+		{"json/emf", "---", "", "you cannot configure log opt 'awslogs-datetime-format' or 'awslogs-multiline-pattern' when log opt 'awslogs-format' is set to 'json/emf'"},
+		{"json/emf", "", "yyyy-dd-mm", "you cannot configure log opt 'awslogs-datetime-format' or 'awslogs-multiline-pattern' when log opt 'awslogs-format' is set to 'json/emf'"},
+	}
+
+	for i, tc := range tests {
+		t.Run(fmt.Sprintf("%d/%s", i, tc.format), func(t *testing.T) {
+			cfg := map[string]string{
+				logGroupKey:  groupName,
+				logFormatKey: tc.format,
+			}
+			if tc.multiLinePattern != "" {
+				cfg[multilinePatternKey] = tc.multiLinePattern
+			}
+			if tc.datetimeFormat != "" {
+				cfg[datetimeFormatKey] = tc.datetimeFormat
+			}
+
+			err := ValidateLogOpt(cfg)
+			if tc.expErrMsg != "" {
+				assert.Error(t, err, tc.expErrMsg)
+			} else {
+				assert.NilError(t, err)
+			}
+		})
+	}
+}
+
 func TestCreateTagSuccess(t *testing.T) {
 	mockClient := newMockClient()
 	info := logger.Info{