From fc624c9a1dc14c24c8414e366813e978dc75d3ea Mon Sep 17 00:00:00 2001 From: matt maier Date: Wed, 31 Aug 2016 14:18:12 -0400 Subject: [PATCH] PR #154: Make route metric names configurable This patch makes the metric names for the service routes configurable by using a template to generate them. --- config/config.go | 1 + config/default.go | 1 + config/load.go | 1 + config/load_test.go | 2 ++ fabio.properties | 34 ++++++++++++++++++++++ metrics/metrics.go | 64 ++++++++++++++++++++++++++++++++++++----- metrics/metrics_test.go | 7 ++++- route/route.go | 7 ++++- 8 files changed, 108 insertions(+), 9 deletions(-) diff --git a/config/config.go b/config/config.go index d3b2a7d3a..327319d46 100644 --- a/config/config.go +++ b/config/config.go @@ -70,6 +70,7 @@ type Runtime struct { type Metrics struct { Target string Prefix string + Names string Interval time.Duration GraphiteAddr string StatsDAddr string diff --git a/config/default.go b/config/default.go index 48cd63ec9..feeac1114 100644 --- a/config/default.go +++ b/config/default.go @@ -41,6 +41,7 @@ var Default = &Config{ }, Metrics: Metrics{ Prefix: "default", + Names: "{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}", Interval: 30 * time.Second, CirconusAPIApp: "fabio", }, diff --git a/config/load.go b/config/load.go index 4a174b4d4..84caf0a8e 100644 --- a/config/load.go +++ b/config/load.go @@ -107,6 +107,7 @@ func load(p *properties.Properties) (cfg *Config, err error) { f.DurationVar(&cfg.Proxy.FlushInterval, "proxy.flushinterval", Default.Proxy.FlushInterval, "flush interval for streaming responses") f.StringVar(&cfg.Metrics.Target, "metrics.target", Default.Metrics.Target, "metrics backend") f.StringVar(&cfg.Metrics.Prefix, "metrics.prefix", Default.Metrics.Prefix, "prefix for reported metrics") + f.StringVar(&cfg.Metrics.Names, "metrics.names", Default.Metrics.Names, "route metric name template") 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") diff --git a/config/load_test.go b/config/load_test.go index ca93a0fd9..1adda056a 100644 --- a/config/load_test.go +++ b/config/load_test.go @@ -45,6 +45,7 @@ registry.consul.register.checkTimeout = 10s registry.consul.service.status = a,b metrics.target = graphite metrics.prefix = someprefix +metrics.names = {{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}} metrics.interval = 5s metrics.graphite.addr = 5.6.7.8:9999 metrics.statsd.addr = 6.7.8.9:9999 @@ -125,6 +126,7 @@ aws.apigw.cert.cn = furb Metrics: Metrics{ Target: "graphite", Prefix: "someprefix", + Names: "{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}", Interval: 5 * time.Second, GraphiteAddr: "5.6.7.8:9999", StatsDAddr: "6.7.8.9:9999", diff --git a/fabio.properties b/fabio.properties index a0150f554..73ff5a522 100644 --- a/fabio.properties +++ b/fabio.properties @@ -490,6 +490,40 @@ # metrics.prefix = default +# metrics.names configures the template for the route metric names. +# The value is expanded by the text/template package and provides +# the following variables: +# +# - Service: the service name +# - Host: the host part of the URL prefix +# - Path: the path part of the URL prefix +# - TargetURL: the URL of the target +# +# The following additional functions are defined: +# +# - clean: lowercase value and replace '.' and ':' with '_' +# +# Given a route rule of +# +# route add testservice www.example.com/ http://10.1.2.3:12345/ +# +# the template variables are: +# +# .Service = testservice +# .Host = www.example.com +# .Path = / +# .TargetURL.Host = 10.1.2.3:12345 +# +# which results to the following metric name when using the +# default template: +# +# testservice.www_example_com./.10_1_2_3_12345 +# +# The default is +# +# metrics.names = {{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}} + + # metrics.interval configures the interval in which metrics are # reported. # diff --git a/metrics/metrics.go b/metrics/metrics.go index 0bc1c8ef7..f7f2d52a5 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -6,11 +6,14 @@ package metrics import ( + "bytes" + "fmt" "log" "net/url" "os" "path/filepath" "strings" + "text/template" "github.com/eBay/fabio/config" "github.com/eBay/fabio/exit" @@ -19,6 +22,20 @@ import ( // DefaultRegistry stores the metrics library provider. var DefaultRegistry Registry = NoopRegistry{} +// DefaultNames contains the default template for route metric names. +const DefaultNames = "{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}" + +// names stores the template for the route metric names. +var names *template.Template + +func init() { + // make sure names is initialized to something + var err error + if names, err = parseNames(DefaultNames); err != nil { + panic(err) + } +} + // NewRegistry creates a new metrics registry. func NewRegistry(cfg config.Metrics) (r Registry, err error) { prefix := cfg.Prefix @@ -26,6 +43,10 @@ func NewRegistry(cfg config.Metrics) (r Registry, err error) { prefix = defaultPrefix() } + if names, err = parseNames(cfg.Names); err != nil { + return nil, fmt.Errorf("metrics: invalid names template. %s", err) + } + switch cfg.Target { case "stdout": log.Printf("[INFO] Sending metrics to stdout") @@ -54,14 +75,43 @@ func NewRegistry(cfg config.Metrics) (r Registry, err error) { panic("unreachable") } +// parseNames parses the route metric name template. +func parseNames(tmpl string) (*template.Template, error) { + funcMap := template.FuncMap{ + "clean": clean, + } + t, err := template.New("names").Funcs(funcMap).Parse(tmpl) + if err != nil { + return nil, err + } + testURL, err := url.Parse("http://127.0.0.1:12345/") + if err != nil { + return nil, err + } + if _, err := TargetName("testservice", "test.example.com", "/test", testURL); err != nil { + return nil, err + } + return t, 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), - clean(host), - clean(path), - clean(targetURL.Host), - }, ".") +func TargetName(service, host, path string, targetURL *url.URL) (string, error) { + if names == nil { + return "", nil + } + + var name bytes.Buffer + + data := struct { + Service, Host, Path string + TargetURL *url.URL + }{service, host, path, targetURL} + + if err := names.Execute(&name, data); err != nil { + return "", err + } + + return name.String(), nil } // clean creates safe names for graphite reporting by replacing diff --git a/metrics/metrics_test.go b/metrics/metrics_test.go index 5783ed610..c567d06fc 100644 --- a/metrics/metrics_test.go +++ b/metrics/metrics_test.go @@ -32,7 +32,12 @@ func TestTargetName(t *testing.T) { if err != nil { t.Fatalf("%d: %v", i, err) } - if got, want := TargetName(tt.service, tt.host, tt.path, u), tt.name; got != want { + + got, err := TargetName(tt.service, tt.host, tt.path, u) + if err != nil { + t.Fatalf("%d: %v", i, err) + } + if want := tt.name; got != want { t.Errorf("%d: got %q want %q", i, got, want) } } diff --git a/route/route.go b/route/route.go index 23b602704..5821f45ea 100644 --- a/route/route.go +++ b/route/route.go @@ -2,6 +2,7 @@ package route import ( "fmt" + "log" "net/url" "sort" "strings" @@ -46,7 +47,11 @@ func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float6 fixedWeight = 0 } - name := metrics.TargetName(service, r.Host, r.Path, targetURL) + name, err := metrics.TargetName(service, r.Host, r.Path, targetURL) + if err != nil { + log.Printf("[ERROR] Invalid metrics name: %s", err) + name = "unknown" + } timer := ServiceRegistry.GetTimer(name) t := &Target{Service: service, Tags: tags, URL: targetURL, FixedWeight: fixedWeight, Timer: timer, timerName: name}