From e05aa9907754ff3272b4028a30ab98f9847eff7a Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Sat, 28 Nov 2015 19:30:51 +0100 Subject: [PATCH] Issue #16: Drive UI via API --- route/matcher.go | 6 +- route/picker.go | 6 +- route/route.go | 113 +++++++++++++++++++------------------- route/route_bench_test.go | 2 +- route/route_test.go | 6 +- route/routes.go | 12 ++-- route/table.go | 14 ++--- route/target.go | 18 +++--- ui/api.go | 72 ++++++++++++++++++++++++ ui/route.go | 102 +++++++++++++++++----------------- ui/server.go | 3 +- 11 files changed, 215 insertions(+), 139 deletions(-) create mode 100644 ui/api.go diff --git a/route/matcher.go b/route/matcher.go index 1b40731cc..1a4801019 100644 --- a/route/matcher.go +++ b/route/matcher.go @@ -6,9 +6,9 @@ import "strings" var match matcher = prefixMatcher // matcher determines whether a host/path matches a route -type matcher func(path string, r *route) bool +type matcher func(path string, r *Route) bool // prefixMatcher matches path to the routes' path. -func prefixMatcher(path string, r *route) bool { - return strings.HasPrefix(path, r.path) +func prefixMatcher(path string, r *Route) bool { + return strings.HasPrefix(path, r.Path) } diff --git a/route/picker.go b/route/picker.go index 650a3d79b..fcc58186e 100644 --- a/route/picker.go +++ b/route/picker.go @@ -10,7 +10,7 @@ import ( var pick picker = rndPicker // Picker selects a target from a list of targets -type picker func(r *route) *Target +type picker func(r *Route) *Target // SetPickerStrategy sets the picker function for the proxy. func SetPickerStrategy(s string) error { @@ -26,12 +26,12 @@ func SetPickerStrategy(s string) error { } // rndPicker picks a random target from the list of targets. -func rndPicker(r *route) *Target { +func rndPicker(r *Route) *Target { return r.wTargets[randIntn(len(r.wTargets))] } // rrPicker picks the next target from a list of targets using round-robin. -func rrPicker(r *route) *Target { +func rrPicker(r *Route) *Target { u := r.wTargets[r.total%uint64(len(r.wTargets))] atomic.AddUint64(&r.total, 1) return u diff --git a/route/route.go b/route/route.go index 5bc32cada..3478d188f 100644 --- a/route/route.go +++ b/route/route.go @@ -16,18 +16,18 @@ import ( // amount of traffic this route should get. You can specify // that a route should get a fixed percentage of the traffic // independent of how many instances are running. -type route struct { - // host contains the host of the route. +type Route struct { + // Host contains the host of the route. // not used for routing but for config generation // Table has a map with the host as key // for faster lookup and smaller search space. - host string + Host string - // path is the path prefix from a request uri - path string + // Path is the path prefix from a request uri + Path string - // targets contains the list of URLs - targets []*Target + // Targets contains the list of URLs + Targets []*Target // wTargets contains 100 targets distributed // according to their weight and ordered RR in the @@ -39,59 +39,59 @@ type route struct { total uint64 } -func newRoute(host, path string) *route { - return &route{host: host, path: path} +func newRoute(host, path string) *Route { + return &Route{Host: host, Path: path} } -func (r *route) addTarget(service string, targetURL *url.URL, fixedWeight float64, tags []string) { +func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float64, tags []string) { if fixedWeight < 0 { fixedWeight = 0 } - name := metrics.TargetName(service, r.host, r.path, targetURL) + name := metrics.TargetName(service, r.Host, r.Path, targetURL) timer := gometrics.GetOrRegisterTimer(name, gometrics.DefaultRegistry) - t := &Target{service: service, tags: tags, URL: targetURL, fixedWeight: fixedWeight, Timer: timer} - r.targets = append(r.targets, t) + t := &Target{Service: service, Tags: tags, URL: targetURL, FixedWeight: fixedWeight, Timer: timer} + r.Targets = append(r.Targets, t) r.weighTargets() } -func (r *route) delService(service string) { +func (r *Route) delService(service string) { var clone []*Target - for _, t := range r.targets { - if t.service == service { + for _, t := range r.Targets { + if t.Service == service { continue } clone = append(clone, t) } - r.targets = clone + r.Targets = clone r.weighTargets() } -func (r *route) delTarget(service string, targetURL *url.URL) { +func (r *Route) delTarget(service string, targetURL *url.URL) { var clone []*Target - for _, t := range r.targets { - if t.service == service && t.URL.String() == targetURL.String() { + for _, t := range r.Targets { + if t.Service == service && t.URL.String() == targetURL.String() { continue } clone = append(clone, t) } - r.targets = clone + r.Targets = clone r.weighTargets() } -func (r *route) setWeight(service string, weight float64, tags []string) int { +func (r *Route) setWeight(service string, weight float64, tags []string) int { loop := func(w float64) int { n := 0 - for _, t := range r.targets { - if service != "" && t.service != service { + for _, t := range r.Targets { + if service != "" && t.Service != service { continue } - if len(tags) > 0 && !contains(t.tags, tags) { + if len(tags) > 0 && !contains(t.Tags, tags) { continue } n++ - t.fixedWeight = w + t.FixedWeight = w } return n } @@ -128,7 +128,7 @@ func contains(src, dst []string) bool { } // targetWeight returns how often target is in wTargets. -func (r *route) targetWeight(targetURL string) (n int) { +func (r *Route) targetWeight(targetURL string) (n int) { for _, t := range r.wTargets { if t.URL.String() == targetURL { n++ @@ -137,25 +137,28 @@ func (r *route) targetWeight(targetURL string) (n int) { return n } +func (r *Route) TargetConfig(t *Target, addWeight bool) string { + s := fmt.Sprintf("route add %s %s %s", t.Service, r.Host+r.Path, t.URL) + if addWeight { + s += fmt.Sprintf(" weight %2.2f", t.Weight) + } else if t.FixedWeight > 0 { + s += fmt.Sprintf(" weight %.2f", t.FixedWeight) + } + if len(t.Tags) > 0 { + s += fmt.Sprintf(" tags %q", strings.Join(t.Tags, ",")) + } + return s +} + // config returns the route configuration in the config language. // with the weights specified by the user. -func (r *route) config(addWeight bool) []string { +func (r *Route) config(addWeight bool) []string { var cfg []string - for _, t := range r.targets { - if t.weight <= 0 { + for _, t := range r.Targets { + if t.Weight <= 0 { continue } - - s := fmt.Sprintf("route add %s %s %s", t.service, r.host+r.path, t.URL) - if addWeight { - s += fmt.Sprintf(" weight %2.2f", t.weight) - } else if t.fixedWeight > 0 { - s += fmt.Sprintf(" weight %.2f", t.fixedWeight) - } - if len(t.tags) > 0 { - s += fmt.Sprintf(" tags %q", strings.Join(t.tags, ",")) - } - cfg = append(cfg, s) + cfg = append(cfg, r.TargetConfig(t, addWeight)) } return cfg } @@ -168,35 +171,35 @@ func (r *route) config(addWeight bool) []string { // // Targets with a dynamic weight will receive an equal share of the remaining // traffic if there is any left. -func (r *route) weighTargets() { +func (r *Route) weighTargets() { // how big is the fixed weighted traffic? var nFixed int var sumFixed float64 - for _, t := range r.targets { - if t.fixedWeight > 0 { + for _, t := range r.Targets { + if t.FixedWeight > 0 { nFixed++ - sumFixed += t.fixedWeight + sumFixed += t.FixedWeight } } // normalize fixed weights up (sumFixed < 1) or down (sumFixed > 1) scale := 1.0 - if sumFixed > 1 || (nFixed == len(r.targets) && sumFixed < 1) { + if sumFixed > 1 || (nFixed == len(r.Targets) && sumFixed < 1) { scale = 1 / sumFixed } // compute the weight for the targets with dynamic weights - dynamic := (1 - sumFixed) / float64(len(r.targets)-nFixed) + dynamic := (1 - sumFixed) / float64(len(r.Targets)-nFixed) if dynamic < 0 { dynamic = 0 } // assign the actual weight to each target - for _, t := range r.targets { - if t.fixedWeight > 0 { - t.weight = t.fixedWeight * scale + for _, t := range r.Targets { + if t.FixedWeight > 0 { + t.Weight = t.FixedWeight * scale } else { - t.weight = dynamic + t.Weight = dynamic } } @@ -215,10 +218,10 @@ func (r *route) weighTargets() { // because of rounding errors gotSlots, wantSlots := 0, 100 - slotCount := make(byN, len(r.targets)) - for i, t := range r.targets { + slotCount := make(byN, len(r.Targets)) + for i, t := range r.Targets { slotCount[i].i = i - slotCount[i].n = int(float64(wantSlots)*t.weight + 0.5) + slotCount[i].n = int(float64(wantSlots)*t.Weight + 0.5) gotSlots += slotCount[i].n } sort.Sort(slotCount) @@ -237,7 +240,7 @@ func (r *route) weighTargets() { } // use slot and move to next one - slots[next] = r.targets[c.i] + slots[next] = r.Targets[c.i] next = (next + step) % gotSlots } } diff --git a/route/route_bench_test.go b/route/route_bench_test.go index 195866952..0ac1dcf21 100644 --- a/route/route_bench_test.go +++ b/route/route_bench_test.go @@ -100,7 +100,7 @@ func makeRequests(t Table) []*http.Request { reqs := []*http.Request{} for host, hr := range t { for _, r := range hr { - req := &http.Request{Host: host, RequestURI: r.path + "/some/additional/path"} + req := &http.Request{Host: host, RequestURI: r.Path + "/some/additional/path"} reqs = append(reqs, req) } } diff --git a/route/route_test.go b/route/route_test.go index 1916ae3dd..d0e5d8797 100644 --- a/route/route_test.go +++ b/route/route_test.go @@ -16,7 +16,7 @@ func mustParse(rawurl string) *url.URL { func TestNewRoute(t *testing.T) { r := newRoute("www.bar.com", "/foo") - if got, want := r.path, "/foo"; got != want { + if got, want := r.Path, "/foo"; got != want { t.Errorf("got %q want %q", got, want) } } @@ -27,10 +27,10 @@ func TestAddTarget(t *testing.T) { r := newRoute("www.bar.com", "/foo") r.addTarget("service", u, 0, nil) - if got, want := len(r.targets), 1; got != want { + if got, want := len(r.Targets), 1; got != want { t.Errorf("target length: got %d want %d", got, want) } - if got, want := r.targets[0].URL, u; got != want { + if got, want := r.Targets[0].URL, u; got != want { t.Errorf("target url: got %s want %s", got, want) } config := []string{"route add service www.bar.com/foo http://foo.com/"} diff --git a/route/routes.go b/route/routes.go index 8461ace01..3d80d515c 100644 --- a/route/routes.go +++ b/route/routes.go @@ -1,12 +1,12 @@ package route // routes stores a list of routes usually for a single host. -type routes []*route +type Routes []*Route // find returns the route with the given path and returns nil if none was found. -func (rt routes) find(path string) *route { +func (rt Routes) find(path string) *Route { for _, r := range rt { - if r.path == path { + if r.Path == path { return r } } @@ -14,6 +14,6 @@ func (rt routes) find(path string) *route { } // sort by path in reverse order (most to least specific) -func (rt routes) Len() int { return len(rt) } -func (rt routes) Swap(i, j int) { rt[i], rt[j] = rt[j], rt[i] } -func (rt routes) Less(i, j int) bool { return rt[j].path < rt[i].path } +func (rt Routes) Len() int { return len(rt) } +func (rt Routes) Swap(i, j int) { rt[i], rt[j] = rt[j], rt[i] } +func (rt Routes) Less(i, j int) bool { return rt[j].Path < rt[i].Path } diff --git a/route/table.go b/route/table.go index 7975352de..ed03ac319 100644 --- a/route/table.go +++ b/route/table.go @@ -38,7 +38,7 @@ func SetTable(t Table) { // Table contains a set of routes grouped by host. // The host routes are sorted from most to least specific // by sorting the routes in reverse order by path. -type Table map[string]routes +type Table map[string]Routes // hostpath splits a host/path prefix into a host and a path. // The path always starts with a slash @@ -73,7 +73,7 @@ func (t Table) AddRoute(service, prefix, target string, weight float64, tags []s // add new host if t[host] == nil { - t[host] = routes{r} + t[host] = Routes{r} return nil } @@ -147,7 +147,7 @@ func (t Table) DelRoute(service, prefix, target string) error { } // route finds the route for host/path or returns nil if none exists. -func (t Table) route(host, path string) *route { +func (t Table) route(host, path string) *Route { hr := t[host] if hr == nil { return nil @@ -187,20 +187,20 @@ func (t Table) doLookup(host, path, trace string) *Target { for _, r := range hr { if match(path, r) { - n := len(r.targets) + n := len(r.Targets) if n == 0 { return nil } if n == 1 { - return r.targets[0] + return r.Targets[0] } if trace != "" { - log.Printf("[TRACE] %s Match %s%s", trace, r.host, r.path) + log.Printf("[TRACE] %s Match %s%s", trace, r.Host, r.Path) } return pick(r) } if trace != "" { - log.Printf("[TRACE] %s No match %s%s", trace, r.host, r.path) + log.Printf("[TRACE] %s No match %s%s", trace, r.Host, r.Path) } } return nil diff --git a/route/target.go b/route/target.go index a5d35f730..a7a061d7a 100644 --- a/route/target.go +++ b/route/target.go @@ -7,22 +7,22 @@ import ( ) type Target struct { - // service is the name of the service the targetURL points to - service string + // Service is the name of the service the targetURL points to + Service string - // tags are the list of tags for this target - tags []string + // Tags are the list of tags for this target + Tags []string // URL is the endpoint the service instance listens on URL *url.URL - // fixedWeight is the weight assigned to this target. + // FixedWeight is the weight assigned to this target. // If the value is 0 the targets weight is dynamic. - fixedWeight float64 + FixedWeight float64 - // weight is the actual weight for this service in percent. - weight float64 + // Weight is the actual weight for this service in percent. + Weight float64 - // timer measures throughput and latency of this target + // Timer measures throughput and latency of this target Timer gometrics.Timer } diff --git a/ui/api.go b/ui/api.go new file mode 100644 index 000000000..870831d6b --- /dev/null +++ b/ui/api.go @@ -0,0 +1,72 @@ +package ui + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/eBay/fabio/route" +) + +type apiRoute struct { + Service string `json:"service"` + Host string `json:"host"` + Path string `json:"path"` + Dst string `json:"dst"` + Weight float64 `json:"weight"` + Tags []string `json:"tags,omitempty"` + Cmd string `json:"cmd"` + Rate1 float64 `json:"rate1"` + Pct99 float64 `json:"pct99"` +} + +func handleRoutes(w http.ResponseWriter, r *http.Request) { + t := route.GetTable() + + var hosts []string + for host := range t { + hosts = append(hosts, host) + } + + var apiRoutes []apiRoute + for _, host := range hosts { + for _, tr := range t[host] { + for _, tg := range tr.Targets { + ar := apiRoute{ + Service: tg.Service, + Host: tr.Host, + Path: tr.Path, + Dst: tg.URL.String(), + Weight: tg.Weight, + Tags: tg.Tags, + Cmd: tr.TargetConfig(tg, true), + Rate1: tg.Timer.Rate1(), + Pct99: tg.Timer.Percentile(0.99), + } + apiRoutes = append(apiRoutes, ar) + } + } + } + writeJSON(w, r, apiRoutes) +} + +func writeJSON(w http.ResponseWriter, r *http.Request, v interface{}) { + _, pretty := r.URL.Query()["pretty"] + + var buf []byte + var err error + if pretty { + buf, err = json.MarshalIndent(v, "", " ") + } else { + buf, err = json.Marshal(v) + } + + if err != nil { + log.Printf("[ERROR] ", err) + http.Error(w, "internal error", 500) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write(buf) +} diff --git a/ui/route.go b/ui/route.go index 5209ce95a..76658555f 100644 --- a/ui/route.go +++ b/ui/route.go @@ -3,41 +3,14 @@ package ui import ( "html/template" "net/http" - "strings" - - "github.com/eBay/fabio/route" ) -func handleRoute(w http.ResponseWriter, r *http.Request) { - var cfg [][]string - for _, s := range route.GetTable().Config(true) { - p := strings.Split(s, "tags") - if len(p) == 1 { - cfg = append(cfg, []string{s, ""}) - } else { - cfg = append(cfg, []string{strings.TrimSpace(p[0]), "tags" + p[1]}) - } - } - - data := struct { - Config [][]string - ConfigURL string - Version string - }{ - cfg, - configURL, - version, - } +func handleUI(w http.ResponseWriter, r *http.Request) { + data := struct{ ConfigURL, Version string }{configURL, version} tmplTable.ExecuteTemplate(w, "table", data) } -func add(x, y int) int { - return x + y -} - -var funcs = template.FuncMap{"add": add} - -var tmplTable = template.Must(template.New("table").Funcs(funcs).Parse(htmlTable)) +var tmplTable = template.Must(template.New("table").Parse(htmlTable)) var htmlTable = ` @@ -45,9 +18,9 @@ var htmlTable = ` ./fabio + -