From 7249c29b86ac36e568cc9994add22e779fdc5b07 Mon Sep 17 00:00:00 2001
From: matt maier <maier@users.noreply.github.com>
Date: Wed, 31 Aug 2016 14:18:12 -0400
Subject: [PATCH 1/2] Add user configurable route metric name capability

---
 config/config.go    | 21 +++++++++++----------
 config/default.go   |  7 ++++---
 config/load.go      |  1 +
 config/load_test.go | 22 ++++++++++++----------
 fabio.properties    | 21 +++++++++++++++++++++
 metrics/metrics.go  | 17 +++++++++++------
 6 files changed, 60 insertions(+), 29 deletions(-)

diff --git a/config/config.go b/config/config.go
index d3b2a7d3a..877ce64c2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -68,16 +68,17 @@ type Runtime struct {
 }
 
 type Metrics struct {
-	Target           string
-	Prefix           string
-	Interval         time.Duration
-	GraphiteAddr     string
-	StatsDAddr       string
-	CirconusAPIKey   string
-	CirconusAPIApp   string
-	CirconusAPIURL   string
-	CirconusCheckID  string
-	CirconusBrokerID string
+	Target                  string
+	Prefix                  string
+	Interval                time.Duration
+	GraphiteAddr            string
+	StatsDAddr              string
+	CirconusAPIKey          string
+	CirconusAPIApp          string
+	CirconusAPIURL          string
+	CirconusCheckID         string
+	CirconusBrokerID        string
+	RouteMetricNameTemplate string
 }
 
 type Registry struct {
diff --git a/config/default.go b/config/default.go
index 48cd63ec9..1f7ce0c57 100644
--- a/config/default.go
+++ b/config/default.go
@@ -40,9 +40,10 @@ var Default = &Config{
 		Color: "light-green",
 	},
 	Metrics: Metrics{
-		Prefix:         "default",
-		Interval:       30 * time.Second,
-		CirconusAPIApp: "fabio",
+		Prefix:                  "default",
+		Interval:                30 * time.Second,
+		CirconusAPIApp:          "fabio",
+		RouteMetricNameTemplate: "$service$.$host$.$path$.$target$",
 	},
 	CertSources: map[string]CertSource{},
 }
diff --git a/config/load.go b/config/load.go
index 4a174b4d4..102d711fc 100644
--- a/config/load.go
+++ b/config/load.go
@@ -115,6 +115,7 @@ func load(p *properties.Properties) (cfg *Config, err error) {
 	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.Metrics.RouteMetricNameTemplate, "metrics.routemetricnametemplate", Default.Metrics.RouteMetricNameTemplate, "route metric name template")
 	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")
diff --git a/config/load_test.go b/config/load_test.go
index ca93a0fd9..2a455e9b4 100644
--- a/config/load_test.go
+++ b/config/load_test.go
@@ -53,6 +53,7 @@ metrics.circonus.apiapp = circonus-apiapp
 metrics.circonus.apiurl = circonus-apiurl
 metrics.circonus.brokerid = circonus-brokerid
 metrics.circonus.checkid = circonus-checkid
+metrics.routemetricnametemplate = $service$.$host$.$path$.$target$
 runtime.gogc = 666
 runtime.gomaxprocs = 12
 ui.addr = 7.8.9.0:1234
