Skip to content

Commit

Permalink
Issue #151: Add support for Circonus metrics
Browse files Browse the repository at this point in the history
This patch adds support for sending metrics
to Circonus (http://circonus.com).

The code is squashed from PR #150 with minor
edits and was written by @maier.
  • Loading branch information
maier authored and magiconair committed Aug 30, 2016
1 parent add14ed commit 4a94696
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 11 deletions.
15 changes: 10 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
}
5 changes: 5 additions & 0 deletions config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
20 changes: 16 additions & 4 deletions config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
51 changes: 51 additions & 0 deletions fabio.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -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
Expand Down
130 changes: 130 additions & 0 deletions metrics/circonus.go
Original file line number Diff line number Diff line change
@@ -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)))
}
83 changes: 83 additions & 0 deletions metrics/circonus_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
9 changes: 9 additions & 0 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 4a94696

Please sign in to comment.