diff --git a/go/vt/discovery/healthcheck.go b/go/vt/discovery/healthcheck.go index 440f1d3de06..27612dd5675 100644 --- a/go/vt/discovery/healthcheck.go +++ b/go/vt/discovery/healthcheck.go @@ -39,6 +39,7 @@ package discovery import ( "bytes" "encoding/json" + "flag" "fmt" "html/template" "net/http" @@ -47,9 +48,8 @@ import ( "sync" "time" - "golang.org/x/net/context" - "github.com/golang/protobuf/proto" + "golang.org/x/net/context" "vitess.io/vitess/go/netutil" "vitess.io/vitess/go/stats" "vitess.io/vitess/go/vt/grpcclient" @@ -67,6 +67,8 @@ var ( hcErrorCounters = stats.NewCountersWithMultiLabels("HealthcheckErrors", "Healthcheck Errors", []string{"Keyspace", "ShardName", "TabletType"}) hcMasterPromotedCounters = stats.NewCountersWithMultiLabels("HealthcheckMasterPromoted", "Master promoted in keyspace/shard name because of health check errors", []string{"Keyspace", "ShardName"}) healthcheckOnce sync.Once + tabletURLTemplateString = flag.String("tablet_url_template", "http://{{.GetTabletHostPort}}", "format string describing debug tablet url formatting. See the Go code for getTabletDebugURL() how to customize this.") + tabletURLTemplate *template.Template ) // See the documentation for NewHealthCheck below for an explanation of these parameters. @@ -117,6 +119,20 @@ const ( ` ) +func init() { + loadTabletURLTemplate() +} + +// loadTabletURLTemplate loads or reloads the URL template. +// Should only be used independently for testing. +func loadTabletURLTemplate() { + tabletURLTemplate = template.New("") + _, err := tabletURLTemplate.Parse(*tabletURLTemplateString) + if err != nil { + log.Exitf("error parsing template: %v", err) + } +} + // HealthCheckStatsListener is the listener to receive health check stats update. type HealthCheckStatsListener interface { // StatsUpdate is called when: @@ -187,6 +203,44 @@ func (e *TabletStats) DeepEqual(f *TabletStats) bool { (e.LastError != nil && f.LastError != nil && e.LastError.Error() == f.LastError.Error())) } +// GetTabletHostPort formats a tablet host port address. +func (e TabletStats) GetTabletHostPort() string { + vtPort := e.Tablet.PortMap["vt"] + return netutil.JoinHostPort(e.Tablet.Hostname, vtPort) +} + +// GetHostNameLevel returns the specified hostname level. If the level does not exist it will pick the closest level. +// This seems unused but can be utilized by certain url formatting templates. See getTabletDebugURL for more details. +func (e TabletStats) GetHostNameLevel(level int) string { + chunkedHostname := strings.Split(e.Tablet.Hostname, ".") + + if level < 0 { + return chunkedHostname[0] + } else if level >= len(chunkedHostname) { + return chunkedHostname[len(chunkedHostname)-1] + } else { + return chunkedHostname[level] + } +} + +// getTabletDebugURL formats a debug url to the tablet. +// It uses a format string that can be passed into the app to format +// the debug URL to accommodate different network setups. It applies +// the html/template string defined to a TabletStats object. The +// format string can refer to members and functions of TabletStats +// like a regular html/template string. +// +// For instance given a tablet with hostname:port of host.dc.domain:22 +// could be configured as follows: +// http://{{.GetTabletHostPort}} -> http://host.dc.domain:22 +// https://{{.Tablet.Hostname}} -> https://host.dc.domain +// https://{{.GetHostNameLevel 0}}.bastion.corp -> https://host.bastion.corp +func (e TabletStats) getTabletDebugURL() string { + var buffer bytes.Buffer + tabletURLTemplate.Execute(&buffer, e) + return buffer.String() +} + // HealthCheck defines the interface of health checking module. // The goal of this object is to maintain a StreamHealth RPC // to a lot of tablets. Tablets are added / removed by calling the @@ -737,7 +791,6 @@ func (tcs *TabletsCacheStatus) StatusAsHTML() template.HTML { sort.Sort(tcs.TabletsStats) } for _, ts := range tcs.TabletsStats { - vtPort := ts.Tablet.PortMap["vt"] color := "green" extra := "" if ts.LastError != nil { @@ -755,11 +808,10 @@ func (tcs *TabletsCacheStatus) StatusAsHTML() template.HTML { extra = fmt.Sprintf(" (RepLag: %v)", ts.Stats.SecondsBehindMaster) } name := ts.Name - addr := netutil.JoinHostPort(ts.Tablet.Hostname, vtPort) if name == "" { - name = addr + name = ts.GetTabletHostPort() } - tLinks = append(tLinks, fmt.Sprintf(`%v%v`, addr, color, name, extra)) + tLinks = append(tLinks, fmt.Sprintf(`%v%v`, ts.getTabletDebugURL(), color, name, extra)) } return template.HTML(strings.Join(tLinks, "
")) } diff --git a/go/vt/discovery/healthcheck_test.go b/go/vt/discovery/healthcheck_test.go index 637d8445a81..1e6086da33f 100644 --- a/go/vt/discovery/healthcheck_test.go +++ b/go/vt/discovery/healthcheck_test.go @@ -23,6 +23,8 @@ import ( "html/template" "io" "reflect" + "strings" + "sync" "testing" "time" @@ -35,9 +37,6 @@ import ( "vitess.io/vitess/go/vt/vttablet/queryservice/fakes" "vitess.io/vitess/go/vt/vttablet/tabletconn" - "strings" - "sync" - querypb "vitess.io/vitess/go/vt/proto/query" topodatapb "vitess.io/vitess/go/vt/proto/topodata" ) @@ -573,6 +572,42 @@ func TestTemplate(t *testing.T) { } } +func TestDebugURLFormatting(t *testing.T) { + flag.Set("tablet_url_template", "https://{{.GetHostNameLevel 0}}.bastion.{{.Tablet.Alias.Cell}}.corp") + loadTabletURLTemplate() + + tablet := topo.NewTablet(0, "cell", "host.dc.domain") + ts := []*TabletStats{ + { + Key: "a", + Tablet: tablet, + Target: &querypb.Target{Keyspace: "k", Shard: "s", TabletType: topodatapb.TabletType_REPLICA}, + Up: true, + Serving: false, + Stats: &querypb.RealtimeStats{SecondsBehindMaster: 1, CpuUsage: 0.3}, + TabletExternallyReparentedTimestamp: 0, + }, + } + tcs := &TabletsCacheStatus{ + Cell: "cell", + Target: &querypb.Target{Keyspace: "k", Shard: "s", TabletType: topodatapb.TabletType_REPLICA}, + TabletsStats: ts, + } + templ := template.New("").Funcs(status.StatusFuncs) + templ, err := templ.Parse(HealthCheckTemplate) + if err != nil { + t.Fatalf("error parsing template: %v", err) + } + wr := &bytes.Buffer{} + if err := templ.Execute(wr, []*TabletsCacheStatus{tcs}); err != nil { + t.Fatalf("error executing template: %v", err) + } + expectedURL := `"https://host.bastion.cell.corp"` + if !strings.Contains(wr.String(), expectedURL) { + t.Fatalf("output missing formatted URL, expectedURL: %s , output: %s", expectedURL, wr.String()) + } +} + type listener struct { output chan *TabletStats }