From 042f72554c4e74f35fff4abadaf4f9a1323396d1 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Sat, 27 Aug 2016 13:45:39 +0200 Subject: [PATCH] Issue #147: Support multiple metrics libraries --- metrics/gometrics/provider.go | 91 ++++++++++++++++++++++++++++ metrics/metrics.go | 106 ++++++++++++++++----------------- metrics/metricslib/noop.go | 25 ++++++++ metrics/metricslib/provider.go | 41 +++++++++++++ proxy/proxy.go | 8 +-- route/route.go | 4 +- route/table.go | 6 +- route/table_registry_test.go | 47 +++++++++++---- route/target.go | 4 +- 9 files changed, 254 insertions(+), 78 deletions(-) create mode 100644 metrics/gometrics/provider.go create mode 100644 metrics/metricslib/noop.go create mode 100644 metrics/metricslib/provider.go diff --git a/metrics/gometrics/provider.go b/metrics/gometrics/provider.go new file mode 100644 index 000000000..f3f64bded --- /dev/null +++ b/metrics/gometrics/provider.go @@ -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) +} diff --git a/metrics/metrics.go b/metrics/metrics.go index 74716afa6..5d6600591 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -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), @@ -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 "_" @@ -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 { @@ -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 -} diff --git a/metrics/metricslib/noop.go b/metrics/metricslib/noop.go new file mode 100644 index 000000000..468cb08f3 --- /dev/null +++ b/metrics/metricslib/noop.go @@ -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 } diff --git a/metrics/metricslib/provider.go b/metrics/metricslib/provider.go new file mode 100644 index 000000000..13da9c1f7 --- /dev/null +++ b/metrics/metricslib/provider.go @@ -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) +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 201a173dd..ee2aa611d 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -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"), } } diff --git a/route/route.go b/route/route.go index b3112fed0..b792c1fd7 100644 --- a/route/route.go +++ b/route/route.go @@ -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. @@ -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) diff --git a/route/table.go b/route/table.go index c2744242e..a28e567fd 100644 --- a/route/table.go +++ b/route/table.go @@ -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 @@ -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) } } diff --git a/route/table_registry_test.go b/route/table_registry_test.go index 5e951de91..3c19b3246 100644 --- a/route/table_registry_test.go +++ b/route/table_registry_test.go @@ -2,34 +2,55 @@ package route import ( "reflect" - "sort" "testing" "github.com/eBay/fabio/metrics" + "github.com/eBay/fabio/metrics/metricslib" ) func TestSyncRegistry(t *testing.T) { - names := func() []string { - var n []string - metrics.ServiceRegistry.Each(func(name string, x interface{}) { - n = append(n, name) - }) - sort.Strings(n) - return n - } - - metrics.ServiceRegistry.UnregisterAll() + metrics.Provider = newStubProvider() + metrics.UnregisterAll() tbl := make(Table) tbl.AddRoute("svc-a", "/aaa", "http://localhost:1234", 1, nil) tbl.AddRoute("svc-b", "/bbb", "http://localhost:5678", 1, nil) - if got, want := names(), []string{"svc-a._./aaa.localhost_1234", "svc-b._./bbb.localhost_5678"}; !reflect.DeepEqual(got, want) { + if got, want := metrics.Names(), []string{"svc-a._./aaa.localhost_1234", "svc-b._./bbb.localhost_5678"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } tbl.DelRoute("svc-b", "/bbb", "http://localhost:5678") syncRegistry(tbl) - if got, want := names(), []string{"svc-a._./aaa.localhost_1234"}; !reflect.DeepEqual(got, want) { + if got, want := metrics.Names(), []string{"svc-a._./aaa.localhost_1234"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } } + +func newStubProvider() metricslib.Provider { + return &stubProvider{names: make(map[string]bool)} +} + +type stubProvider struct { + names map[string]bool +} + +func (p *stubProvider) Names() []string { + n := []string{} + for k := range p.names { + n = append(n, k) + } + return n +} + +func (p *stubProvider) Unregister(name string) { + delete(p.names, name) +} + +func (p *stubProvider) UnregisterAll() { + p.names = map[string]bool{} +} + +func (p *stubProvider) GetTimer(name string) metricslib.Timer { + p.names[name] = true + return metricslib.NoopTimer{} +} diff --git a/route/target.go b/route/target.go index bbe1539d5..69bc94c3b 100644 --- a/route/target.go +++ b/route/target.go @@ -3,7 +3,7 @@ package route import ( "net/url" - gometrics "github.com/rcrowley/go-metrics" + "github.com/eBay/fabio/metrics/metricslib" ) type Target struct { @@ -24,7 +24,7 @@ type Target struct { Weight float64 // Timer measures throughput and latency of this target - Timer gometrics.Timer + Timer metricslib.Timer // timerName is the name of the timer in the metrics registry timerName string