From 6bd72a9f8d9101336024a373f13b9a4156a8244c Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Tue, 29 Mar 2022 18:11:08 +0200 Subject: [PATCH 1/4] Clarify and clean up template contract for status Ring was passing a mix of time.Time and formatted string for the time: unified to pass always time.Time, and decide what to do when rendering. This will allow us some extra customisation if replacing the template with a non-default one. Also replaced the "Unhealthy" state by "UNHEALTHY" in the template, because the rest of states are uppercase. Not replacing the constant value because it's being used for metrics and we'd have to fix the recording rules in all the downstream projects. Changed how heartbeat time is rendered: rendering the duration since the heartbeat and the time in parenthesis: heartbeats are so common that showing a date doesn't make sense, and since they're usually few seconds away, the "duration ago" is what makes more sense IMO. Signed-off-by: Oleg Zaytsev --- kv/memberlist/kv_init_service.go | 13 ++----- ring/http.go | 60 +++++++++++++++----------------- ring/status.gohtml | 6 ++-- 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/kv/memberlist/kv_init_service.go b/kv/memberlist/kv_init_service.go index 404c49d8b..968bbe5ac 100644 --- a/kv/memberlist/kv_init_service.go +++ b/kv/memberlist/kv_init_service.go @@ -171,21 +171,13 @@ func (kvs *KVInitService) ServeHTTP(w http.ResponseWriter, req *http.Request) { if strings.Contains(accept, "application/json") { w.Header().Set("Content-Type", "application/json") - data, err := json.Marshal(v) - if err != nil { + if err := json.NewEncoder(w).Encode(v); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return } - - // We ignore errors here, because we cannot do anything about them. - // Write will trigger sending Status code, so we cannot send a different status code afterwards. - // Also this isn't internal error, but error communicating with client. - _, _ = w.Write(data) return } - err := defaultPageTemplate.Execute(w, v) - if err != nil { + if err := defaultPageTemplate.Execute(w, v); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } @@ -226,7 +218,6 @@ func viewKey(w http.ResponseWriter, store map[string]valueDesc, key string, form } func formatValue(w http.ResponseWriter, val interface{}, format string) { - w.WriteHeader(200) w.Header().Add("content-type", "text/plain") diff --git a/ring/http.go b/ring/http.go index c5ad18e9e..0fa0e036b 100644 --- a/ring/http.go +++ b/ring/http.go @@ -17,26 +17,36 @@ import ( var defaultPageContent string var defaultPageTemplate = template.Must(template.New("webpage").Funcs(template.FuncMap{ "mod": func(i, j int) bool { return i%j == 0 }, + "humanFloat": func(f float64) string { + return fmt.Sprintf("%.4g", f) + }, + "timeOrEmptyString": func(t time.Time) string { + if t.IsZero() { + return "" + } + return t.Format(time.RFC3339Nano) + }, + "durationSince": func(t time.Time) string { return time.Since(t).Truncate(time.Millisecond).String() }, }).Parse(defaultPageContent)) -type ingesterDesc struct { - ID string `json:"id"` - State string `json:"state"` - Address string `json:"address"` - HeartbeatTimestamp string `json:"timestamp"` - RegisteredTimestamp string `json:"registered_timestamp"` - Zone string `json:"zone"` - Tokens []uint32 `json:"tokens"` - NumTokens int `json:"-"` - Ownership float64 `json:"-"` -} - type httpResponse struct { Ingesters []ingesterDesc `json:"shards"` Now time.Time `json:"now"` ShowTokens bool `json:"-"` } +type ingesterDesc struct { + ID string `json:"id"` + State string `json:"state"` + Address string `json:"address"` + HeartbeatTimestamp time.Time `json:"timestamp"` + RegisteredTimestamp time.Time `json:"registered_timestamp"` + Zone string `json:"zone"` + Tokens []uint32 `json:"tokens"` + NumTokens int `json:"-"` + Ownership float64 `json:"-"` +} + type ringAccess interface { casRing(ctx context.Context, f func(in interface{}) (out interface{}, retry bool, err error)) error getRing(context.Context) (*Desc, error) @@ -85,7 +95,7 @@ func (h *ringPageHandler) handle(w http.ResponseWriter, req *http.Request) { } _, ownedTokens := ringDesc.countTokens() - ingesterIDs := []string{} + var ingesterIDs []string for id := range ringDesc.Ingesters { ingesterIDs = append(ingesterIDs, id) } @@ -95,28 +105,21 @@ func (h *ringPageHandler) handle(w http.ResponseWriter, req *http.Request) { var ingesters []ingesterDesc for _, id := range ingesterIDs { ing := ringDesc.Ingesters[id] - heartbeatTimestamp := time.Unix(ing.Timestamp, 0) state := ing.State.String() if !ing.IsHealthy(Reporting, h.heartbeatPeriod, now) { - state = unhealthy - } - - // Format the registered timestamp. - registeredTimestamp := "" - if ing.RegisteredTimestamp != 0 { - registeredTimestamp = ing.GetRegisteredAt().String() + state = "UNHEALTHY" } ingesters = append(ingesters, ingesterDesc{ ID: id, State: state, Address: ing.Addr, - HeartbeatTimestamp: heartbeatTimestamp.String(), - RegisteredTimestamp: registeredTimestamp, + HeartbeatTimestamp: time.Unix(ing.Timestamp, 0).UTC(), + RegisteredTimestamp: ing.GetRegisteredAt().UTC(), Tokens: ing.Tokens, Zone: ing.Zone, NumTokens: len(ing.Tokens), - Ownership: (float64(ownedTokens[id]) / float64(math.MaxUint32)) * 100, + Ownership: float64(ownedTokens[id]) / float64(math.MaxUint32), }) } @@ -161,14 +164,7 @@ func (h *ringPageHandler) forget(ctx context.Context, id string) error { func writeJSONResponse(w http.ResponseWriter, v httpResponse) { w.Header().Set("Content-Type", "application/json") - data, err := json.Marshal(v) - if err != nil { + if err := json.NewEncoder(w).Encode(v); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return } - - // We ignore errors here, because we cannot do anything about them. - // Write will trigger sending Status code, so we cannot send a different status code afterwards. - // Also this isn't internal error, but error communicating with client. - _, _ = w.Write(data) } diff --git a/ring/status.gohtml b/ring/status.gohtml index 4a8832ffa..80e5b6a8f 100644 --- a/ring/status.gohtml +++ b/ring/status.gohtml @@ -35,10 +35,10 @@ {{ .Zone }} {{ .State }} {{ .Address }} - {{ .RegisteredTimestamp }} - {{ .HeartbeatTimestamp }} + {{ .RegisteredTimestamp | timeOrEmptyString }} + {{ .HeartbeatTimestamp | durationSince }} ago ({{ .HeartbeatTimestamp.Format "15:04:05.999" }}) {{ .NumTokens }} - {{ .Ownership }}% + {{ .Ownership | humanFloat }}% From 0b963d9d335c3358ca0b88b7155b57ae1a2fa27f Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Tue, 29 Mar 2022 18:17:36 +0200 Subject: [PATCH 2/4] Update CHANGELOG.md Signed-off-by: Oleg Zaytsev --- CHANGELOG.md | 1 + ring/http.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3ba2f72..23bafebb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * [CHANGE] ring/client: It's now possible to set different value than `consul` as default KV store. #120 * [CHANGE] Lifecycler: Default value of lifecycler's `final-sleep` is now `0s` (i.e. no sleep). #121 * [CHANGE] Lifecycler: It's now possible to change default value of lifecycler's `final-sleep`. #121 +* [CHANGE] Minor cosmetic changes in ring and memberlist HTTP status templates. #149 * [ENHANCEMENT] Add middleware package. #38 * [ENHANCEMENT] Add the ring package #45 * [ENHANCEMENT] Add limiter package. #41 diff --git a/ring/http.go b/ring/http.go index 0fa0e036b..809013f5a 100644 --- a/ring/http.go +++ b/ring/http.go @@ -119,7 +119,7 @@ func (h *ringPageHandler) handle(w http.ResponseWriter, req *http.Request) { Tokens: ing.Tokens, Zone: ing.Zone, NumTokens: len(ing.Tokens), - Ownership: float64(ownedTokens[id]) / float64(math.MaxUint32), + Ownership: (float64(ownedTokens[id]) / float64(math.MaxUint32)) * 100, }) } From 7a42efffe77a6cea8a46e47e23b5d5df8d3293be Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 30 Mar 2022 09:43:53 +0200 Subject: [PATCH 3/4] Use only 2 decimals for percentage We don't need 4 Signed-off-by: Oleg Zaytsev --- ring/http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ring/http.go b/ring/http.go index 809013f5a..3c842d0ee 100644 --- a/ring/http.go +++ b/ring/http.go @@ -18,7 +18,7 @@ var defaultPageContent string var defaultPageTemplate = template.Must(template.New("webpage").Funcs(template.FuncMap{ "mod": func(i, j int) bool { return i%j == 0 }, "humanFloat": func(f float64) string { - return fmt.Sprintf("%.4g", f) + return fmt.Sprintf("%.2g", f) }, "timeOrEmptyString": func(t time.Time) string { if t.IsZero() { From d2926ce863a81aad855a73401b9069c03f8d219f Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 30 Mar 2022 09:45:10 +0200 Subject: [PATCH 4/4] Add 'Content-Type: text/html' header Signed-off-by: Oleg Zaytsev --- kv/memberlist/kv_init_service.go | 1 + ring/http.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/kv/memberlist/kv_init_service.go b/kv/memberlist/kv_init_service.go index 968bbe5ac..1a8313cde 100644 --- a/kv/memberlist/kv_init_service.go +++ b/kv/memberlist/kv_init_service.go @@ -177,6 +177,7 @@ func (kvs *KVInitService) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } + w.Header().Set("Content-Type", "text/html") if err := defaultPageTemplate.Execute(w, v); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } diff --git a/ring/http.go b/ring/http.go index 3c842d0ee..18a56177c 100644 --- a/ring/http.go +++ b/ring/http.go @@ -141,8 +141,8 @@ func renderHTTPResponse(w http.ResponseWriter, v httpResponse, t *template.Templ return } - err := t.Execute(w, v) - if err != nil { + w.Header().Set("Content-Type", "text/html") + if err := t.Execute(w, v); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }