consul/agent/xds/accesslogs/accesslogs.go

188 lines
6.8 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// 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"
"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"}},
},
}
}