Skip to content

Commit

Permalink
Issue #147: Support multiple metrics libraries
Browse files Browse the repository at this point in the history
  • Loading branch information
magiconair committed Aug 29, 2016
1 parent 34d6959 commit 8a34961
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 85 deletions.
11 changes: 10 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,16 @@ func startAdmin(cfg *config.Config) {
}

func initMetrics(cfg *config.Config) {
if err := metrics.Init(cfg.Metrics); err != nil {
if cfg.Metrics.Target == "" {
log.Printf("[INFO] Metrics disabled")
return
}

var err error
if metrics.DefaultRegistry, err = metrics.NewRegistry(cfg.Metrics); err != nil {
exit.Fatal("[FATAL] ", err)
}
if route.ServiceRegistry, err = metrics.NewRegistry(cfg.Metrics); err != nil {
exit.Fatal("[FATAL] ", err)
}
}
Expand Down
81 changes: 81 additions & 0 deletions metrics/gometrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package metrics

import (
"errors"
"fmt"
"log"
"net"
"os"
"sort"
"time"

graphite "github.com/cyberdelia/go-metrics-graphite"
statsd "github.com/pubnub/go-metrics-statsd"
gm "github.com/rcrowley/go-metrics"
)

// gmStdoutRegistry returns a go-metrics registry that reports to stdout.
func gmStdoutRegistry(interval time.Duration) (Registry, error) {
logger := log.New(os.Stderr, "localhost: ", log.Lmicroseconds)
r := gm.NewRegistry()
go gm.Log(r, interval, logger)
return &gmRegistry{r}, nil
}

// gmGraphiteRegistry returns a go-metrics registry that reports to a Graphite server.
func gmGraphiteRegistry(prefix, addr string, interval time.Duration) (Registry, error) {
if addr == "" {
return nil, errors.New(" graphite addr missing")
}

a, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, fmt.Errorf(" cannot connect to Graphite: %s", err)
}

r := gm.NewRegistry()
go graphite.Graphite(r, interval, prefix, a)
return &gmRegistry{r}, nil
}

// gmStatsDRegistry returns a go-metrics registry that reports to a StatsD server.
func gmStatsDRegistry(prefix, addr string, interval time.Duration) (Registry, error) {
if addr == "" {
return nil, errors.New(" statsd addr missing")
}

a, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, fmt.Errorf(" cannot connect to StatsD: %s", err)
}

r := gm.NewRegistry()
go statsd.StatsD(r, interval, prefix, a)
return &gmRegistry{r}, nil
}

// gmRegistry implements the Registry interface
// using the github.com/rcrowley/go-metrics library.
type gmRegistry struct {
r gm.Registry
}

func (p *gmRegistry) Names() (names []string) {
p.r.Each(func(name string, _ interface{}) {
names = append(names, name)
})
sort.Strings(names)
return names
}

func (p *gmRegistry) Unregister(name string) {
p.r.Unregister(name)
}

func (p *gmRegistry) UnregisterAll() {
p.r.UnregisterAll()
}

func (p *gmRegistry) GetTimer(name string) Timer {
return gm.GetOrRegisterTimer(name, p.r)
}
85 changes: 25 additions & 60 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,51 @@
// Package metrics provides functions for collecting
// and managing metrics through different metrics libraries.
//
// Metrics library implementations must implement the
// Registry interface in the package.
package metrics

import (
"errors"
"fmt"
"log"
"net"
"net/url"
"os"
"path/filepath"
"strings"
"time"

"github.com/cyberdelia/go-metrics-graphite"
"github.com/eBay/fabio/config"
"github.com/eBay/fabio/exit"
"github.com/pubnub/go-metrics-statsd"
gometrics "github.com/rcrowley/go-metrics"
)

var pfx string
// DefaultRegistry stores the metrics library provider.
var DefaultRegistry Registry = NoopRegistry{}

// ServiceRegistry contains a separate metrics registry for
// the timers for all targets to avoid conflicts
// with globally registered timers.
var ServiceRegistry = gometrics.NewRegistry()

