From fb76256c2682e24e7ab624f76a4dfa25727f48bd Mon Sep 17 00:00:00 2001 From: matt maier Date: Mon, 18 Jul 2016 09:34:43 -0400 Subject: [PATCH] Add Circonus support for Telemetry metrics --- command/agent/command.go | 32 ++++++++ command/agent/config.go | 143 ++++++++++++++++++++++++++++++++++- command/agent/config_test.go | 45 +++++++++++ 3 files changed, 219 insertions(+), 1 deletion(-) diff --git a/command/agent/command.go b/command/agent/command.go index bb40fa2632..5c5888d6d0 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -15,6 +15,7 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/armon/go-metrics/circonus" "github.com/armon/go-metrics/datadog" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/watch" @@ -716,6 +717,37 @@ func (c *Command) Run(args []string) int { fanout = append(fanout, sink) } + if config.Telemetry.CirconusAPIToken != "" || config.Telemetry.CirconusCheckSubmissionURL != "" { + cfg := &circonus.Config{} + cfg.Interval = config.Telemetry.CirconusSubmitInterval + cfg.CheckManager.API.TokenKey = config.Telemetry.CirconusAPIToken + cfg.CheckManager.API.TokenApp = config.Telemetry.CirconusAPIApp + cfg.CheckManager.API.URL = config.Telemetry.CirconusAPIURL + cfg.CheckManager.Check.SubmissionURL = config.Telemetry.CirconusCheckSubmissionURL + cfg.CheckManager.Check.ID = config.Telemetry.CirconusCheckID + cfg.CheckManager.Check.ForceMetricActivation = config.Telemetry.CirconusCheckForceMetricActivation + cfg.CheckManager.Check.InstanceID = config.Telemetry.CirconusCheckInstanceID + cfg.CheckManager.Check.SearchTag = config.Telemetry.CirconusCheckSearchTag + cfg.CheckManager.Broker.ID = config.Telemetry.CirconusBrokerID + cfg.CheckManager.Broker.SelectTag = config.Telemetry.CirconusBrokerSelectTag + + if cfg.CheckManager.Check.InstanceID == "" { + cfg.CheckManager.Check.InstanceID = fmt.Sprintf("%s:%s", config.NodeName, config.Datacenter) + } + + if cfg.CheckManager.Check.SearchTag == "" { + cfg.CheckManager.Check.SearchTag = "service:consul" + } + + sink, err := circonus.NewCirconusSink(cfg) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to start Circonus sink. Got: %s", err)) + return 1 + } + sink.Start() + fanout = append(fanout, sink) + } + // Initialize the global sink if len(fanout) > 0 { fanout = append(fanout, inm) diff --git a/command/agent/config.go b/command/agent/config.go index 21a3fa454b..c637f7bc7c 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -130,6 +130,70 @@ type Telemetry struct { // DogStatsdTags are the global tags that should be sent with each packet to dogstatsd // It is a list of strings, where each string looks like "my_tag_name:my_tag_value" DogStatsdTags []string `mapstructure:"dogstatsd_tags"` + + // Circonus: see https://github.com/circonus-labs/circonus-gometrics + // for more details on the various configuration options. + // Valid configuration combinations: + // - CirconusAPIToken + // metric management enabled (search for existing check or create a new one) + // - CirconusSubmissionUrl + // metric management disabled (use check with specified submission_url, + // broker must be using a public SSL certificate) + // - CirconusAPIToken + CirconusCheckSubmissionURL + // metric management enabled (use check with specified submission_url) + // - CirconusAPIToken + CirconusCheckID + // metric management enabled (use check with specified id) + + // CirconusAPIToken is a valid API Token used to create/manage check. If provided, + // metric management is enabled. + // Default: none + CirconusAPIToken string `mapstructure:"circonus_api_token"` + // CirconusAPIApp is an app name associated with API token. + // Default: "circonus-gometrics" + CirconusAPIApp string `mapstructure:"circonus_api_app"` + // CirconusAPIURL is the base URL to use for contacting the Circonus API. + // Default: "https://api.circonus.com/v2" + CirconusAPIURL string `mapstructure:"circonus_api_url"` + // CirconusSubmitInterval is the interval at which metrics are submitted to Circonus. + // Default: 10s + CirconusSubmitInterval string `mapstructure:"circonus_submission_interval"` + // CirconusCheckSubmissionURL is the check.config.submission_url field from a + // previously created HTTPTRAP check. + // Default: none + CirconusCheckSubmissionURL string `mapstructure:"circonus_submission_url"` + // CirconusCheckID is the check id (not check bundle id) from a previously created + // HTTPTRAP check. The numeric portion of the check._cid field. + // Default: none + CirconusCheckID string `mapstructure:"circonus_check_id"` + // CirconusCheckForceMetricActivation will force enabling metrics, as they are encountered, + // if the metric already exists and is NOT active. If check management is enabled, the default + // behavior is to add new metrics as they are encoutered. If the metric already exists in the + // check, it will *NOT* be activated. This setting overrides that behavior. + // Default: "false" + CirconusCheckForceMetricActivation string `mapstructure:"circonus_check_force_metric_activation"` + // CirconusCheckInstanceID serves to uniquely identify the metrics comming from this "instance". + // It can be used to maintain metric continuity with transient or ephemeral instances as + // they move around within an infrastructure. + // Default: hostname:app + CirconusCheckInstanceID string `mapstructure:"circonus_check_instance_id"` + // CirconusCheckSearchTag is a special tag which, when coupeled with the instance id, helps to + // narrow down the search results when neither a Submission URL or Check ID is provided. + // Default: service:app (e.g. service:consul) + CirconusCheckSearchTag string `mapstructure:"circonus_check_search_tag"` + // CirconusBrokerID is an explicit broker to use when creating a new check. The numeric portion + // of broker._cid. If metric management is enabled and neither a Submission URL nor Check ID + // is provided, an attempt will be made to search for an existing check using Instance ID and + // Search Tag. If one is not found, a new HTTPTRAP check will be created. + // Default: use Select Tag if provided, otherwise, a random Enterprise Broker associated + // with the specified API token or the default Circonus Broker. + // Default: none + CirconusBrokerID string `mapstructure:"circonus_broker_id"` + // CirconusBrokerSelectTag is a special tag which will be used to select a broker when + // a Broker ID is not provided. The best use of this is to as a hint for which broker + // should be used based on *where* this particular instance is running. + // (e.g. a specific geo location or datacenter, dc:sfo) + // Default: none + CirconusBrokerSelectTag string `mapstructure:"circonus_broker_search_tag"` } // Config is the configuration that can be set for an Agent. @@ -690,6 +754,47 @@ func DecodeConfig(r io.Reader) (*Config, error) { result.Telemetry.DogStatsdTags[i] = sub[i].(string) } } + + if sub, ok := obj["circonus_api_token"]; ok && result.Telemetry.CirconusAPIToken == "" { + result.Telemetry.CirconusAPIToken = sub.(string) + } + + if sub, ok := obj["circonus_api_app"]; ok && result.Telemetry.CirconusAPIApp == "" { + result.Telemetry.CirconusAPIApp = sub.(string) + } + + if sub, ok := obj["circonus_api_url"]; ok && result.Telemetry.CirconusAPIURL == "" { + result.Telemetry.CirconusAPIURL = sub.(string) + } + + if sub, ok := obj["circonus_submission_url"]; ok && result.Telemetry.CirconusCheckSubmissionURL == "" { + result.Telemetry.CirconusCheckSubmissionURL = sub.(string) + } + + if sub, ok := obj["circonus_submit_interval"]; ok && result.Telemetry.CirconusSubmitInterval == "" { + result.Telemetry.CirconusSubmitInterval = sub.(string) + } + + if sub, ok := obj["circonus_check_id"]; ok && result.Telemetry.CirconusCheckID == "" { + result.Telemetry.CirconusCheckID = sub.(string) + } + if sub, ok := obj["circonus_check_force_metric_activation"]; ok && result.Telemetry.CirconusCheckForceMetricActivation == "" { + result.Telemetry.CirconusCheckForceMetricActivation = sub.(string) + } + if sub, ok := obj["circonus_check_instance_id"]; ok && result.Telemetry.CirconusCheckInstanceID == "" { + result.Telemetry.CirconusCheckInstanceID = sub.(string) + } + if sub, ok := obj["circonus_check_search_tag"]; ok && result.Telemetry.CirconusCheckSearchTag == "" { + result.Telemetry.CirconusCheckSearchTag = sub.(string) + } + + if sub, ok := obj["circonus_broker_id"]; ok && result.Telemetry.CirconusBrokerID == "" { + result.Telemetry.CirconusBrokerID = sub.(string) + } + + if sub, ok := obj["circonus_broker_select_tag"]; ok && result.Telemetry.CirconusBrokerSelectTag == "" { + result.Telemetry.CirconusBrokerSelectTag = sub.(string) + } } // Decode @@ -711,7 +816,10 @@ func DecodeConfig(r io.Reader) (*Config, error) { // use mapstructure decoding, so we need to account for those as well. allowedKeys := []string{ "service", "services", "check", "checks", "statsd_addr", "statsite_addr", "statsite_prefix", - "dogstatsd_addr", "dogstatsd_tags", + "dogstatsd_addr", "dogstatsd_tags", "circonus_api_token", "circonus_api_app", + "circonus_api_url", "circonus_submission_url", "circonus_submit_interval", + "circonus_check_id", "circonus_check_force_metric_activation", "circonus_check_instance_id", + "circonus_check_search_tag", "circonus_broker_id", "circonus_broker_select_tag", } var unused []string @@ -1071,6 +1179,39 @@ func MergeConfig(a, b *Config) *Config { if b.Telemetry.DogStatsdTags != nil { result.Telemetry.DogStatsdTags = b.Telemetry.DogStatsdTags } + if b.Telemetry.CirconusAPIToken != "" { + result.Telemetry.CirconusAPIToken = b.Telemetry.CirconusAPIToken + } + if b.Telemetry.CirconusAPIApp != "" { + result.Telemetry.CirconusAPIApp = b.Telemetry.CirconusAPIApp + } + if b.Telemetry.CirconusAPIURL != "" { + result.Telemetry.CirconusAPIURL = b.Telemetry.CirconusAPIURL + } + if b.Telemetry.CirconusCheckSubmissionURL != "" { + result.Telemetry.CirconusCheckSubmissionURL = b.Telemetry.CirconusCheckSubmissionURL + } + if b.Telemetry.CirconusSubmitInterval != "" { + result.Telemetry.CirconusSubmitInterval = b.Telemetry.CirconusSubmitInterval + } + if b.Telemetry.CirconusCheckID != "" { + result.Telemetry.CirconusCheckID = b.Telemetry.CirconusCheckID + } + if b.Telemetry.CirconusCheckForceMetricActivation != "" { + result.Telemetry.CirconusCheckForceMetricActivation = b.Telemetry.CirconusCheckForceMetricActivation + } + if b.Telemetry.CirconusCheckInstanceID != "" { + result.Telemetry.CirconusCheckInstanceID = b.Telemetry.CirconusCheckInstanceID + } + if b.Telemetry.CirconusCheckSearchTag != "" { + result.Telemetry.CirconusCheckSearchTag = b.Telemetry.CirconusCheckSearchTag + } + if b.Telemetry.CirconusBrokerID != "" { + result.Telemetry.CirconusBrokerID = b.Telemetry.CirconusBrokerID + } + if b.Telemetry.CirconusBrokerSelectTag != "" { + result.Telemetry.CirconusBrokerSelectTag = b.Telemetry.CirconusBrokerSelectTag + } if b.EnableDebug { result.EnableDebug = true } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 3fc0a6880d..fd12c90a2f 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -725,6 +725,51 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } + // Circonus settings + input = `{"circonus_api_token": "12345678-1234-1234-12345678", "circonus_api_app": "testApp", + "circonus_api_url": "https://api.host.foo/v2", "circonus_submit_interval": "15s", + "circonus_submission_url": "https://submit.host.bar:123/one/two/three", + "circonus_check_id": "12345", "circonus_check_force_metric_activation": "true", + "circonus_check_instance_id": "a:b", "circonus_check_search_tag": "c:d", + "circonus_broker_id": "6789", "circonus_broker_select_tag": "e:f"}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + if config.Telemetry.CirconusAPIToken != "12345678-1234-1234-12345678" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusAPIApp != "testApp" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusAPIURL != "https://api.host.foo/v2" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusSubmitInterval != "15s" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusCheckSubmissionURL != "https://submit.host.bar:123/one/two/three" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusCheckID != "12345" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusCheckForceMetricActivation != "true" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusCheckInstanceID != "a:b" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusCheckSearchTag != "c:d" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusBrokerID != "6789" { + t.Fatalf("bad: %#v", config) + } + if config.Telemetry.CirconusBrokerSelectTag != "e:f" { + t.Fatalf("bad: %#v", config) + } + // New telemetry input = `{"telemetry": { "statsite_prefix": "my_prefix", "statsite_address": "127.0.0.1:7250", "statsd_address":"127.0.0.1:7251", "disable_hostname": true, "dogstatsd_addr": "1.1.1.1:111", "dogstatsd_tags": [ "tag_1:val_1" ] } }` config, err = DecodeConfig(bytes.NewReader([]byte(input)))