// 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/v2beta1" "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"}}, }, } }