diff --git a/kv/memberlist/kv_init_service.go b/kv/memberlist/kv_init_service.go index e63aac2f4..a15e194c4 100644 --- a/kv/memberlist/kv_init_service.go +++ b/kv/memberlist/kv_init_service.go @@ -2,6 +2,7 @@ package memberlist import ( "context" + _ "embed" "encoding/json" "fmt" "html/template" @@ -157,7 +158,7 @@ func (kvs *KVInitService) ServeHTTP(w http.ResponseWriter, req *http.Request) { sent, received := kv.getSentAndReceivedMessages() - v := pageData{ + v := statusPageData{ Now: time.Now(), Memberlist: kv.memberlist, SortedMembers: members, @@ -183,7 +184,7 @@ func (kvs *KVInitService) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - err := pageTemplate.Execute(w, v) + err := defaultPageTemplate.Execute(w, v) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } @@ -276,7 +277,9 @@ func downloadKey(w http.ResponseWriter, kv *KV, store map[string]valueDesc, key _, _ = w.Write(encoded) } -type pageData struct { +// statusPageData is the root element of the status page served through HTTP. +// Depending on the accepted content-type, it will be either marshaled as JSON or provided to the rendered HTML template. +type statusPageData struct { Now time.Time Memberlist *memberlist.Memberlist SortedMembers []*memberlist.Node @@ -285,149 +288,8 @@ type pageData struct { ReceivedMessages []message } -var pageTemplate = template.Must(template.New("webpage").Funcs(template.FuncMap{ +//go:embed status.gohtml +var defaultPageContent string +var defaultPageTemplate = template.Must(template.New("webpage").Funcs(template.FuncMap{ "StringsJoin": strings.Join, -}).Parse(pageContent)) - -const pageContent = ` - - - - - Memberlist Status - - -

Memberlist Status

-

Current time: {{ .Now }}

- - - -

KV Store

- - - - - - - - - - - - {{ range $k, $v := .Store }} - - - - - - {{ end }} - -
KeyValue DetailsActions
{{ $k }}{{ $v }} - json - | json-pretty - | struct - | download -
- -

Note that value "version" is node-specific. It starts with 0 (on restart), and increases on each received update. Size is in bytes.

- -

Memberlist Cluster Members

- - - - - - - - - - - - {{ range .SortedMembers }} - - - - - - {{ end }} - -
NameAddressState
{{ .Name }}{{ .Address }}{{ .State }}
- -

State: 0 = Alive, 1 = Suspect, 2 = Dead, 3 = Left

- -

Received Messages

- - Delete All Messages (received and sent) - - - - - - - - - - - - - - - - {{ range .ReceivedMessages }} - - - - - - - - - - {{ end }} - -
IDTimeKeyValue in the MessageVersion After Update (0 = no change)ChangesActions
{{ .ID }}{{ .Time.Format "15:04:05.000" }}{{ .Pair.Key }}size: {{ .Pair.Value | len }}, codec: {{ .Pair.Codec }}{{ .Version }}{{ StringsJoin .Changes ", " }} - json - | json-pretty - | struct -
- -

Sent Messages

- - Delete All Messages (received and sent) - - - - - - - - - - - - - - - - {{ range .SentMessages }} - - - - - - - - - - {{ end }} - -
IDTimeKeyValueVersionChangesActions
{{ .ID }}{{ .Time.Format "15:04:05.000" }}{{ .Pair.Key }}size: {{ .Pair.Value | len }}, codec: {{ .Pair.Codec }}{{ .Version }}{{ StringsJoin .Changes ", " }} - json - | json-pretty - | struct -
- -` +}).Parse(defaultPageContent)) diff --git a/kv/memberlist/kv_init_service_test.go b/kv/memberlist/kv_init_service_test.go index 35c668433..4d6f5e287 100644 --- a/kv/memberlist/kv_init_service_test.go +++ b/kv/memberlist/kv_init_service_test.go @@ -21,7 +21,7 @@ func TestPage(t *testing.T) { _ = ml.Shutdown() }) - require.NoError(t, pageTemplate.Execute(&bytes.Buffer{}, pageData{ + require.NoError(t, defaultPageTemplate.Execute(&bytes.Buffer{}, statusPageData{ Now: time.Now(), Memberlist: ml, SortedMembers: ml.Members(), diff --git a/kv/memberlist/memberlist_client.go b/kv/memberlist/memberlist_client.go index 30f0992d3..4570f2778 100644 --- a/kv/memberlist/memberlist_client.go +++ b/kv/memberlist/memberlist_client.go @@ -8,6 +8,7 @@ import ( "errors" "flag" "fmt" + "html/template" "math" "strings" "sync" @@ -164,6 +165,10 @@ type KVConfig struct { // Codecs to register. Codecs need to be registered before joining other members. Codecs []codec.Codec `yaml:"-"` + + // CustomHTTPHandlerTemplate will be rendered by HTTPHandler instead of the embedded default one, if provided. + // This option is set internally and never exposed to the user. + CustomHTTPHandlerTemplate *template.Template `yaml:"-"` } // RegisterFlagsWithPrefix registers flags. diff --git a/kv/memberlist/status.gohtml b/kv/memberlist/status.gohtml new file mode 100644 index 000000000..3ab6d0936 --- /dev/null +++ b/kv/memberlist/status.gohtml @@ -0,0 +1,143 @@ +{{- /*gotype: github.com/grafana/dskit/kv/memberlist.statusPageData */ -}} + + + + + Memberlist Status + + +

Memberlist Status

+

Current time: {{ .Now }}

+ + + +

KV Store

+ + + + + + + + + + + + {{ range $k, $v := .Store }} + + + + + + {{ end }} + +
KeyValue DetailsActions
{{ $k }}{{ $v }} + json + | json-pretty + | struct + | download +
+ +

Note that value "version" is node-specific. It starts with 0 (on restart), and increases on each received update. + Size is in bytes.

+ +

Memberlist Cluster Members

+ + + + + + + + + + + + {{ range .SortedMembers }} + + + + + + {{ end }} + +
NameAddressState
{{ .Name }}{{ .Address }}{{ .State }}
+ +

State: 0 = Alive, 1 = Suspect, 2 = Dead, 3 = Left

+ +

Received Messages

+ +Delete All Messages (received and sent) + + + + + + + + + + + + + + + + {{ range .ReceivedMessages }} + + + + + + + + + + {{ end }} + +
IDTimeKeyValue in the MessageVersion After Update (0 = no change)ChangesActions
{{ .ID }}{{ .Time.Format "15:04:05.000" }}{{ .Pair.Key }}size: {{ .Pair.Value | len }}, codec: {{ .Pair.Codec }}{{ .Version }}{{ StringsJoin .Changes ", " }} + json + | json-pretty + | struct +
+ +

Sent Messages

+ +Delete All Messages (received and sent) + + + + + + + + + + + + + + + + {{ range .SentMessages }} + + + + + + + + + + {{ end }} + +
IDTimeKeyValueVersionChangesActions
{{ .ID }}{{ .Time.Format "15:04:05.000" }}{{ .Pair.Key }}size: {{ .Pair.Value | len }}, codec: {{ .Pair.Codec }}{{ .Version }}{{ StringsJoin .Changes ", " }} + json + | json-pretty + | struct +
+ + \ No newline at end of file diff --git a/ring/basic_lifecycler.go b/ring/basic_lifecycler.go index 32775c982..bb43ea207 100644 --- a/ring/basic_lifecycler.go +++ b/ring/basic_lifecycler.go @@ -3,6 +3,7 @@ package ring import ( "context" "fmt" + "html/template" "net/http" "sort" "sync" @@ -55,6 +56,9 @@ type BasicLifecyclerConfig struct { // If true lifecycler doesn't unregister instance from the ring when it's stopping. Default value is false, // which means unregistering. KeepInstanceInTheRingOnShutdown bool + + // CustomHTTPHandlerTemplate will be rendered by HTTPHandler instead of the embedded default one, if provided. + CustomHTTPHandlerTemplate *template.Template } // BasicLifecycler is a basic ring lifecycler which allows to hook custom @@ -507,5 +511,5 @@ func (l *BasicLifecycler) getRing(ctx context.Context) (*Desc, error) { } func (l *BasicLifecycler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - newRingPageHandler(l, l.cfg.HeartbeatPeriod).handle(w, req) + newRingPageHandler(l, l.cfg.HeartbeatPeriod, l.cfg.CustomHTTPHandlerTemplate).handle(w, req) } diff --git a/ring/http.go b/ring/http.go index 1d6c10e80..39185738d 100644 --- a/ring/http.go +++ b/ring/http.go @@ -2,6 +2,7 @@ package ring import ( "context" + _ "embed" "encoding/json" "fmt" "html/template" @@ -12,80 +13,14 @@ import ( "time" ) -const pageContent = ` - - - - - Ring Status - - -

Ring Status

-

Current time: {{ .Now }}

-
- - - - - - - - - - - - - - - - - {{ range $i, $ing := .Ingesters }} - {{ if mod $i 2 }} - - {{ else }} - - {{ end }} - - - - - - - - - - - {{ end }} - -
Instance IDAvailability ZoneStateAddressRegistered AtLast HeartbeatTokensOwnershipActions
{{ .ID }}{{ .Zone }}{{ .State }}{{ .Address }}{{ .RegisteredTimestamp }}{{ .HeartbeatTimestamp }}{{ .NumTokens }}{{ .Ownership }}%
-
- {{ if .ShowTokens }} - - {{ else }} - - {{ end }} - - {{ if .ShowTokens }} - {{ range $i, $ing := .Ingesters }} -

Instance: {{ .ID }}

-

- Tokens:
- {{ range $token := .Tokens }} - {{ $token }} - {{ end }} -

- {{ end }} - {{ end }} -
- -` - -var pageTemplate *template.Template +//go:embed status.gohtml +var defaultPageContent string +var defaultPageTemplate *template.Template func init() { t := template.New("webpage") t.Funcs(template.FuncMap{"mod": func(i, j int) bool { return i%j == 0 }}) - pageTemplate = template.Must(t.Parse(pageContent)) + defaultPageTemplate = template.Must(t.Parse(defaultPageContent)) } type ingesterDesc struct { @@ -114,12 +49,17 @@ type ringAccess interface { type ringPageHandler struct { r ringAccess heartbeatPeriod time.Duration + template *template.Template } -func newRingPageHandler(r ringAccess, heartbeatPeriod time.Duration) *ringPageHandler { +func newRingPageHandler(r ringAccess, heartbeatPeriod time.Duration, template *template.Template) *ringPageHandler { + if template == nil { + template = defaultPageTemplate + } return &ringPageHandler{ r: r, heartbeatPeriod: heartbeatPeriod, + template: template, } } @@ -195,7 +135,7 @@ func (h *ringPageHandler) handle(w http.ResponseWriter, req *http.Request) { Ingesters: ingesters, Now: now, ShowTokens: tokensParam == "true", - }, pageTemplate, req) + }, h.template, req) } // RenderHTTPResponse either responds with json or a rendered html page using the passed in template diff --git a/ring/lifecycler.go b/ring/lifecycler.go index 43d458ced..409966fb8 100644 --- a/ring/lifecycler.go +++ b/ring/lifecycler.go @@ -4,6 +4,7 @@ import ( "context" "flag" "fmt" + "html/template" "net/http" "os" "sort" @@ -50,6 +51,10 @@ type LifecyclerConfig struct { // Injected internally ListenPort int `yaml:"-"` + + // CustomHTTPHandlerTemplate will be rendered by HTTPHandler instead of the embedded default one, if provided. + // This option is set internally and never exposed to the user. + CustomHTTPHandlerTemplate *template.Template `yaml:"-"` } // RegisterFlags adds the flags required to config this to the given FlagSet. @@ -870,7 +875,7 @@ func (i *Lifecycler) getRing(ctx context.Context) (*Desc, error) { } func (i *Lifecycler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - newRingPageHandler(i, i.cfg.HeartbeatPeriod).handle(w, req) + newRingPageHandler(i, i.cfg.HeartbeatPeriod, i.cfg.CustomHTTPHandlerTemplate).handle(w, req) } // unregister removes our entry from consul. diff --git a/ring/ring.go b/ring/ring.go index 6c7e4a49f..a23775297 100644 --- a/ring/ring.go +++ b/ring/ring.go @@ -6,6 +6,7 @@ import ( "context" "flag" "fmt" + "html/template" "math" "math/rand" "net/http" @@ -131,6 +132,10 @@ type Config struct { // Whether the shuffle-sharding subring cache is disabled. This option is set // internally and never exposed to the user. SubringCacheDisabled bool `yaml:"-"` + + // CustomHTTPHandlerTemplate will be rendered by HTTPHandler instead of the embedded default one, if provided. + // This option is set internally and never exposed to the user. + CustomHTTPHandlerTemplate *template.Template `yaml:"-"` } // RegisterFlags adds the flags required to config this to the given FlagSet with a specified prefix @@ -861,7 +866,7 @@ func (r *Ring) getRing(ctx context.Context) (*Desc, error) { } func (r *Ring) ServeHTTP(w http.ResponseWriter, req *http.Request) { - newRingPageHandler(r, r.cfg.HeartbeatTimeout).handle(w, req) + newRingPageHandler(r, r.cfg.HeartbeatTimeout, r.cfg.CustomHTTPHandlerTemplate).handle(w, req) } // Operation describes which instances can be included in the replica set, based on their state. diff --git a/ring/status.gohtml b/ring/status.gohtml new file mode 100644 index 000000000..4a8832ffa --- /dev/null +++ b/ring/status.gohtml @@ -0,0 +1,69 @@ +{{- /*gotype: github.com/grafana/dskit/ring.httpResponse */ -}} + + + + + Ring Status + + +

Ring Status

+

Current time: {{ .Now }}

+
+ + + + + + + + + + + + + + + + + {{ range $i, $ing := .Ingesters }} + {{ if mod $i 2 }} + + {{ else }} + + {{ end }} + + + + + + + + + + + {{ end }} + +
Instance IDAvailability ZoneStateAddressRegistered AtLast HeartbeatTokensOwnershipActions
{{ .ID }}{{ .Zone }}{{ .State }}{{ .Address }}{{ .RegisteredTimestamp }}{{ .HeartbeatTimestamp }}{{ .NumTokens }}{{ .Ownership }}% + +
+
+ {{ if .ShowTokens }} + + {{ else }} + + {{ end }} + + {{ if .ShowTokens }} + {{ range $i, $ing := .Ingesters }} +

Instance: {{ .ID }}

+

+ Tokens:
+ {{ range $token := .Tokens }} + {{ $token }} + {{ end }} +

+ {{ end }} + {{ end }} +
+ + \ No newline at end of file