2023-03-28 18:39:22 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
2023-08-11 13:12:13 +00:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-03-28 18:39:22 +00:00
|
|
|
|
2022-12-22 20:18:15 +00:00
|
|
|
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"
|
|
|
|
"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.AccessLogsConfig, isListener bool) ([]*envoy_accesslog_v3.AccessLog, error) {
|
|
|
|
if logs == nil || !logs.Enabled {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if isListener && logs.DisableListenerLogs {
|
|
|
|
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.AccessLogsConfig) (*anypb.Any, error) {
|
|
|
|
logFormat, err := getLogFormat(logs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not get envoy log format: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch logs.Type {
|
|
|
|
case structs.DefaultLogSinkType, structs.StdOutLogSinkType:
|
|
|
|
return getStdoutLogger(logFormat)
|
|
|
|
case structs.StdErrLogSinkType:
|
|
|
|
return getStderrLogger(logFormat)
|
|
|
|
case structs.FileLogSinkType:
|
|
|
|
return getFileLogger(logFormat, logs.Path)
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unsupported log format: %s", logs.Type)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.AccessLogsConfig) (*envoy_core_v3.SubstitutionFormatString, error) {
|
|
|
|
|
|
|
|
var format, formatType string
|
|
|
|
if logs.TextFormat == "" && logs.JSONFormat == "" {
|
|
|
|
format = defaultJSONFormat
|
|
|
|
formatType = "json"
|
|
|
|
} else if logs.JSONFormat != "" {
|
|
|
|
format = logs.JSONFormat
|
|
|
|
formatType = "json"
|
|
|
|
} else {
|
|
|
|
format = logs.TextFormat
|
|
|
|
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"}},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|