Skip to content

Commit

Permalink
pkg/config: support custom Envoy JSON fields
Browse files Browse the repository at this point in the history
Add support for custon Envoy JSON fields via use of `=` in the field
name.

See [design doc](design/envoy-json-logging-custom-fields-design.md) for
more information.

Fixes projectcontour#3032

Signed-off-by: Mikael Manukyan <mmanukyan@vmware.com>
Co-authored-by: Mikael Manukyan <mmanukyan@vmware.com>
  • Loading branch information
KauzClay and mike1808 committed Oct 23, 2020
1 parent df3f1b1 commit d8c9424
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 28 deletions.
76 changes: 53 additions & 23 deletions pkg/config/accesslog.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,59 @@ package config
//used for specifying fields for Envoy to log when JSON logging is enabled.
//Only fields specified in this map may be used for JSON logging.
var JSONFields = map[string]string{
"@timestamp": "%START_TIME%",
"ts": "%START_TIME%",
"authority": "%REQ(:AUTHORITY)%",
"bytes_received": "%BYTES_RECEIVED%",
"bytes_sent": "%BYTES_SENT%",
"downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%",
"downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
"duration": "%DURATION%",
"method": "%REQ(:METHOD)%",
"path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%",
"protocol": "%PROTOCOL%",
"request_id": "%REQ(X-REQUEST-ID)%",
"requested_server_name": "%REQUESTED_SERVER_NAME%",
"response_code": "%RESPONSE_CODE%",
"response_flags": "%RESPONSE_FLAGS%",
"uber_trace_id": "%REQ(UBER-TRACE-ID)%",
"upstream_cluster": "%UPSTREAM_CLUSTER%",
"upstream_host": "%UPSTREAM_HOST%",
"upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%",
"upstream_service_time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%",
"user_agent": "%REQ(USER-AGENT)%",
"x_forwarded_for": "%REQ(X-FORWARDED-FOR)%",
"x_trace_id": "%REQ(X-TRACE-ID)%",
"@timestamp": "%START_TIME%",
"ts": "%START_TIME%",
"authority": "%REQ(:AUTHORITY)%",
"bytes_received": "%BYTES_RECEIVED%",
"bytes_sent": "%BYTES_SENT%",
"connection_id": "%CONNECTION_ID%",
"connection_termination_details": "%CONNECTION_TERMINATION_DETAILS%",
"downstream_direct_remote_address": "%DOWNSTREAM_DIRECT_REMOTE_ADDRESS%",
"downstream_direct_remote_address_without_port": "%DOWNSTREAM_DIRECT_REMOTE_ADDRESS_WITHOUT_PORT%",
"downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%",
"downstream_local_address_without_port": "%DOWNSTREAM_LOCAL_ADDRESS_WITHOUT_PORT%",
"downstream_local_port": "%DOWNSTREAM_LOCAL_PORT%",
"downstream_local_subject": "%DOWNSTREAM_LOCAL_SUBJECT%",
"downstream_local_uri_san": "%DOWNSTREAM_LOCAL_URI_SAN%",
"downstream_peer_cert": "%DOWNSTREAM_PEER_CERT%",
"downstream_peer_cert_v_end": "%DOWNSTREAM_PEER_CERT_V_END%",
"downstream_peer_cert_v_start": "%DOWNSTREAM_PEER_CERT_V_START%",
"downstream_peer_fingerprint_1": "%DOWNSTREAM_PEER_FINGERPRINT_1%",
"downstream_peer_fingerprint_256": "%DOWNSTREAM_PEER_FINGERPRINT_256%",
"downstream_peer_issuer": "%DOWNSTREAM_PEER_ISSUER%",
"downstream_peer_serial": "%DOWNSTREAM_PEER_SERIAL%",
"downstream_peer_subject": "%DOWNSTREAM_PEER_SUBJECT%",
"downstream_peer_uri_san": "%DOWNSTREAM_PEER_URI_SAN%",
"downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
"downstream_remote_address_without_port": "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%",
"downstream_tls_cipher": "%DOWNSTREAM_TLS_CIPHER%",
"downstream_tls_session_id": "%DOWNSTREAM_TLS_SESSION_ID%",
"downstream_tls_version": "%DOWNSTREAM_TLS_VERSION%",
"duration": "%DURATION%",
"grpc_status": "%GRPC_STATUS%",
"hostname": "%HOSTNAME%",
"method": "%REQ(:METHOD)%",
"path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%",
"protocol": "%PROTOCOL%",
"request_duration": "%REQUEST_DURATION%",
"request_id": "%REQ(X-REQUEST-ID)%",
"requested_server_name": "%REQUESTED_SERVER_NAME%",
"response_code": "%RESPONSE_CODE%",
"response_code_details": "%RESPONSE_CODE_DETAILS%",
"response_duration": "%RESPONSE_DURATION%",
"response_flags": "%RESPONSE_FLAGS%",
"response_tx_duration": "%RESPONSE_TX_DURATION%",
"route_name": "%ROUTE_NAME%",
"start_time": "%START_TIME%",
"uber_trace_id": "%REQ(UBER-TRACE-ID)%",
"upstream_cluster": "%UPSTREAM_CLUSTER%",
"upstream_host": "%UPSTREAM_HOST%",
"upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%",
"upstream_service_time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%",
"upstream_transport_failure_reason": "%UPSTREAM_TRANSPORT_FAILURE_REASON%",
"user_agent": "%REQ(USER-AGENT)%",
"x_forwarded_for": "%REQ(X-FORWARDED-FOR)%",
"x_trace_id": "%REQ(X-TRACE-ID)%",
}

