1b81cccbf9
* Expose Envoy /stats for statsd agents; Add testcases * Remove merge conflict leftover * Add support for prefix instead of path; Fix docstring to mirror these changes * Add new config field to docs; Add testcases to check that /stats/prometheus is exposed as well * Parametrize matchType (prefix or path) and value * Update website/source/docs/connect/proxies/envoy.md Co-Authored-By: Paul Banks <banks@banksco.de> Co-authored-by: Paul Banks <banks@banksco.de>
475 lines
16 KiB
Go
475 lines
16 KiB
Go
package envoy
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"text/template"
|
|
)
|
|
|
|
// BootstrapConfig is the set of keys we care about in a Connect.Proxy.Config
|
|
// map. Note that this only includes config keys that affects Envoy bootstrap
|
|
// generation. For Envoy config keys that affect runtime xDS behavior see
|
|
// agent/xds/config.go.
|
|
type BootstrapConfig struct {
|
|
// StatsdURL allows simple configuration of the statsd metrics sink. If
|
|
// tagging is required, use DogstatsdURL instead. The URL must be in one of
|
|
// the following forms:
|
|
// - udp://<ip>:<port>
|
|
// - $ENV_VAR_NAME in this case the ENV var named will have it's
|
|
// value taken and is expected to contain a URL in
|
|
// one of the supported forms above.
|
|
StatsdURL string `mapstructure:"envoy_statsd_url"`
|
|
|
|
// DogstatsdURL allows simple configuration of the dogstatsd metrics sink
|
|
// which allows tags and Unix domain sockets. The URL must be in one of the
|
|
// following forms:
|
|
// - udp://<ip>:<port>
|
|
// - unix:///full/path/to/unix.sock
|
|
// - $ENV_VAR_NAME in this case the ENV var named will have it's
|
|
// value taken and is expected to contain a URL in
|
|
// one of the supported forms above.
|
|
DogstatsdURL string `mapstructure:"envoy_dogstatsd_url"`
|
|
|
|
// StatsTags is a slice of string values that will be added as tags to
|
|
// metrics. They are used to configure
|
|
// https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/metrics/v2/stats.proto#envoy-api-msg-config-metrics-v2-statsconfig
|
|
// and add to the basic tags Consul adds by default like the local_cluster
|
|
// name. Only exact values are supported here. Full configuration of
|
|
// stats_config.stats_tags can be made by overriding envoy_stats_config_json.
|
|
StatsTags []string `mapstructure:"envoy_stats_tags"`
|
|
|
|
// PrometheusBindAddr configures an <ip>:<port> on which the Envoy will listen
|
|
// and expose a single /metrics HTTP endpoint for Prometheus to scrape. It
|
|
// does this by proxying that URL to the internal admin server's prometheus
|
|
// endpoint which allows exposing metrics on the network without the security
|
|
// risk of exposing the full admin server API. Any other URL requested will be
|
|
// a 404.
|
|
//
|
|
// Note that as of Envoy 1.9.0, the built in Prometheus endpoint only exports
|
|
// counters and gauges but not timing information via histograms. This is
|
|
// fixed in 1.10-dev currently in Envoy master. Other changes since 1.9.0 make
|
|
// master incompatible with the current release of Consul Connect. This will
|
|
// be fixed in a future Consul version as Envoy 1.10 reaches stable release.
|
|
PrometheusBindAddr string `mapstructure:"envoy_prometheus_bind_addr"`
|
|
|
|
// StatsBindAddr configures an <ip>:<port> on which the Envoy will listen
|
|
// and expose the /stats HTTP path prefix for any agent to access. It
|
|
// does this by proxying that path prefix to the internal admin server
|
|
// which allows exposing metrics on the network without the security
|
|
// risk of exposing the full admin server API. Any other URL requested will be
|
|
// a 404.
|
|
StatsBindAddr string `mapstructure:"envoy_stats_bind_addr"`
|
|
|
|
// OverrideJSONTpl allows replacing the base template used to render the
|
|
// bootstrap. This is an "escape hatch" allowing arbitrary control over the
|
|
// proxy's configuration but will the most effort to maintain and correctly
|
|
// configure the aspects that Connect relies upon to work. It's recommended
|
|
// that this only be used if necessary, and that it be based on the default
|
|
// template in
|
|
// https://github.com/hashicorp/consul/blob/master/command/connect/envoy/bootstrap_tpl.go
|
|
// for the correct version of Consul and Envoy being used.
|
|
OverrideJSONTpl string `mapstructure:"envoy_bootstrap_json_tpl"`
|
|
|
|
// StaticClustersJSON is a JSON string containing zero or more Cluster
|
|
// definitions. They are appended to the "static_resources.clusters" list. A
|
|
// single cluster should be given as a plain object, if more than one is to be
|
|
// added, they should be separated by a comma suitable for direct injection
|
|
// into a JSON array.
|
|
//
|
|
// Note that cluster names should be chosen in such a way that they won't
|
|
// collide with service names since we use plain service names as cluster
|
|
// names in xDS to make metrics population simpler and cluster names mush be
|
|
// unique.
|
|
//
|
|
// This is mostly intended for providing clusters for tracing or metrics
|
|
// services.
|
|
//
|
|
// See https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/api/v2/cds.proto.
|
|
StaticClustersJSON string `mapstructure:"envoy_extra_static_clusters_json"`
|
|
|
|
// StaticListenersJSON is a JSON string containing zero or more Listener
|
|
// definitions. They are appended to the "static_resources.listeners" list. A
|
|
// single listener should be given as a plain object, if more than one is to
|
|
// be added, they should be separated by a comma suitable for direct injection
|
|
// into a JSON array.
|
|
//
|
|
// See https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/api/v2/lds.proto.
|
|
StaticListenersJSON string `mapstructure:"envoy_extra_static_listeners_json"`
|
|
|
|
// StatsSinksJSON is a JSON string containing zero or more StatsSink
|
|
// definititions. They are appended to the `stats_sinks` array at the top
|
|
// level of the bootstrap config. A single sink should be given as a plain
|
|
// object, if more than one is to be added, they should be separated by a
|
|
// comma suitable for direct injection into a JSON array.
|
|
//
|
|
// See
|
|
// https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/metrics/v2/stats.proto#config-metrics-v2-statssink.
|
|
//
|
|
// If this is non-empty then it will override anything configured in
|
|
// StatsTags.
|
|
StatsSinksJSON string `mapstructure:"envoy_extra_stats_sinks_json"`
|
|
|
|
// StatsConfigJSON is a JSON string containing an object in the right format
|
|
// to be rendered as the body of the `stats_config` field at the top level of
|
|
// the bootstrap config. It's format may vary based on Envoy version used. See
|
|
// https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/metrics/v2/stats.proto#envoy-api-msg-config-metrics-v2-statsconfig.
|
|
//
|
|
// If this is non-empty then it will override anything configured in
|
|
// StatsdURL or DogstatsdURL.
|
|
StatsConfigJSON string `mapstructure:"envoy_stats_config_json"`
|
|
|
|
// StatsFlushInterval is the time duration between Envoy stats flushes. It is
|
|
// in proto3 "duration" string format for example "1.12s" See
|
|
// https://developers.google.com/protocol-buffers/docs/proto3#json and
|
|
// https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/bootstrap/v2/bootstrap.proto#bootstrap
|
|
StatsFlushInterval string `mapstructure:"envoy_stats_flush_interval"`
|
|
|
|
// TracingConfigJSON is a JSON string containing an object in the right format
|
|
// to be rendered as the body of the `tracing` field at the top level of
|
|
// the bootstrap config. It's format may vary based on Envoy version used.
|
|
// See https://www.envoyproxy.io/docs/envoy/v1.9.0/api-v2/config/trace/v2/trace.proto.
|
|
TracingConfigJSON string `mapstructure:"envoy_tracing_json"`
|
|
}
|
|
|
|
// Template returns the bootstrap template to use as a base.
|
|
func (c *BootstrapConfig) Template() string {
|
|
if c.OverrideJSONTpl != "" {
|
|
return c.OverrideJSONTpl
|
|
}
|
|
return bootstrapTemplate
|
|
}
|
|
|
|
func (c *BootstrapConfig) GenerateJSON(args *BootstrapTplArgs) ([]byte, error) {
|
|
if err := c.ConfigureArgs(args); err != nil {
|
|
return nil, err
|
|
}
|
|
t, err := template.New("bootstrap").Parse(c.Template())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var buf bytes.Buffer
|
|
err = t.Execute(&buf, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Pretty print the JSON.
|
|
var buf2 bytes.Buffer
|
|
if err := json.Indent(&buf2, buf.Bytes(), "", " "); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf2.Bytes(), nil
|
|
}
|
|
|
|
// ConfigureArgs takes the basic template arguments generated from the command
|
|
// arguments and environment and modifies them according to the BootstrapConfig.
|
|
func (c *BootstrapConfig) ConfigureArgs(args *BootstrapTplArgs) error {
|
|
|
|
// Attempt to setup sink(s) from high-level config. Note the args are passed
|
|
// by ref and modified in place.
|
|
if err := c.generateStatsSinks(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.StatsConfigJSON != "" {
|
|
// StatsConfig overridden explicitly
|
|
args.StatsConfigJSON = c.StatsConfigJSON
|
|
} else {
|
|
// Attempt to setup tags from high-level config. Note the args are passed by
|
|
// ref and modified in place.
|
|
if err := c.generateStatsConfig(args); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if c.StaticClustersJSON != "" {
|
|
args.StaticClustersJSON = c.StaticClustersJSON
|
|
}
|
|
if c.StaticListenersJSON != "" {
|
|
args.StaticListenersJSON = c.StaticListenersJSON
|
|
}
|
|
// Setup prometheus if needed. This MUST happen after the Static*JSON is set above
|
|
if c.PrometheusBindAddr != "" {
|
|
if err := c.generateMetricsListenerConfig(args, c.PrometheusBindAddr, "envoy_prometheus_metrics", "path", "/metrics", "/stats/prometheus"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Setup /stats proxy listener if needed. This MUST happen after the Static*JSON is set above
|
|
if c.StatsBindAddr != "" {
|
|
if err := c.generateMetricsListenerConfig(args, c.StatsBindAddr, "envoy_metrics", "prefix", "/stats", "/stats"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if c.TracingConfigJSON != "" {
|
|
args.TracingConfigJSON = c.TracingConfigJSON
|
|
}
|
|
|
|
if c.StatsFlushInterval != "" {
|
|
args.StatsFlushInterval = c.StatsFlushInterval
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *BootstrapConfig) generateStatsSinks(args *BootstrapTplArgs) error {
|
|
var stats_sinks []string
|
|
|
|
if c.StatsdURL != "" {
|
|
sinkJSON, err := c.generateStatsSinkJSON("envoy.statsd", c.StatsdURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stats_sinks = append(stats_sinks, sinkJSON)
|
|
}
|
|
if c.DogstatsdURL != "" {
|
|
sinkJSON, err := c.generateStatsSinkJSON("envoy.dog_statsd", c.DogstatsdURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stats_sinks = append(stats_sinks, sinkJSON)
|
|
}
|
|
if c.StatsSinksJSON != "" {
|
|
stats_sinks = append(stats_sinks, c.StatsSinksJSON)
|
|
}
|
|
|
|
if len(stats_sinks) > 0 {
|
|
args.StatsSinksJSON = "[\n" + strings.Join(stats_sinks, ",\n") + "\n]"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *BootstrapConfig) generateStatsSinkJSON(name string, addr string) (string, error) {
|
|
// Resolve address ENV var
|
|
if len(addr) > 2 && addr[0] == '$' {
|
|
addr = os.Getenv(addr[1:])
|
|
}
|
|
|
|
u, err := url.Parse(addr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse %s sink address %q", name, addr)
|
|
}
|
|
|
|
var addrJSON string
|
|
switch u.Scheme {
|
|
case "udp":
|
|
addrJSON = `
|
|
"socket_address": {
|
|
"address": "` + u.Hostname() + `",
|
|
"port_value": ` + u.Port() + `
|
|
}
|
|
`
|
|
case "unix":
|
|
addrJSON = `
|
|
"pipe": {
|
|
"path": "` + u.Path + `"
|
|
}
|
|
`
|
|
default:
|
|
return "", fmt.Errorf("unsupported address protocol %q for %s sink",
|
|
u.Scheme, name)
|
|
}
|
|
|
|
return `{
|
|
"name": "` + name + `",
|
|
"config": {
|
|
"address": {
|
|
` + addrJSON + `
|
|
}
|
|
}
|
|
}`, nil
|
|
}
|
|
|
|
var sniTagJSONs []string
|
|
|
|
func init() {
|
|
// <subset>.<service>.<namespace>.<datacenter>.<internal|external>.<trustdomain>.consul
|
|
// - cluster.pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
|
|
// - cluster.f8f8f8f8~pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
|
|
// - cluster.v2.pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
|
|
// - cluster.f8f8f8f8~v2.pong.default.dc2.internal.e5b08d03-bfc3-c870-1833-baddb116e648.consul.bind_errors: 0
|
|
const PART = `[^.]+`
|
|
rules := [][]string{
|
|
{"consul.custom_hash",
|
|
fmt.Sprintf(`^cluster\.((?:(%s)~)?(?:%s\.)?%s\.%s\.%s\.%s\.%s\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)},
|
|
{"consul.service_subset",
|
|
fmt.Sprintf(`^cluster\.((?:%s~)?(?:(%s)\.)?%s\.%s\.%s\.%s\.%s\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)},
|
|
{"consul.service",
|
|
fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?(%s)\.%s\.%s\.%s\.%s\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)},
|
|
{"consul.namespace",
|
|
fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?%s\.(%s)\.%s\.%s\.%s\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)},
|
|
{"consul.datacenter",
|
|
fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?%s\.%s\.(%s)\.%s\.%s\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)},
|
|
{"consul.routing_type",
|
|
fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?%s\.%s\.%s\.(%s)\.%s\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)}, // internal:true/false would be idea
|
|
{"consul.trust_domain",
|
|
fmt.Sprintf(`^cluster\.((?:%s~)?(?:%s\.)?%s\.%s\.%s\.%s\.(%s)\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)},
|
|
{"consul.target",
|
|
fmt.Sprintf(`^cluster\.(((?:%s~)?(?:%s\.)?%s\.%s\.%s)\.%s\.%s\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)},
|
|
{"consul.full_target",
|
|
fmt.Sprintf(`^cluster\.(((?:%s~)?(?:%s\.)?%s\.%s\.%s\.%s\.%s)\.consul\.)`, PART, PART, PART, PART, PART, PART, PART)},
|
|
}
|
|
|
|
for _, rule := range rules {
|
|
m := map[string]string{
|
|
"tag_name": rule[0],
|
|
"regex": rule[1],
|
|
}
|
|
d, err := json.Marshal(m)
|
|
if err != nil {
|
|
panic("error pregenerating SNI envoy tags: " + err.Error())
|
|
}
|
|
sniTagJSONs = append(sniTagJSONs, string(d))
|
|
}
|
|
}
|
|
|
|
func (c *BootstrapConfig) generateStatsConfig(args *BootstrapTplArgs) error {
|
|
var tagJSONs []string
|
|
|
|
// Add some default tags if not already overridden
|
|
defaults := map[string]string{
|
|
"local_cluster": args.ProxyCluster,
|
|
}
|
|
|
|
// Explode SNI portions.
|
|
tagJSONs = append(tagJSONs, sniTagJSONs...)
|
|
|
|
for _, tag := range c.StatsTags {
|
|
parts := strings.SplitN(tag, "=", 2)
|
|
// If there is no equals, treat it as a boolean tag and just assign value of
|
|
// 1 e.g. "canary" will out put the tag "canary: 1"
|
|
v := "1"
|
|
if len(parts) == 2 {
|
|
v = parts[1]
|
|
}
|
|
k := strings.ToLower(parts[0])
|
|
tagJSON := `{
|
|
"tag_name": "` + k + `",
|
|
"fixed_value": "` + v + `"
|
|
}`
|
|
tagJSONs = append(tagJSONs, tagJSON)
|
|
// Remove this in case we override a default
|
|
delete(defaults, k)
|
|
}
|
|
|
|
for k, v := range defaults {
|
|
if v == "" {
|
|
// Skip stuff we just didn't have data for, this is only really the case
|
|
// in tests currently.
|
|
continue
|
|
}
|
|
tagJSON := `{
|
|
"tag_name": "` + k + `",
|
|
"fixed_value": "` + v + `"
|
|
}`
|
|
tagJSONs = append(tagJSONs, tagJSON)
|
|
}
|
|
|
|
if len(tagJSONs) > 0 {
|
|
// use_all_default_tags is true by default but we'll make it explicit!
|
|
args.StatsConfigJSON = `{
|
|
"stats_tags": [
|
|
` + strings.Join(tagJSONs, ",\n") + `
|
|
],
|
|
"use_all_default_tags": true
|
|
}`
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *BootstrapConfig) generateMetricsListenerConfig(args *BootstrapTplArgs, bindAddr, name, matchType, matchValue, prefixRewrite string) error {
|
|
host, port, err := net.SplitHostPort(bindAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid %s bind address: %s", name, err)
|
|
}
|
|
|
|
clusterJSON := `{
|
|
"name": "self_admin",
|
|
"connect_timeout": "5s",
|
|
"type": "STATIC",
|
|
"http_protocol_options": {},
|
|
"hosts": [
|
|
{
|
|
"socket_address": {
|
|
"address": "127.0.0.1",
|
|
"port_value": ` + args.AdminBindPort + `
|
|
}
|
|
}
|
|
]
|
|
}`
|
|
listenerJSON := `{
|
|
"name": "` + name + `_listener",
|
|
"address": {
|
|
"socket_address": {
|
|
"address": "` + host + `",
|
|
"port_value": ` + port + `
|
|
}
|
|
},
|
|
"filter_chains": [
|
|
{
|
|
"filters": [
|
|
{
|
|
"name": "envoy.http_connection_manager",
|
|
"config": {
|
|
"stat_prefix": "` + name + `",
|
|
"codec_type": "HTTP1",
|
|
"route_config": {
|
|
"name": "self_admin_route",
|
|
"virtual_hosts": [
|
|
{
|
|
"name": "self_admin",
|
|
"domains": [
|
|
"*"
|
|
],
|
|
"routes": [
|
|
{
|
|
"match": {
|
|
"` + matchType + `": "` + matchValue + `"
|
|
},
|
|
"route": {
|
|
"cluster": "self_admin",
|
|
"prefix_rewrite": "` + prefixRewrite + `"
|
|
}
|
|
},
|
|
{
|
|
"match": {
|
|
"prefix": "/"
|
|
},
|
|
"direct_response": {
|
|
"status": 404
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
"http_filters": [
|
|
{
|
|
"name": "envoy.router"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`
|
|
|
|
if args.StaticClustersJSON != "" {
|
|
clusterJSON = ",\n" + clusterJSON
|
|
}
|
|
args.StaticClustersJSON += clusterJSON
|
|
|
|
if args.StaticListenersJSON != "" {
|
|
listenerJSON = ",\n" + listenerJSON
|
|
}
|
|
args.StaticListenersJSON += listenerJSON
|
|
return nil
|
|
}
|