func Init(cfg config.Metrics) error {
pfx = cfg.Prefix
if pfx == "default" {
pfx = defaultPrefix()
// NewRegistry creates a new metrics registry.
func NewRegistry(cfg config.Metrics) (r Registry, err error) {
prefix := cfg.Prefix
if prefix == "default" {
prefix = defaultPrefix()
}

switch cfg.Target {
case "stdout":
log.Printf("[INFO] Sending metrics to stdout")
return initStdout(cfg.Interval)
return gmStdoutRegistry(cfg.Interval)

case "graphite":
if cfg.GraphiteAddr == "" {
return errors.New("metrics: graphite addr missing")
}
log.Printf("[INFO] Sending metrics to Graphite on %s as %q", cfg.GraphiteAddr, prefix)
return gmGraphiteRegistry(prefix, cfg.GraphiteAddr, cfg.Interval)

log.Printf("[INFO] Sending metrics to Graphite on %s as %q", cfg.GraphiteAddr, pfx)
return initGraphite(cfg.GraphiteAddr, cfg.Interval)
case "statsd":
if cfg.StatsDAddr == "" {
return errors.New("metrics: statsd addr missing")
}
log.Printf("[INFO] Sending metrics to StatsD on %s as %q", cfg.StatsDAddr, prefix)
return gmStatsDRegistry(prefix, cfg.StatsDAddr, cfg.Interval)

log.Printf("[INFO] Sending metrics to StatsD on %s as %q", cfg.StatsDAddr, pfx)
return initStatsD(cfg.StatsDAddr, cfg.Interval)

case "":
log.Printf("[INFO] Metrics disabled")
default:
exit.Fatal("[FATAL] Invalid metrics target ", cfg.Target)
}
return nil
panic("unreachable")
}

// TargetName returns the metrics name from the given parameters.
func TargetName(service, host, path string, targetURL *url.URL) string {
return strings.Join([]string{
clean(service),
Expand All @@ -67,6 +55,9 @@ func TargetName(service, host, path string, targetURL *url.URL) string {
}, ".")
}

// clean creates safe names for graphite reporting by replacing
// some characters with underscores.
// TODO(fs): This may need updating for other metrics backends.
func clean(s string) string {
if s == "" {
return "_"
Expand All @@ -79,6 +70,8 @@ func clean(s string) string {
// stubbed out for testing
var hostname = os.Hostname

// defaultPrefix determines the default metrics prefix from
// the current hostname and the name of the executable.
func defaultPrefix() string {
host, err := hostname()
if err != nil {
Expand All @@ -87,31 +80,3 @@ func defaultPrefix() string {
exe := filepath.Base(os.Args[0])
return clean(host) + "." + clean(exe)
}

func initStdout(interval time.Duration) error {
logger := log.New(os.Stderr, "localhost: ", log.Lmicroseconds)
go gometrics.Log(gometrics.DefaultRegistry, interval, logger)
go gometrics.Log(ServiceRegistry, interval, logger)
return nil
}

func initGraphite(addr string, interval time.Duration) error {
a, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return fmt.Errorf("metrics: cannot connect to Graphite: %s", err)
}

go graphite.Graphite(gometrics.DefaultRegistry, interval, pfx, a)
go graphite.Graphite(ServiceRegistry, interval, pfx, a)
return nil
}

func initStatsD(addr string, interval time.Duration) error {
a, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return fmt.Errorf("metrics: cannot connect to StatsD: %s", err)
}
go statsd.StatsD(gometrics.DefaultRegistry, interval, pfx, a)
go statsd.StatsD(ServiceRegistry, interval, pfx, a)
return nil
}
25 changes: 25 additions & 0 deletions metrics/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package metrics

import "time"

// NoopRegistry is a stub implementation of the Registry interface.
type NoopRegistry struct{}

func (p NoopRegistry) Names() []string { return nil }

func (p NoopRegistry) Unregister(name string) {}

func (p NoopRegistry) UnregisterAll() {}

func (p NoopRegistry) GetTimer(name string) Timer { return noopTimer }

var noopTimer = NoopTimer{}

// NoopTimer is a stub implementation of the Timer interface.
type NoopTimer struct{}

func (t NoopTimer) UpdateSince(start time.Time) {}

func (t NoopTimer) Rate1() float64 { return 0 }

func (t NoopTimer) Percentile(nth float64) float64 { return 0 }
40 changes: 40 additions & 0 deletions metrics/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package metrics

import "time"

// Registry defines an interface for metrics values which
// can be implemented by different metrics libraries.
// An implementation must be safe to be used by multiple
// go routines.
type Registry interface {
// Names returns the list of registered metrics acquired
// through the GetXXX() functions. It should return them
// sorted in alphabetical order.
Names() []string

// Unregister removes the registered metric and stops
// reporting it to an external backend.
Unregister(name string)

// UnregisterAll removes all registered metrics and stops
// reporting them to an external backend.
UnregisterAll()

// GetTimer returns a timer metric for the given name.
// If the metric does not exist yet it should be created
// otherwise the existing metric should be returned.
GetTimer(name string) Timer
}

// Timer defines a metric for counting and timing durations for events.
type Timer interface {
// Percentile returns the nth percentile of the duration.
Percentile(nth float64) float64

// Rate1 returns the 1min rate.
Rate1() float64

// UpdateSince counts an event and records the duration
// as the delta between 'start' and the function is called.
UpdateSince(start time.Time)
}
7 changes: 3 additions & 4 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,21 @@ import (
"time"

"github.com/eBay/fabio/config"

gometrics "github.com/rcrowley/go-metrics"
"github.com/eBay/fabio/metrics"
)

// Proxy is a dynamic reverse proxy.
type Proxy struct {
tr http.RoundTripper
cfg config.Proxy
requests gometrics.Timer
requests metrics.Timer
}

func New(tr http.RoundTripper, cfg config.Proxy) *Proxy {
return &Proxy{
tr: tr,
cfg: cfg,
requests: gometrics.GetOrRegisterTimer("requests", gometrics.DefaultRegistry),
requests: metrics.DefaultRegistry.GetTimer("requests"),
}
}

Expand Down
4 changes: 1 addition & 3 deletions route/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"strings"

"github.com/eBay/fabio/metrics"

gometrics "github.com/rcrowley/go-metrics"
)

// Route maps a path prefix to one or more target URLs.
Expand Down Expand Up @@ -49,7 +47,7 @@ func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float6
}

name := metrics.TargetName(service, r.Host, r.Path, targetURL)
timer := gometrics.GetOrRegisterTimer(name, metrics.ServiceRegistry)
timer := ServiceRegistry.GetTimer(name)

t := &Target{Service: service, Tags: tags, URL: targetURL, FixedWeight: fixedWeight, Timer: timer, timerName: name}
r.Targets = append(r.Targets, t)
Expand Down
9 changes: 6 additions & 3 deletions route/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ var errNoMatch = errors.New("route: no target match")
// table stores the active routing table. Must never be nil.
var table atomic.Value

// ServiceRegistry stores the metrics for the services.
var ServiceRegistry metrics.Registry = metrics.NoopRegistry{}

// init initializes the routing table.
func init() {
table.Store(make(Table))
Expand Down Expand Up @@ -58,9 +61,9 @@ func syncRegistry(t Table) {
timers := map[string]bool{}

// get all registered timers
metrics.ServiceRegistry.Each(func(name string, m interface{}) {
for _, name := range ServiceRegistry.Names() {
timers[name] = false
})
}

// mark the ones from this table as active.
// this can also add new entries but we do not
Expand All @@ -77,7 +80,7 @@ func syncRegistry(t Table) {
// unregister inactive timers
for name, active := range timers {
if !active {
metrics.ServiceRegistry.Unregister(name)
ServiceRegistry.Unregister(name)
log.Printf("[INFO] Unregistered timer %s", name)
}
}
Expand Down
Loading

0 comments on commit 8a34961

Please sign in to comment.