github.com/outbrain/consul@v1.4.5/lib/telemetry.go (about)

     1  package lib
     2  
     3  import (
     4  	"reflect"
     5  	"time"
     6  
     7  	metrics "github.com/armon/go-metrics"
     8  	"github.com/armon/go-metrics/circonus"
     9  	"github.com/armon/go-metrics/datadog"
    10  	"github.com/armon/go-metrics/prometheus"
    11  )
    12  
    13  // TelemetryConfig is embedded in config.RuntimeConfig and holds the
    14  // configuration variables for go-metrics. It is a separate struct to allow it
    15  // to be exported as JSON and passed to other process like managed connect
    16  // proxies so they can inherit the agent's telemetry config.
    17  //
    18  // It is in lib package rather than agent/config because we need to use it in
    19  // the shared InitTelemetry functions below, but we can't import agent/config
    20  // due to a dependency cycle.
    21  type TelemetryConfig struct {
    22  	// Circonus*: see https://github.com/circonus-labs/circonus-gometrics
    23  	// for more details on the various configuration options.
    24  	// Valid configuration combinations:
    25  	//    - CirconusAPIToken
    26  	//      metric management enabled (search for existing check or create a new one)
    27  	//    - CirconusSubmissionUrl
    28  	//      metric management disabled (use check with specified submission_url,
    29  	//      broker must be using a public SSL certificate)
    30  	//    - CirconusAPIToken + CirconusCheckSubmissionURL
    31  	//      metric management enabled (use check with specified submission_url)
    32  	//    - CirconusAPIToken + CirconusCheckID
    33  	//      metric management enabled (use check with specified id)
    34  
    35  	// CirconusAPIApp is an app name associated with API token.
    36  	// Default: "consul"
    37  	//
    38  	// hcl: telemetry { circonus_api_app = string }
    39  	CirconusAPIApp string `json:"circonus_api_app,omitempty" mapstructure:"circonus_api_app"`
    40  
    41  	// CirconusAPIToken is a valid API Token used to create/manage check. If provided,
    42  	// metric management is enabled.
    43  	// Default: none
    44  	//
    45  	// hcl: telemetry { circonus_api_token = string }
    46  	CirconusAPIToken string `json:"circonus_api_token,omitempty" mapstructure:"circonus_api_token"`
    47  
    48  	// CirconusAPIURL is the base URL to use for contacting the Circonus API.
    49  	// Default: "https://api.circonus.com/v2"
    50  	//
    51  	// hcl: telemetry { circonus_api_url = string }
    52  	CirconusAPIURL string `json:"circonus_apiurl,omitempty" mapstructure:"circonus_apiurl"`
    53  
    54  	// CirconusBrokerID is an explicit broker to use when creating a new check. The numeric portion
    55  	// of broker._cid. If metric management is enabled and neither a Submission URL nor Check ID
    56  	// is provided, an attempt will be made to search for an existing check using Instance ID and
    57  	// Search Tag. If one is not found, a new HTTPTRAP check will be created.
    58  	// Default: use Select Tag if provided, otherwise, a random Enterprise Broker associated
    59  	// with the specified API token or the default Circonus Broker.
    60  	// Default: none
    61  	//
    62  	// hcl: telemetry { circonus_broker_id = string }
    63  	CirconusBrokerID string `json:"circonus_broker_id,omitempty" mapstructure:"circonus_broker_id"`
    64  
    65  	// CirconusBrokerSelectTag is a special tag which will be used to select a broker when
    66  	// a Broker ID is not provided. The best use of this is to as a hint for which broker
    67  	// should be used based on *where* this particular instance is running.
    68  	// (e.g. a specific geo location or datacenter, dc:sfo)
    69  	// Default: none
    70  	//
    71  	// hcl: telemetry { circonus_broker_select_tag = string }
    72  	CirconusBrokerSelectTag string `json:"circonus_broker_select_tag,omitempty" mapstructure:"circonus_broker_select_tag"`
    73  
    74  	// CirconusCheckDisplayName is the name for the check which will be displayed in the Circonus UI.
    75  	// Default: value of CirconusCheckInstanceID
    76  	//
    77  	// hcl: telemetry { circonus_check_display_name = string }
    78  	CirconusCheckDisplayName string `json:"circonus_check_display_name,omitempty" mapstructure:"circonus_check_display_name"`
    79  
    80  	// CirconusCheckForceMetricActivation will force enabling metrics, as they are encountered,
    81  	// if the metric already exists and is NOT active. If check management is enabled, the default
    82  	// behavior is to add new metrics as they are encountered. If the metric already exists in the
    83  	// check, it will *NOT* be activated. This setting overrides that behavior.
    84  	// Default: "false"
    85  	//
    86  	// hcl: telemetry { circonus_check_metrics_activation = (true|false)
    87  	CirconusCheckForceMetricActivation string `json:"circonus_check_force_metric_activation,omitempty" mapstructure:"circonus_check_force_metric_activation"`
    88  
    89  	// CirconusCheckID is the check id (not check bundle id) from a previously created
    90  	// HTTPTRAP check. The numeric portion of the check._cid field.
    91  	// Default: none
    92  	//
    93  	// hcl: telemetry { circonus_check_id = string }
    94  	CirconusCheckID string `json:"circonus_check_id,omitempty" mapstructure:"circonus_check_id"`
    95  
    96  	// CirconusCheckInstanceID serves to uniquely identify the metrics coming from this "instance".
    97  	// It can be used to maintain metric continuity with transient or ephemeral instances as
    98  	// they move around within an infrastructure.
    99  	// Default: hostname:app
   100  	//
   101  	// hcl: telemetry { circonus_check_instance_id = string }
   102  	CirconusCheckInstanceID string `json:"circonus_check_instance_id,omitempty" mapstructure:"circonus_check_instance_id"`
   103  
   104  	// CirconusCheckSearchTag is a special tag which, when coupled with the instance id, helps to
   105  	// narrow down the search results when neither a Submission URL or Check ID is provided.
   106  	// Default: service:app (e.g. service:consul)
   107  	//
   108  	// hcl: telemetry { circonus_check_search_tag = string }
   109  	CirconusCheckSearchTag string `json:"circonus_check_search_tag,omitempty" mapstructure:"circonus_check_search_tag"`
   110  
   111  	// CirconusCheckSearchTag is a special tag which, when coupled with the instance id, helps to
   112  	// narrow down the search results when neither a Submission URL or Check ID is provided.
   113  	// Default: service:app (e.g. service:consul)
   114  	//
   115  	// hcl: telemetry { circonus_check_tags = string }
   116  	CirconusCheckTags string `json:"circonus_check_tags,omitempty" mapstructure:"circonus_check_tags"`
   117  
   118  	// CirconusSubmissionInterval is the interval at which metrics are submitted to Circonus.
   119  	// Default: 10s
   120  	//
   121  	// hcl: telemetry { circonus_submission_interval = "duration" }
   122  	CirconusSubmissionInterval string `json:"circonus_submission_interval,omitempty" mapstructure:"circonus_submission_interval"`
   123  
   124  	// CirconusCheckSubmissionURL is the check.config.submission_url field from a
   125  	// previously created HTTPTRAP check.
   126  	// Default: none
   127  	//
   128  	// hcl: telemetry { circonus_submission_url = string }
   129  	CirconusSubmissionURL string `json:"circonus_submission_url,omitempty" mapstructure:"circonus_submission_url"`
   130  
   131  	// DisableHostname will disable hostname prefixing for all metrics.
   132  	//
   133  	// hcl: telemetry { disable_hostname = (true|false)
   134  	DisableHostname bool `json:"disable_hostname,omitempty" mapstructure:"disable_hostname"`
   135  
   136  	// DogStatsdAddr is the address of a dogstatsd instance. If provided,
   137  	// metrics will be sent to that instance
   138  	//
   139  	// hcl: telemetry { dogstatsd_addr = string }
   140  	DogstatsdAddr string `json:"dogstatsd_addr,omitempty" mapstructure:"dogstatsd_addr"`
   141  
   142  	// DogStatsdTags are the global tags that should be sent with each packet to dogstatsd
   143  	// It is a list of strings, where each string looks like "my_tag_name:my_tag_value"
   144  	//
   145  	// hcl: telemetry { dogstatsd_tags = []string }
   146  	DogstatsdTags []string `json:"dogstatsd_tags,omitempty" mapstructure:"dogstatsd_tags"`
   147  
   148  	// PrometheusRetentionTime is the retention time for prometheus metrics if greater than 0.
   149  	// A value of 0 disable Prometheus support. Regarding Prometheus, it is considered a good
   150  	// practice to put large values here (such as a few days), and at least the interval between
   151  	// prometheus requests.
   152  	//
   153  	// hcl: telemetry { prometheus_retention_time = "duration" }
   154  	PrometheusRetentionTime time.Duration `json:"prometheus_retention_time,omitempty" mapstructure:"prometheus_retention_time"`
   155  
   156  	// FilterDefault is the default for whether to allow a metric that's not
   157  	// covered by the filter.
   158  	//
   159  	// hcl: telemetry { filter_default = (true|false) }
   160  	FilterDefault bool `json:"filter_default,omitempty" mapstructure:"filter_default"`
   161  
   162  	// AllowedPrefixes is a list of filter rules to apply for allowing metrics
   163  	// by prefix. Use the 'prefix_filter' option and prefix rules with '+' to be
   164  	// included.
   165  	//
   166  	// hcl: telemetry { prefix_filter = []string{"+<expr>", "+<expr>", ...} }
   167  	AllowedPrefixes []string `json:"allowed_prefixes,omitempty" mapstructure:"allowed_prefixes"`
   168  
   169  	// BlockedPrefixes is a list of filter rules to apply for blocking metrics
   170  	// by prefix. Use the 'prefix_filter' option and prefix rules with '-' to be
   171  	// excluded.
   172  	//
   173  	// hcl: telemetry { prefix_filter = []string{"-<expr>", "-<expr>", ...} }
   174  	BlockedPrefixes []string `json:"blocked_prefixes,omitempty" mapstructure:"blocked_prefixes"`
   175  
   176  	// MetricsPrefix is the prefix used to write stats values to.
   177  	// Default: "consul."
   178  	//
   179  	// hcl: telemetry { metrics_prefix = string }
   180  	MetricsPrefix string `json:"metrics_prefix,omitempty" mapstructure:"metrics_prefix"`
   181  
   182  	// StatsdAddr is the address of a statsd instance. If provided,
   183  	// metrics will be sent to that instance.
   184  	//
   185  	// hcl: telemetry { statsd_address = string }
   186  	StatsdAddr string `json:"statsd_address,omitempty" mapstructure:"statsd_address"`
   187  
   188  	// StatsiteAddr is the address of a statsite instance. If provided,
   189  	// metrics will be streamed to that instance.
   190  	//
   191  	// hcl: telemetry { statsite_address = string }
   192  	StatsiteAddr string `json:"statsite_address,omitempty" mapstructure:"statsite_address"`
   193  }
   194  
   195  // MergeDefaults copies any non-zero field from defaults into the current
   196  // config.
   197  func (c *TelemetryConfig) MergeDefaults(defaults *TelemetryConfig) {
   198  	if defaults == nil {
   199  		return
   200  	}
   201  	cfgPtrVal := reflect.ValueOf(c)
   202  	cfgVal := cfgPtrVal.Elem()
   203  	otherVal := reflect.ValueOf(*defaults)
   204  	for i := 0; i < cfgVal.NumField(); i++ {
   205  		f := cfgVal.Field(i)
   206  		if !f.IsValid() || !f.CanSet() {
   207  			continue
   208  		}
   209  		// See if the current value is a zero-value, if _not_ skip it
   210  		//
   211  		// No built in way to check for zero-values for all types so only
   212  		// implementing this for the types we actually have for now. Test failure
   213  		// should catch the case where we add new types later.
   214  		switch f.Kind() {
   215  		case reflect.Slice:
   216  			if !f.IsNil() {
   217  				continue
   218  			}
   219  		case reflect.Int, reflect.Int64: // time.Duration == int64
   220  			if f.Int() != 0 {
   221  				continue
   222  			}
   223  		case reflect.String:
   224  			if f.String() != "" {
   225  				continue
   226  			}
   227  		case reflect.Bool:
   228  			if f.Bool() != false {
   229  				continue
   230  			}
   231  		default:
   232  			// Needs implementing, should be caught by tests.
   233  			continue
   234  		}
   235  
   236  		// It's zero, copy it from defaults
   237  		f.Set(otherVal.Field(i))
   238  	}
   239  }
   240  
   241  func statsiteSink(cfg TelemetryConfig, hostname string) (metrics.MetricSink, error) {
   242  	addr := cfg.StatsiteAddr
   243  	if addr == "" {
   244  		return nil, nil
   245  	}
   246  	return metrics.NewStatsiteSink(addr)
   247  }
   248  
   249  func statsdSink(cfg TelemetryConfig, hostname string) (metrics.MetricSink, error) {
   250  	addr := cfg.StatsdAddr
   251  	if addr == "" {
   252  		return nil, nil
   253  	}
   254  	return metrics.NewStatsdSink(addr)
   255  }
   256  
   257  func dogstatdSink(cfg TelemetryConfig, hostname string) (metrics.MetricSink, error) {
   258  	addr := cfg.DogstatsdAddr
   259  	if addr == "" {
   260  		return nil, nil
   261  	}
   262  	sink, err := datadog.NewDogStatsdSink(addr, hostname)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	sink.SetTags(cfg.DogstatsdTags)
   267  	return sink, nil
   268  }
   269  
   270  func prometheusSink(cfg TelemetryConfig, hostname string) (metrics.MetricSink, error) {
   271  	if cfg.PrometheusRetentionTime.Nanoseconds() < 1 {
   272  		return nil, nil
   273  	}
   274  	prometheusOpts := prometheus.PrometheusOpts{
   275  		Expiration: cfg.PrometheusRetentionTime,
   276  	}
   277  	sink, err := prometheus.NewPrometheusSinkFrom(prometheusOpts)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	return sink, nil
   282  }
   283  
   284  func circonusSink(cfg TelemetryConfig, hostname string) (metrics.MetricSink, error) {
   285  	token := cfg.CirconusAPIToken
   286  	url := cfg.CirconusSubmissionURL
   287  	if token == "" && url == "" {
   288  		return nil, nil
   289  	}
   290  
   291  	conf := &circonus.Config{}
   292  	conf.Interval = cfg.CirconusSubmissionInterval
   293  	conf.CheckManager.API.TokenKey = token
   294  	conf.CheckManager.API.TokenApp = cfg.CirconusAPIApp
   295  	conf.CheckManager.API.URL = cfg.CirconusAPIURL
   296  	conf.CheckManager.Check.SubmissionURL = url
   297  	conf.CheckManager.Check.ID = cfg.CirconusCheckID
   298  	conf.CheckManager.Check.ForceMetricActivation = cfg.CirconusCheckForceMetricActivation
   299  	conf.CheckManager.Check.InstanceID = cfg.CirconusCheckInstanceID
   300  	conf.CheckManager.Check.SearchTag = cfg.CirconusCheckSearchTag
   301  	conf.CheckManager.Check.DisplayName = cfg.CirconusCheckDisplayName
   302  	conf.CheckManager.Check.Tags = cfg.CirconusCheckTags
   303  	conf.CheckManager.Broker.ID = cfg.CirconusBrokerID
   304  	conf.CheckManager.Broker.SelectTag = cfg.CirconusBrokerSelectTag
   305  
   306  	if conf.CheckManager.Check.DisplayName == "" {
   307  		conf.CheckManager.Check.DisplayName = "Consul"
   308  	}
   309  
   310  	if conf.CheckManager.API.TokenApp == "" {
   311  		conf.CheckManager.API.TokenApp = "consul"
   312  	}
   313  
   314  	if conf.CheckManager.Check.SearchTag == "" {
   315  		conf.CheckManager.Check.SearchTag = "service:consul"
   316  	}
   317  
   318  	sink, err := circonus.NewCirconusSink(conf)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	sink.Start()
   323  	return sink, nil
   324  }
   325  
   326  // InitTelemetry configures go-metrics based on map of telemetry config
   327  // values as returned by Runtimecfg.Config().
   328  func InitTelemetry(cfg TelemetryConfig) (*metrics.InmemSink, error) {
   329  	// Setup telemetry
   330  	// Aggregate on 10 second intervals for 1 minute. Expose the
   331  	// metrics over stderr when there is a SIGUSR1 received.
   332  	memSink := metrics.NewInmemSink(10*time.Second, time.Minute)
   333  	metrics.DefaultInmemSignal(memSink)
   334  	metricsConf := metrics.DefaultConfig(cfg.MetricsPrefix)
   335  	metricsConf.EnableHostname = !cfg.DisableHostname
   336  	metricsConf.FilterDefault = cfg.FilterDefault
   337  	metricsConf.AllowedPrefixes = cfg.AllowedPrefixes
   338  	metricsConf.BlockedPrefixes = cfg.BlockedPrefixes
   339  
   340  	var sinks metrics.FanoutSink
   341  	addSink := func(name string, fn func(TelemetryConfig, string) (metrics.MetricSink, error)) error {
   342  		s, err := fn(cfg, metricsConf.HostName)
   343  		if err != nil {
   344  			return err
   345  		}
   346  		if s != nil {
   347  			sinks = append(sinks, s)
   348  		}
   349  		return nil
   350  	}
   351  
   352  	if err := addSink("statsite", statsiteSink); err != nil {
   353  		return nil, err
   354  	}
   355  	if err := addSink("statsd", statsdSink); err != nil {
   356  		return nil, err
   357  	}
   358  	if err := addSink("dogstatd", dogstatdSink); err != nil {
   359  		return nil, err
   360  	}
   361  	if err := addSink("circonus", circonusSink); err != nil {
   362  		return nil, err
   363  	}
   364  	if err := addSink("prometheus", prometheusSink); err != nil {
   365  		return nil, err
   366  	}
   367  
   368  	if len(sinks) > 0 {
   369  		sinks = append(sinks, memSink)
   370  		metrics.NewGlobal(metricsConf, sinks)
   371  	} else {
   372  		metricsConf.EnableHostname = false
   373  		metrics.NewGlobal(metricsConf, memSink)
   374  	}
   375  	return memSink, nil
   376  }