consul/agent/xds/accesslogs/accesslogs.go

189 lines
6.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package accesslogs
import (
"fmt"
envoy_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_fileaccesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/file/v3"
envoy_streamaccesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/stream/v3"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/structpb"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
)
const (
defaultJSONFormat = `
{
"start_time": "%START_TIME%",
"route_name": "%ROUTE_NAME%",
"method": "%REQ(:METHOD)%",
"path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%",
"protocol": "%PROTOCOL%",
"response_code": "%RESPONSE_CODE%",
"response_flags": "%RESPONSE_FLAGS%",
"response_code_details": "%RESPONSE_CODE_DETAILS%",
"connection_termination_details": "%CONNECTION_TERMINATION_DETAILS%",
"bytes_received": "%BYTES_RECEIVED%",
"bytes_sent": "%BYTES_SENT%",
"duration": "%DURATION%",
"upstream_service_time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%",
"x_forwarded_for": "%REQ(X-FORWARDED-FOR)%",
"user_agent": "%REQ(USER-AGENT)%",
"request_id": "%REQ(X-REQUEST-ID)%",
"authority": "%REQ(:AUTHORITY)%",
"upstream_host": "%UPSTREAM_HOST%",
"upstream_cluster": "%UPSTREAM_CLUSTER%",
"upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%",
"downstream_local_address": "%DOWNSTREAM_LOCAL_ADDRESS%",
"downstream_remote_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
"requested_server_name": "%REQUESTED_SERVER_NAME%",
"upstream_transport_failure_reason": "%UPSTREAM_TRANSPORT_FAILURE_REASON%"
}
`
)
// MakeAccessLogs returns a fully-hydrated slice of Envoy Access log configurations based
// on the proxy-defaults settings. Currently only one access logger is supported.
// Listeners (as opposed to listener filters) can trigger an access log filter with the boolean.
// Tests are located in agent/xds/listeners_test.go.
func MakeAccessLogs(logs structs.AccessLogs, isListener bool) ([]*envoy_accesslog_v3.AccessLog, error) {
if logs == nil || !logs.GetEnabled() {
return nil, nil
}
if isListener && logs.GetDisableListenerLogs() {
return nil, nil
}
config, err := getLogger(logs)
if err != nil {
return nil, fmt.Errorf("failed to get logger: %w", err)
}
var filter *envoy_accesslog_v3.AccessLogFilter
name := "Consul Listener Filter Log"
if isListener {
name = "Consul Listener Log"
filter = getListenerAccessLogFilter()
}
newFilter := &envoy_accesslog_v3.AccessLog{
Name: name,
Filter: filter,
ConfigType: &envoy_accesslog_v3.AccessLog_TypedConfig{
TypedConfig: config,
},
}
return []*envoy_accesslog_v3.AccessLog{newFilter}, nil
}
// getLogger returns an individual instance of an Envoy logger based on proxy-defaults
func getLogger(logs structs.AccessLogs) (*anypb.Any, error) {
logFormat, err := getLogFormat(logs)
if err != nil {
return nil, fmt.Errorf("could not get envoy log format: %w", err)
}
switch logs.GetType() {
case pbmesh.LogSinkType_LOG_SINK_TYPE_DEFAULT, pbmesh.LogSinkType_LOG_SINK_TYPE_STDOUT:
return getStdoutLogger(logFormat)
case pbmesh.LogSinkType_LOG_SINK_TYPE_STDERR:
return getStderrLogger(logFormat)
case pbmesh.LogSinkType_LOG_SINK_TYPE_FILE:
return getFileLogger(logFormat, logs.GetPath())
default:
return nil, fmt.Errorf("unsupported log format: %s", logs.GetType())
}
}
// getLogFormat returns an Envoy log format object that is compatible with all log sinks.
// If a format is not provided in the proxy-defaults, the default JSON format is used.
func getLogFormat(logs structs.AccessLogs) (*envoy_core_v3.SubstitutionFormatString, error) {
var format, formatType string
if logs.GetTextFormat() == "" && logs.GetJsonFormat() == "" {
format = defaultJSONFormat
formatType = "json"
} else if logs.GetJsonFormat() != "" {
format = logs.GetJsonFormat()
formatType = "json"
} else {
format = logs.GetTextFormat()
formatType = "text"
}
switch formatType {
case "json":
jsonFormat := structpb.Struct{}
if err := jsonFormat.UnmarshalJSON([]byte(format)); err != nil {
return nil, fmt.Errorf("could not unmarshal JSON format string: %w", err)
}
return &envoy_core_v3.SubstitutionFormatString{
Format: &envoy_core_v3.SubstitutionFormatString_JsonFormat{
JsonFormat: &jsonFormat,
},
}, nil
case "text":
textFormat := lib.EnsureTrailingNewline(format)
return &envoy_core_v3.SubstitutionFormatString{
Format: &envoy_core_v3.SubstitutionFormatString_TextFormatSource{
TextFormatSource: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_InlineString{
InlineString: textFormat,
},
},
},
}, nil
default:
return nil, fmt.Errorf("invalid log format type")
}
}
// getStdoutLogger returns Envoy's representation of a stdout log sink with the provided format.
func getStdoutLogger(logFormat *envoy_core_v3.SubstitutionFormatString) (*anypb.Any, error) {
return anypb.New(&envoy_streamaccesslog_v3.StdoutAccessLog{
AccessLogFormat: &envoy_streamaccesslog_v3.StdoutAccessLog_LogFormat{
LogFormat: logFormat,
},
})
}
// getStderrLogger returns Envoy's representation of a stderr log sink with the provided format.
func getStderrLogger(logFormat *envoy_core_v3.SubstitutionFormatString) (*anypb.Any, error) {
return anypb.New(&envoy_streamaccesslog_v3.StderrAccessLog{
AccessLogFormat: &envoy_streamaccesslog_v3.StderrAccessLog_LogFormat{
LogFormat: logFormat,
},
})
}
// getFileLogger returns Envoy's representation of a file log sink with the provided format and path to a file.
func getFileLogger(logFormat *envoy_core_v3.SubstitutionFormatString, path string) (*anypb.Any, error) {
return anypb.New(&envoy_fileaccesslog_v3.FileAccessLog{
AccessLogFormat: &envoy_fileaccesslog_v3.FileAccessLog_LogFormat{
LogFormat: logFormat,
},
Path: path,
})
}
// getListenerAccessLogFilter returns a filter that will be used on listeners to decide when a log is emitted.
// Set to "NR" which corresponds to "No route configured for a given request in addition
// to 404 response code, or no matching filter chain for a downstream connection."
func getListenerAccessLogFilter() *envoy_accesslog_v3.AccessLogFilter {
return &envoy_accesslog_v3.AccessLogFilter{
FilterSpecifier: &envoy_accesslog_v3.AccessLogFilter_ResponseFlagFilter{
ResponseFlagFilter: &envoy_accesslog_v3.ResponseFlagFilter{Flags: []string{"NR"}},
},
}
}