1package lib
2
3import (
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.
21type 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.
197func (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
241func 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
249func 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
257func 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
270func 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
284func 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().
328func 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}
377