// DefaultFields are fields that will be included by default when JSON logging is enabled.
Expand Down
91 changes: 86 additions & 5 deletions pkg/config/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"io"
"os"
"path/filepath"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -89,6 +90,88 @@ func (a AccessLogType) Validate() error {
const EnvoyAccessLog AccessLogType = "envoy"
const JSONAccessLog AccessLogType = "json"

type AccessLogFields []string

func (a AccessLogFields) Validate() error {
re := regexp.MustCompile(`%(([A-Z_]+)(\([^)]+\)(:[0-9]+)?)?%)?`)

for _, val := range a {
parts := strings.SplitN(val, "=", 2)
if len(parts) == 1 {
if _, ok := JSONFields[val]; !ok {
return fmt.Errorf("invalid JSON log field name %s", val)
}
} else {
tokens := re.FindAllStringSubmatch(parts[1], -1)
if len(tokens) == 0 {
return fmt.Errorf("invalid JSON field: %s", val)
}

for _, f := range tokens {
op := f[2]
if op == "" {
return fmt.Errorf("invalid JSON field: %s, invalid Envoy format: %s", val, f)
}

if _, ok := EnvoyOperators[op]; !ok {
return fmt.Errorf("invalid JSON field: %s, invalid Envoy format: %s, invalid Envoy operator: %s", val, f, op)
}
}
}
}

return nil
}

var EnvoyOperators = map[string]struct{}{
"START_TIME": {},
"BYTES_RECEIVED": {},
"PROTOCOL": {},
"RESPONSE_CODE": {},
"RESPONSE_CODE_DETAILS": {},
"CONNECTION_TERMINATION_DETAILS": {},
"BYTES_SENT": {},
"DURATION": {},
"REQUEST_DURATION": {},
"RESPONSE_DURATION": {},
"RESPONSE_TX_DURATION": {},
"RESPONSE_FLAGS": {},
"ROUTE_NAME": {},
"UPSTREAM_HOST": {},
"UPSTREAM_CLUSTER": {},
"UPSTREAM_LOCAL_ADDRESS": {},
"UPSTREAM_TRANSPORT_FAILURE_REASON": {},
"DOWNSTREAM_REMOTE_ADDRESS": {},
"DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT": {},
"DOWNSTREAM_DIRECT_REMOTE_ADDRESS": {},
"DOWNSTREAM_DIRECT_REMOTE_ADDRESS_WITHOUT_PORT": {},
"DOWNSTREAM_LOCAL_ADDRESS": {},
"DOWNSTREAM_LOCAL_ADDRESS_WITHOUT_PORT": {},
"CONNECTION_ID": {},
"GRPC_STATUS": {},
"DOWNSTREAM_LOCAL_PORT": {},
"REQ": {},
"RESP": {},
"TRAILER": {},
"REQUESTED_SERVER_NAME": {},
"DOWNSTREAM_LOCAL_URI_SAN": {},
"DOWNSTREAM_PEER_URI_SAN": {},
"DOWNSTREAM_LOCAL_SUBJECT": {},
"DOWNSTREAM_PEER_SUBJECT": {},
"DOWNSTREAM_PEER_ISSUER": {},
"DOWNSTREAM_TLS_SESSION_ID": {},
"DOWNSTREAM_TLS_CIPHER": {},
"DOWNSTREAM_TLS_VERSION": {},
"DOWNSTREAM_PEER_FINGERPRINT_256": {},
"DOWNSTREAM_PEER_FINGERPRINT_1": {},
"DOWNSTREAM_PEER_SERIAL": {},
"DOWNSTREAM_PEER_CERT": {},
"DOWNSTREAM_PEER_CERT_V_START": {},
"DOWNSTREAM_PEER_CERT_V_END": {},
"HOSTNAME": {},
"LOCAL_REPLY_BODY": {},
}

// HTTPVersionType is the name of a supported HTTP version.
type HTTPVersionType string

Expand Down Expand Up @@ -288,7 +371,7 @@ type Parameters struct {

// AccessLogFields sets the fields that JSON logging will
// output when AccessLogFormat is json.
AccessLogFields []string `yaml:"json-fields,omitempty"`
AccessLogFields AccessLogFields `yaml:"json-fields,omitempty"`

// TLS contains TLS policy parameters.
TLS TLSParameters `yaml:"tls,omitempty"`
Expand Down Expand Up @@ -341,10 +424,8 @@ func (p *Parameters) Validate() error {
return err
}

for _, f := range p.AccessLogFields {
if _, ok := JSONFields[f]; !ok {
return fmt.Errorf("invalid JSON log field name %s", f)
}
if err := p.AccessLogFields.Validate(); err != nil {
return err
}

// Check TLS secret names.
Expand Down
18 changes: 18 additions & 0 deletions pkg/config/parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ func TestValidateAccessLogType(t *testing.T) {
assert.NoError(t, JSONAccessLog.Validate())
}

func TestValidateAccessLogFields(t *testing.T) {
assert.Error(t, AccessLogFields([]string{"dog", "cat"}).Validate())
assert.Error(t, AccessLogFields([]string{"@timestamp", "dog"}).Validate())
assert.Error(t, AccessLogFields([]string{"@timestamp", "content-id=%REQ=dog%"}).Validate())
assert.Error(t, AccessLogFields([]string{"@timestamp", "content-id=%dog(%"}).Validate())
assert.Error(t, AccessLogFields([]string{"@timestamp", "content-id=%REQ()%"}).Validate())
assert.Error(t, AccessLogFields([]string{"@timestamp", "content-id=%DOG%"}).Validate())
assert.Error(t, AccessLogFields([]string{"@timestamp", "duration=my durations % are %DURATION%.0 and %REQ(:METHOD)%"}).Validate())

assert.NoError(t, AccessLogFields([]string{"@timestamp", "method"}).Validate())
assert.NoError(t, AccessLogFields([]string{"@timestamp", "response_duration"}).Validate())
assert.NoError(t, AccessLogFields([]string{"@timestamp", "duration=%DURATION%.0"}).Validate())
assert.NoError(t, AccessLogFields([]string{"@timestamp", "duration=My duration=%DURATION%.0"}).Validate())
assert.NoError(t, AccessLogFields([]string{"@timestamp", "duration=%START_TIME(%s.%6f)%"}).Validate())
assert.NoError(t, AccessLogFields([]string{"@timestamp", "content-id=%REQ(X-CONTENT-ID)%"}).Validate())
assert.NoError(t, AccessLogFields([]string{"@timestamp", "duration=my durations are %DURATION%.0 and method is %REQ(:METHOD)%"}).Validate())
}

func TestValidateHTTPVersionType(t *testing.T) {
assert.Error(t, HTTPVersionType("").Validate())
assert.Error(t, HTTPVersionType("foo").Validate())
Expand Down

0 comments on commit d8c9424

Please sign in to comment.