diff --git a/config/config.go b/config/config.go index 884fcbb42..6b6f9b01d 100644 --- a/config/config.go +++ b/config/config.go @@ -67,11 +67,16 @@ type Runtime struct { } type Metrics struct { - Target string - Prefix string - Interval time.Duration - GraphiteAddr string - StatsDAddr string + Target string + Prefix string + Interval time.Duration + GraphiteAddr string + StatsDAddr string + CirconusAPIKey string + CirconusAPIApp string + CirconusAPIURL string + CirconusCheckID string + CirconusBrokerID string } type Registry struct { diff --git a/config/default.go b/config/default.go index f73a27d4a..48cd63ec9 100644 --- a/config/default.go +++ b/config/default.go @@ -40,8 +40,9 @@ var Default = &Config{ Color: "light-green", }, Metrics: Metrics{ - Prefix: "default", - Interval: 30 * time.Second, + Prefix: "default", + Interval: 30 * time.Second, + CirconusAPIApp: "fabio", }, CertSources: map[string]CertSource{}, } diff --git a/config/load.go b/config/load.go index 29d8f6055..721828203 100644 --- a/config/load.go +++ b/config/load.go @@ -110,6 +110,11 @@ func load(p *properties.Properties) (cfg *Config, err error) { f.DurationVar(&cfg.Metrics.Interval, "metrics.interval", Default.Metrics.Interval, "metrics reporting interval") f.StringVar(&cfg.Metrics.GraphiteAddr, "metrics.graphite.addr", Default.Metrics.GraphiteAddr, "graphite server address") f.StringVar(&cfg.Metrics.StatsDAddr, "metrics.statsd.addr", Default.Metrics.StatsDAddr, "statsd server address") + f.StringVar(&cfg.Metrics.CirconusAPIKey, "metrics.circonus.apikey", Default.Metrics.CirconusAPIKey, "Circonus API token key") + f.StringVar(&cfg.Metrics.CirconusAPIApp, "metrics.circonus.apiapp", Default.Metrics.CirconusAPIApp, "Circonus API token app") + f.StringVar(&cfg.Metrics.CirconusAPIURL, "metrics.circonus.apiurl", Default.Metrics.CirconusAPIURL, "Circonus API URL") + f.StringVar(&cfg.Metrics.CirconusBrokerID, "metrics.circonus.brokerid", Default.Metrics.CirconusBrokerID, "Circonus Broker ID") + f.StringVar(&cfg.Metrics.CirconusCheckID, "metrics.circonus.checkid", Default.Metrics.CirconusCheckID, "Circonus Check ID") f.StringVar(&cfg.Registry.Backend, "registry.backend", Default.Registry.Backend, "registry backend") f.StringVar(&cfg.Registry.File.Path, "registry.file.path", Default.Registry.File.Path, "path to file based routing table") f.StringVar(&cfg.Registry.Static.Routes, "registry.static.routes", Default.Registry.Static.Routes, "static routes") diff --git a/config/load_test.go b/config/load_test.go index 525b05e9b..6e8bd2e8d 100644 --- a/config/load_test.go +++ b/config/load_test.go @@ -47,6 +47,12 @@ metrics.target = graphite metrics.prefix = someprefix metrics.interval = 5s metrics.graphite.addr = 5.6.7.8:9999 +metrics.statsd.addr = 6.7.8.9:9999 +metrics.circonus.apikey = circonus-apikey +metrics.circonus.apiapp = circonus-apiapp +metrics.circonus.apiurl = circonus-apiurl +metrics.circonus.brokerid = circonus-brokerid +metrics.circonus.checkid = circonus-checkid runtime.gogc = 666 runtime.gomaxprocs = 12 ui.addr = 7.8.9.0:1234 @@ -117,10 +123,16 @@ aws.apigw.cert.cn = furb }, }, Metrics: Metrics{ - Target: "graphite", - Prefix: "someprefix", - Interval: 5 * time.Second, - GraphiteAddr: "5.6.7.8:9999", + Target: "graphite", + Prefix: "someprefix", + Interval: 5 * time.Second, + GraphiteAddr: "5.6.7.8:9999", + StatsDAddr: "6.7.8.9:9999", + CirconusAPIKey: "circonus-apikey", + CirconusAPIApp: "circonus-apiapp", + CirconusAPIURL: "circonus-apiurl", + CirconusBrokerID: "circonus-brokerid", + CirconusCheckID: "circonus-checkid", }, Runtime: Runtime{ GOGC: 666, diff --git a/fabio.properties b/fabio.properties index 336c8171c..8f416e41a 100644 --- a/fabio.properties +++ b/fabio.properties @@ -462,6 +462,7 @@ # stdout: report metrics to stdout # graphite: report metrics to Graphite on ${metrics.graphite.addr} # statsd: report metrics to StatsD on ${metrics.statsd.addr} +# circonus: report metrics to Circonus (http://circonus.com/) # # The default is # @@ -505,6 +506,56 @@ # metrics.statsd.addr = +# metrics.circonus.apikey configures the API token key to use when +# submitting metrics to Circonus. See: https://login.circonus.com/user/tokens +# This is required when ${metrics.target} is set to "circonus". +# +# The default is +# +# metrics.circonus.apikey = + + +# metrics.circonus.apiapp configures the API token app to use when +# submitting metrics to Circonus. See: https://login.circonus.com/user/tokens +# This is optional when ${metrics.target} is set to "circonus". +# +# The default is +# +# metrics.circonus.apiapp = fabio + + +# metrics.circonus.apiurl configures the API URL to use when +# submitting metrics to Circonus. https://api.circonus.com/v2/ +# will be used if no specific URL is provided. +# This is optional when ${metrics.target} is set to "circonus". +# +# The default is +# +# metrics.circonus.apiurl = + + +# metrics.circonus.brokerid configures a specific broker to use when +# creating a check for submitting metrics to Circonus. +# This is optional when ${metrics.target} is set to "circonus". +# Optional for public brokers, required for Inside brokers. +# Only applicable if a check is being created. +# +# The default is +# +# metrics.circonus.brokerid = + + +# metrics.circonus.checkid configures a specific check to use when +# submitting metrics to Circonus. +# This is optional when ${metrics.target} is set to "circonus". +# An attempt will be made to search for a previously created check, +# if no applicable check is found, one will be created. +# +# The default is +# +# metrics.circonus.checkid = + + # runtime.gogc configures GOGC (the GC target percentage). # # Setting runtime.gogc is equivalent to setting the GOGC diff --git a/metrics/circonus.go b/metrics/circonus.go new file mode 100644 index 000000000..840151574 --- /dev/null +++ b/metrics/circonus.go @@ -0,0 +1,130 @@ +package metrics + +import ( + "errors" + "fmt" + "log" + "os" + "sync" + "time" + + cgm "github.com/circonus-labs/circonus-gometrics" +) + +var ( + circonus *cgmRegistry + once sync.Once +) + +const serviceName = "fabio" + +// circonusRegistry returns a provider that reports to Circonus. +func circonusRegistry(prefix string, + circKey string, + circApp string, + circURL string, + circBrokerID string, + circCheckID string, + interval time.Duration) (Registry, error) { + + var initError error + + once.Do(func() { + if circKey == "" { + initError = errors.New("metrics: Circonus API token key") + return + } + + if circApp == "" { + circApp = serviceName + } + + host, err := os.Hostname() + if err != nil { + initError = fmt.Errorf("metrics: unable to initialize Circonus %s", err) + return + } + + cfg := &cgm.Config{} + + cfg.CheckManager.API.TokenKey = circKey + cfg.CheckManager.API.TokenApp = circApp + cfg.CheckManager.API.URL = circURL + cfg.CheckManager.Check.ID = circCheckID + cfg.CheckManager.Broker.ID = circBrokerID + cfg.Interval = fmt.Sprintf("%.0fs", interval.Seconds()) + cfg.CheckManager.Check.InstanceID = host + cfg.CheckManager.Check.DisplayName = fmt.Sprintf("%s /%s", host, serviceName) + cfg.CheckManager.Check.SearchTag = fmt.Sprintf("service:%s", serviceName) + + metrics, err := cgm.NewCirconusMetrics(cfg) + if err != nil { + initError = fmt.Errorf("metrics: unable to initialize Circonus %s", err) + return + } + + circonus = &cgmRegistry{metrics, prefix} + + metrics.Start() + + log.Print("[INFO] Sending metrics to Circonus") + }) + + return circonus, initError +} + +type cgmRegistry struct { + metrics *cgm.CirconusMetrics + prefix string +} + +// Names is not supported by Circonus. +func (m *cgmRegistry) Names() []string { return nil } + +// Unregister is implicitly supported by Circonus, +// stop submitting the metric and it stops being sent to Circonus. +func (m *cgmRegistry) Unregister(name string) {} + +// UnregisterAll is implicitly supported by Circonus, +// stop submitting metrics and they will no longer be sent to Circonus. +func (m *cgmRegistry) UnregisterAll() {} + +// GetCounter returns a counter for the given metric name. +func (m *cgmRegistry) GetCounter(name string) Counter { + metricName := fmt.Sprintf("%s`%s", m.prefix, name) + return &cgmCounter{m.metrics, metricName} +} + +// GetTimer returns a timer for the given metric name. +func (m *cgmRegistry) GetTimer(name string) Timer { + metricName := fmt.Sprintf("%s`%s", m.prefix, name) + return &cgmTimer{m.metrics, metricName} +} + +type cgmCounter struct { + metrics *cgm.CirconusMetrics + name string +} + +// Inc increases the counter by n. +func (c *cgmCounter) Inc(n int64) { + c.metrics.IncrementByValue(c.name, uint64(n)) +} + +type cgmTimer struct { + metrics *cgm.CirconusMetrics + name string +} + +// Percentile is not supported by Circonus. +func (t *cgmTimer) Percentile(nth float64) float64 { return 0 } + +// Rate1 is not supported by Circonus. +func (t *cgmTimer) Rate1() float64 { return 0 } + +// UpdateSince adds delta between start and current time as +// a sample to a histogram. The histogram is created if it +// does not already exist. +func (t *cgmTimer) UpdateSince(start time.Time) { + t.metrics.Timing(t.name, float64(time.Since(start))) +} diff --git a/metrics/circonus_test.go b/metrics/circonus_test.go new file mode 100644 index 000000000..bef1f7b42 --- /dev/null +++ b/metrics/circonus_test.go @@ -0,0 +1,83 @@ +package metrics + +import ( + "os" + "testing" + "time" +) + +func TestRegistry(t *testing.T) { + t.Log("Testing registry interface") + + p := &cgmRegistry{} + + t.Log("\tNames()") + names := p.Names() + if names != nil { + t.Errorf("Expected nil got '%+v'", names) + } + + t.Log("\tUnregister()") + p.Unregister("foo") + + t.Log("\tUnregisterAll()") + p.UnregisterAll() + + t.Log("\tGetTimer()") + timer := p.GetTimer("foo") + if timer == nil { + t.Error("Expected a timer, got nil") + } +} + +func TestTimer(t *testing.T) { + t.Log("Testing timer interface") + + timer := &cgmTimer{} + + t.Log("\tPercentile()") + pct := timer.Percentile(99.9) + if pct != 0 { + t.Errorf("Expected 0 got '%+v'", pct) + } + + t.Log("\tRate1()") + rate := timer.Rate1() + if rate != 0 { + t.Errorf("Expected 0 got '%+v'", rate) + } +} + +func TestAll(t *testing.T) { + start := time.Now() + + if os.Getenv("CIRCONUS_API_TOKEN") == "" { + t.Skip("skipping test; $CIRCONUS_API_TOKEN not set") + } + + t.Log("Testing cgm functionality -- this *will* create/use a check") + + apiKey := os.Getenv("CIRCONUS_API_TOKEN") + apiApp := os.Getenv("CIRCONUS_API_APP") + apiURL := os.Getenv("CIRCONUS_API_URL") + brokerID := os.Getenv("CIRCONUS_BROKER_ID") + checkID := os.Getenv("CIRCONUS_CHECK_ID") + + interval, err := time.ParseDuration("60s") + if err != nil { + t.Fatalf("Unable to parse interval %+v", err) + } + + circ, err := circonusRegistry("test", apiKey, apiApp, apiURL, brokerID, checkID, interval) + if err != nil { + t.Fatalf("Unable to initialize Circonus +%v", err) + } + + counter := circ.GetCounter("fooCounter") + counter.Inc(3) + + timer := circ.GetTimer("fooTimer") + timer.UpdateSince(start) + + circonus.metrics.Flush() +} diff --git a/metrics/metrics.go b/metrics/metrics.go index 6c31937f0..0bc1c8ef7 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -39,6 +39,15 @@ func NewRegistry(cfg config.Metrics) (r Registry, err error) { log.Printf("[INFO] Sending metrics to StatsD on %s as %q", cfg.StatsDAddr, prefix) return gmStatsDRegistry(prefix, cfg.StatsDAddr, cfg.Interval) + case "circonus": + return circonusRegistry(prefix, + cfg.CirconusAPIKey, + cfg.CirconusAPIApp, + cfg.CirconusAPIURL, + cfg.CirconusBrokerID, + cfg.CirconusCheckID, + cfg.Interval) + default: exit.Fatal("[FATAL] Invalid metrics target ", cfg.Target) }