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 28, 2016
1 parent bbc2ce8 commit 042f725
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 78 deletions.
91 changes: 91 additions & 0 deletions metrics/gometrics/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Package gometrics provides an implementation of the
// metricslib.Provider interface using the github.com/rcrowley/go-metrics
// library.
package gometrics

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

"github.com/eBay/fabio/metrics/metricslib"

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

// StdoutProvider returns a provider that reports to stdout.
func StdoutProvider(interval time.Duration) (metricslib.Provider, error) {
registry := gm.NewRegistry()
logger := log.New(os.Stderr, "localhost: ", log.Lmicroseconds)
go gm.Log(gm.DefaultRegistry, interval, logger)
go gm.Log(registry, interval, logger)
return &gmProvider{registry}, nil
}

// GraphiteProvider returns a provider that reports to a Graphite server.
func GraphiteProvider(prefix, addr string, interval time.Duration) (metricslib.Provider, error) {
if addr == "" {
return nil, errors.New("metrics: graphite addr missing")
}

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

registry := gm.NewRegistry()
go graphite.Graphite(gm.DefaultRegistry, interval, prefix, a)
go graphite.Graphite(registry, interval, prefix, a)
return &gmProvider{registry}, nil
}

// StatsDProvider returns a provider that reports to a StatsD server.
func StatsDProvider(prefix, addr string, interval time.Duration) (metricslib.Provider, error) {
if addr == "" {
return nil, errors.New("metrics: statsd addr missing")
}

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

registry := gm.NewRegistry()
go statsd.StatsD(gm.DefaultRegistry, interval, prefix, a)
go statsd.StatsD(registry, interval, prefix, a)
return &gmProvider{registry}, nil
}

// gmProvider implements the metricslib.Provider interface
// using the github.com/rcrowley/go-metrics library.
type gmProvider struct {
// registry keeps track of registered metrics values
// to keep them separate from the default metrics.
registry gm.Registry
}

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

func (p *gmProvider) Unregister(name string) {
p.registry.Unregister(name)
}

func (p *gmProvider) UnregisterAll() {
p.registry.UnregisterAll()
}

func (p *gmProvider) GetTimer(name string) metricslib.Timer {
return gm.GetOrRegisterTimer(name, p.registry)
}
106 changes: 53 additions & 53 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,86 @@
// Package metrics provides functions for collecting
// and managing metrics through different metrics libraries.
//
// Metrics library implementations must implement the
// Provider interface in the metricslib package.
//
// The current implementation supports only a single
// metrics provider.
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"
"github.com/eBay/fabio/metrics/gometrics"
"github.com/eBay/fabio/metrics/metricslib"
)

var pfx string
// provider stores the metrics library provider.
var Provider metricslib.Provider = metricslib.NoopProvider{}

// ServiceRegistry contains a separate metrics registry for
// the timers for all targets to avoid conflicts
// with globally registered timers.
var ServiceRegistry = gometrics.NewRegistry()
// Names returns the list of registered metrics acquired
// through the GetXXX() functions in alphabetical order.
func Names() []string { return Provider.Names() }

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

// UnregisterAll removes all registered metrics and stops
// reporting them to an external backend.
func UnregisterAll() { Provider.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.
func GetTimer(name string) metricslib.Timer { return Provider.GetTimer(name) }

// prefix stores the prefix for all metrics names.
var prefix string

// Init configures the metrics library provider and starts reporting.
func Init(cfg config.Metrics) error {
pfx = cfg.Prefix
if pfx == "default" {
pfx = defaultPrefix()
prefix = cfg.Prefix
if prefix == "default" {
prefix = defaultPrefix()
}

var err error
switch cfg.Target {
case "stdout":
if Provider, err = gometrics.StdoutProvider(cfg.Interval); err != nil {
return err
}
log.Printf("[INFO] Sending metrics to stdout")
return initStdout(cfg.Interval)

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

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")
if Provider, err = gometrics.StatsDProvider(prefix, cfg.StatsDAddr, cfg.Interval); err != nil {
return err
}

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

case "":
log.Printf("[INFO] Metrics disabled")

default:
exit.Fatal("[FATAL] Invalid metrics target ", cfg.Target)
}
return nil
}

// 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 +90,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 +105,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 +115,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/metricslib/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package metricslib

import "time"

// NoopProvider is a stub implementation of the Provider interface.
type NoopProvider struct{}

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

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

func (p NoopProvider) UnregisterAll() {}

func (p NoopProvider) 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 }
41 changes: 41 additions & 0 deletions metrics/metricslib/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Package metricslib defines the common interfaces for
// different metrics libraries.
package metricslib

import "time"

// Provider defines a common interface for metric libraries.
// An implementation must be safe to be used by multiple
// go routines.
type Provider 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)
}
8 changes: 4 additions & 4 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import (
"time"

"github.com/eBay/fabio/config"

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

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

func New(tr http.RoundTripper, cfg config.Proxy) *Proxy {
return &Proxy{
tr: tr,
cfg: cfg,
requests: gometrics.GetOrRegisterTimer("requests", gometrics.DefaultRegistry),
requests: metrics.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 := metrics.GetTimer(name)

t := &Target{Service: service, Tags: tags, URL: targetURL, FixedWeight: fixedWeight, Timer: timer, timerName: name}
r.Targets = append(r.Targets, t)
Expand Down
6 changes: 3 additions & 3 deletions route/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ func syncRegistry(t Table) {
timers := map[string]bool{}

// get all registered timers
metrics.ServiceRegistry.Each(func(name string, m interface{}) {
for _, name := range metrics.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 +77,7 @@ func syncRegistry(t Table) {
// unregister inactive timers
for name, active := range timers {
if !active {
metrics.ServiceRegistry.Unregister(name)
metrics.Unregister(name)
log.Printf("[INFO] Unregistered timer %s", name)
}
}
Expand Down
Loading

0 comments on commit 042f725

Please sign in to comment.