@@ -123,16 +124,17 @@ aws.apigw.cert.cn = furb
 			},
 		},
 		Metrics: Metrics{
-			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",
+			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",
+			RouteMetricNameTemplate: "$service$.$host$.$path$.$target$",
 		},
 		Runtime: Runtime{
 			GOGC:       666,
diff --git a/fabio.properties b/fabio.properties
index a0150f554..dc0757f43 100644
--- a/fabio.properties
+++ b/fabio.properties
@@ -564,6 +564,27 @@
 # metrics.circonus.checkid =
 
 
+# metrics.routemetricnametemplate provides the ability to customize
+# the route metric names. Variables available for template expansion
+# are: service host path and target.
+#
+# Given a route rule of: route add testservice www.example.com/ http://10.1.2.3:12345/
+# The template variables would be:
+#
+# $service$=testservice
+# $host$=www_example_com
+# $path$=/
+# $target$=10_1_2_3_12345
+#
+# The resulting metric name (using the default template):
+#
+# testservice.www_example_com./.10_1_2_3_12345
+#
+# The default is
+#
+# metrics.routemetricnametemplate = $service$.$host$.$path$.$target$
+
+
 # runtime.gogc configures GOGC (the GC target percentage).
 #
 # Setting runtime.gogc is equivalent to setting the GOGC
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 0bc1c8ef7..d09b1b7eb 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -19,6 +19,8 @@ import (
 // DefaultRegistry stores the metrics library provider.
 var DefaultRegistry Registry = NoopRegistry{}
 
+var routeMetricNameTemplate string
+
 // NewRegistry creates a new metrics registry.
 func NewRegistry(cfg config.Metrics) (r Registry, err error) {
 	prefix := cfg.Prefix
@@ -26,6 +28,8 @@ func NewRegistry(cfg config.Metrics) (r Registry, err error) {
 		prefix = defaultPrefix()
 	}
 
+	routeMetricNameTemplate = cfg.RouteMetricNameTemplate
+
 	switch cfg.Target {
 	case "stdout":
 		log.Printf("[INFO] Sending metrics to stdout")
@@ -56,12 +60,13 @@ func NewRegistry(cfg config.Metrics) (r Registry, err error) {
 
 // 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),
-	}, ".")
+	name := routeMetricNameTemplate
+	name = strings.Replace(name, "$service$", clean(service), -1)
+	name = strings.Replace(name, "$host$", clean(host), -1)
+	name = strings.Replace(name, "$path$", clean(path), -1)
+	name = strings.Replace(name, "$target$", clean(targetURL.Host), -1)
+
+	return name
 }
 
 // clean creates safe names for graphite reporting by replacing

From 9caa276909cca6985050e8bb8cf437d4fd4f07a0 Mon Sep 17 00:00:00 2001
From: matt maier <maier@users.noreply.github.com>
Date: Thu, 1 Sep 2016 10:43:14 -0400
Subject: [PATCH 2/2] Update to use text/template

---
 config/default.go       |  2 +-
 config/load_test.go     |  4 +--
 fabio.properties        | 12 +++++---
 metrics/metrics.go      | 67 +++++++++++++++++++++++++++++++++++------
 metrics/metrics_test.go | 19 +++++++++++-
 route/route.go          |  2 +-
 6 files changed, 86 insertions(+), 20 deletions(-)

diff --git a/config/default.go b/config/default.go
index 1f7ce0c57..3c76d440f 100644
--- a/config/default.go
+++ b/config/default.go
@@ -43,7 +43,7 @@ var Default = &Config{
 		Prefix:                  "default",
 		Interval:                30 * time.Second,
 		CirconusAPIApp:          "fabio",
-		RouteMetricNameTemplate: "$service$.$host$.$path$.$target$",
+		RouteMetricNameTemplate: "{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}",
 	},
 	CertSources: map[string]CertSource{},
 }
diff --git a/config/load_test.go b/config/load_test.go
index 2a455e9b4..15eb0af3f 100644
--- a/config/load_test.go
+++ b/config/load_test.go
@@ -53,7 +53,7 @@ metrics.circonus.apiapp = circonus-apiapp
 metrics.circonus.apiurl = circonus-apiurl
 metrics.circonus.brokerid = circonus-brokerid
 metrics.circonus.checkid = circonus-checkid
-metrics.routemetricnametemplate = $service$.$host$.$path$.$target$
+metrics.routemetricnametemplate = {{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}
 runtime.gogc = 666
 runtime.gomaxprocs = 12
 ui.addr = 7.8.9.0:1234
@@ -134,7 +134,7 @@ aws.apigw.cert.cn = furb
 			CirconusAPIURL:          "circonus-apiurl",
 			CirconusBrokerID:        "circonus-brokerid",
 			CirconusCheckID:         "circonus-checkid",
-			RouteMetricNameTemplate: "$service$.$host$.$path$.$target$",
+			RouteMetricNameTemplate: "{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}",
 		},
 		Runtime: Runtime{
 			GOGC:       666,
diff --git a/fabio.properties b/fabio.properties
index dc0757f43..cb5845a0d 100644
--- a/fabio.properties
+++ b/fabio.properties
@@ -571,10 +571,12 @@
 # Given a route rule of: route add testservice www.example.com/ http://10.1.2.3:12345/
 # The template variables would be:
 #
-# $service$=testservice
-# $host$=www_example_com
-# $path$=/
-# $target$=10_1_2_3_12345
+# .Service = testservice
+# .Host = www.example.com
+# .Path  = /
+# .TargetURL.Host = 10.1.2.3:12345
+#
+# Function: clean - lowercases value and replaces '.' and ':' with '_'
 #
 # The resulting metric name (using the default template):
 #
@@ -582,7 +584,7 @@
 #
 # The default is
 #
-# metrics.routemetricnametemplate = $service$.$host$.$path$.$target$
+# metrics.routemetricnametemplate = {{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}
 
 
 # runtime.gogc configures GOGC (the GC target percentage).
diff --git a/metrics/metrics.go b/metrics/metrics.go
index d09b1b7eb..5b16ddd20 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -6,11 +6,13 @@
 package metrics
 
 import (
+	"bytes"
 	"log"
 	"net/url"
 	"os"
 	"path/filepath"
 	"strings"
+	"text/template"
 
 	"github.com/eBay/fabio/config"
 	"github.com/eBay/fabio/exit"
@@ -19,7 +21,12 @@ import (
 // DefaultRegistry stores the metrics library provider.
 var DefaultRegistry Registry = NoopRegistry{}
 
-var routeMetricNameTemplate string
+var routeMetricNameTemplate *template.Template
+
+type routeMetricNameAttributes struct {
+	Service, Host, Path string
+	TargetURL           *url.URL
+}
 
 // NewRegistry creates a new metrics registry.
 func NewRegistry(cfg config.Metrics) (r Registry, err error) {
@@ -28,7 +35,17 @@ func NewRegistry(cfg config.Metrics) (r Registry, err error) {
 		prefix = defaultPrefix()
 	}
 
-	routeMetricNameTemplate = cfg.RouteMetricNameTemplate
+	funcMap := template.FuncMap{
+		"clean": clean,
+	}
+
+	routeMetricNameTemplate, err = template.New("routeMetricName").Funcs(funcMap).Parse(cfg.RouteMetricNameTemplate)
+	if err != nil {
+		return nil, err
+	}
+	if err := verifyTemplate(); err != nil {
+		return nil, err
+	}
 
 	switch cfg.Target {
 	case "stdout":
@@ -59,14 +76,21 @@ func NewRegistry(cfg config.Metrics) (r Registry, err error) {
 }
 
 // TargetName returns the metrics name from the given parameters.
-func TargetName(service, host, path string, targetURL *url.URL) string {
-	name := routeMetricNameTemplate
-	name = strings.Replace(name, "$service$", clean(service), -1)
-	name = strings.Replace(name, "$host$", clean(host), -1)
-	name = strings.Replace(name, "$path$", clean(path), -1)
-	name = strings.Replace(name, "$target$", clean(targetURL.Host), -1)
-
-	return name
+func TargetName(service, host, path string, targetURL *url.URL) (string, error) {
+	var name bytes.Buffer
+
+	data := &routeMetricNameAttributes{
+		Service:   service,
+		Host:      host,
+		Path:      path,
+		TargetURL: targetURL,
+	}
+
+	if err := routeMetricNameTemplate.Execute(&name, data); err != nil {
+		return "", err
+	}
+
+	return name.String(), nil
 }
 
 // clean creates safe names for graphite reporting by replacing
@@ -94,3 +118,26 @@ func defaultPrefix() string {
 	exe := filepath.Base(os.Args[0])
 	return clean(host) + "." + clean(exe)
 }
+
+// verifyTemplate checks the route metric name template syntax
+func verifyTemplate() error {
+	var name bytes.Buffer
+
+	testURL, err := url.Parse("http://127.0.0.1:12345/")
+	if err != nil {
+		return err
+	}
+
+	data := &routeMetricNameAttributes{
+		Service:   "testservice",
+		Host:      "test.example.com",
+		Path:      "/test",
+		TargetURL: testURL,
+	}
+
+	if err := routeMetricNameTemplate.Execute(&name, data); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/metrics/metrics_test.go b/metrics/metrics_test.go
index 5783ed610..184254ee3 100644
--- a/metrics/metrics_test.go
+++ b/metrics/metrics_test.go
@@ -4,6 +4,7 @@ import (
 	"net/url"
 	"os"
 	"testing"
+	"text/template"
 )
 
 func TestDefaultPrefix(t *testing.T) {
@@ -27,12 +28,28 @@ func TestTargetName(t *testing.T) {
 		{"", "", "", "http://1.2.3.4:1234/bar", "_._._.1_2_3_4_1234"},
 	}
 
+	funcMap := template.FuncMap{
+		"clean": clean,
+	}
+
+	var err error
+	ts := "{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}"
+	routeMetricNameTemplate, err = template.New("routeMetricName").Funcs(funcMap).Parse(ts)
+	if err != nil {
+		t.Fatalf("Template %s: %v", ts, err)
+	}
+
 	for i, tt := range tests {
 		u, err := url.Parse(tt.target)
 		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..1a026a569 100644
--- a/route/route.go
+++ b/route/route.go
@@ -46,7 +46,7 @@ func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float6
 		fixedWeight = 0
 	}
 
-	name := metrics.TargetName(service, r.Host, r.Path, targetURL)
+	name, _ := metrics.TargetName(service, r.Host, r.Path, targetURL)
 	timer := ServiceRegistry.GetTimer(name)
 
 	t := &Target{Service: service, Tags: tags, URL: targetURL, FixedWeight: fixedWeight, Timer: timer, timerName: name}