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