diff --git a/CHANGELOG.md b/CHANGELOG.md index eab21a4ec..9cba7fa8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ * [ENHANCEMENT] Ring: Add ring page handler to BasicLifecycler and Lifecycler. #112 * [ENHANCEMENT] Lifecycler: It's now possible to change default value of lifecycler's `final-sleep`. #121 * [ENHANCEMENT] Memberlist: Update to latest fork of memberlist. #160 +* [ENHANCEMENT] Memberlist: extracted HTTP status page handler to `memberlist.HTTPStatusHandler` which now can be instantiated with a custom template. #163 * [BUGFIX] spanlogger: Support multiple tenant IDs. #59 * [BUGFIX] Memberlist: fixed corrupted packets when sending compound messages with more than 255 messages or messages bigger than 64KB. #85 * [BUGFIX] Ring: `ring_member_ownership_percent` and `ring_tokens_owned` metrics are not updated on scale down. #109 diff --git a/kv/memberlist/http_status_handler.go b/kv/memberlist/http_status_handler.go new file mode 100644 index 000000000..5fae4d0c7 --- /dev/null +++ b/kv/memberlist/http_status_handler.go @@ -0,0 +1,217 @@ +package memberlist + +import ( + _ "embed" + "encoding/json" + "fmt" + "html/template" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/hashicorp/memberlist" +) + +// HTTPStatusHandler is a http.Handler with status information about memberlist. +type HTTPStatusHandler struct { + kvs *KVInitService + tpl *template.Template +} + +// StatusPageData represents the data passed to the template rendered by HTTPStatusHandler +type StatusPageData struct { + Now time.Time + Memberlist *memberlist.Memberlist + SortedMembers []*memberlist.Node + Store map[string]ValueDesc + SentMessages []Message + ReceivedMessages []Message +} + +// NewHTTPStatusHandler creates a new HTTPStatusHandler that will render the provided template using the data from StatusPageData. +func NewHTTPStatusHandler(kvs *KVInitService, tpl *template.Template) HTTPStatusHandler { + return HTTPStatusHandler{kvs, tpl} +} + +func (h HTTPStatusHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + kv := h.kvs.getKV() + if kv == nil { + w.Header().Set("Content-Type", "text/plain") + // Ignore inactionable errors. + _, _ = w.Write([]byte("This instance doesn't use memberlist.")) + return + } + + const ( + downloadKeyParam = "downloadKey" + viewKeyParam = "viewKey" + viewMsgParam = "viewMsg" + deleteMessagesParam = "deleteMessages" + ) + + if err := req.ParseForm(); err == nil { + if req.Form[downloadKeyParam] != nil { + downloadKey(w, kv, kv.storeCopy(), req.Form[downloadKeyParam][0]) // Use first value, ignore the rest. + return + } + + if req.Form[viewKeyParam] != nil { + viewKey(w, kv.storeCopy(), req.Form[viewKeyParam][0], getFormat(req)) + return + } + + if req.Form[viewMsgParam] != nil { + msgID, err := strconv.Atoi(req.Form[viewMsgParam][0]) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + sent, received := kv.getSentAndReceivedMessages() + + for _, m := range append(sent, received...) { + if m.ID == msgID { + viewMessage(w, kv, m, getFormat(req)) + return + } + } + + http.Error(w, "message not found", http.StatusNotFound) + return + } + + if len(req.Form[deleteMessagesParam]) > 0 && req.Form[deleteMessagesParam][0] == "true" { + kv.deleteSentReceivedMessages() + + // Redirect back. + w.Header().Set("Location", "?"+deleteMessagesParam+"=false") + w.WriteHeader(http.StatusFound) + return + } + } + + members := kv.memberlist.Members() + sort.Slice(members, func(i, j int) bool { + return members[i].Name < members[j].Name + }) + + sent, received := kv.getSentAndReceivedMessages() + + v := StatusPageData{ + Now: time.Now(), + Memberlist: kv.memberlist, + SortedMembers: members, + Store: kv.storeCopy(), + SentMessages: sent, + ReceivedMessages: received, + } + + accept := req.Header.Get("Accept") + if strings.Contains(accept, "application/json") { + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(v); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + w.Header().Set("Content-Type", "text/html") + if err := h.tpl.Execute(w, v); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func getFormat(req *http.Request) string { + const viewFormat = "format" + + format := "" + if len(req.Form[viewFormat]) > 0 { + format = req.Form[viewFormat][0] + } + return format +} + +func viewMessage(w http.ResponseWriter, kv *KV, msg Message, format string) { + c := kv.GetCodec(msg.Pair.Codec) + if c == nil { + http.Error(w, "codec not found", http.StatusNotFound) + return + } + + val, err := c.Decode(msg.Pair.Value) + if err != nil { + http.Error(w, fmt.Sprintf("failed to decode: %v", err), http.StatusInternalServerError) + return + } + + formatValue(w, val, format) +} + +func viewKey(w http.ResponseWriter, store map[string]ValueDesc, key string, format string) { + if store[key].value == nil { + http.Error(w, "value not found", http.StatusNotFound) + return + } + + formatValue(w, store[key].value, format) +} + +func formatValue(w http.ResponseWriter, val interface{}, format string) { + w.WriteHeader(200) + w.Header().Add("content-type", "text/plain") + + switch format { + case "json", "json-pretty": + enc := json.NewEncoder(w) + if format == "json-pretty" { + enc.SetIndent("", " ") + } + + err := enc.Encode(val) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + default: + _, _ = fmt.Fprintf(w, "%#v", val) + } +} + +func downloadKey(w http.ResponseWriter, kv *KV, store map[string]ValueDesc, key string) { + if store[key].value == nil { + http.Error(w, "value not found", http.StatusNotFound) + return + } + + val := store[key] + + c := kv.GetCodec(store[key].CodecID) + if c == nil { + http.Error(w, "codec not found", http.StatusNotFound) + return + } + + encoded, err := c.Encode(val.value) + if err != nil { + http.Error(w, fmt.Sprintf("failed to encode: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Add("content-type", "application/octet-stream") + // Set content-length so that client knows whether it has received full response or not. + w.Header().Add("content-length", strconv.Itoa(len(encoded))) + w.Header().Add("content-disposition", fmt.Sprintf("attachment; filename=%d-%s", val.Version, key)) + w.WriteHeader(200) + + // Ignore errors, we cannot do anything about them. + _, _ = w.Write(encoded) +} + +//go:embed status.gohtml +var defaultPageContent string +var defaultPageTemplate = template.Must(template.New("webpage").Funcs(template.FuncMap{ + "StringsJoin": strings.Join, +}).Parse(defaultPageContent)) diff --git a/kv/memberlist/http_status_handler_test.go b/kv/memberlist/http_status_handler_test.go new file mode 100644 index 000000000..0752e01c0 --- /dev/null +++ b/kv/memberlist/http_status_handler_test.go @@ -0,0 +1,52 @@ +package memberlist + +import ( + "bytes" + "testing" + "time" + + "github.com/hashicorp/memberlist" + "github.com/stretchr/testify/require" +) + +func TestPage(t *testing.T) { + conf := memberlist.DefaultLANConfig() + ml, err := memberlist.Create(conf) + require.NoError(t, err) + + t.Cleanup(func() { + _ = ml.Shutdown() + }) + + require.NoError(t, defaultPageTemplate.Execute(&bytes.Buffer{}, StatusPageData{ + Now: time.Now(), + Memberlist: ml, + SortedMembers: ml.Members(), + Store: nil, + ReceivedMessages: []Message{{ + ID: 10, + Time: time.Now(), + Size: 50, + Pair: KeyValuePair{ + Key: "hello", + Value: []byte("world"), + Codec: "codec", + }, + Version: 20, + Changes: []string{"A", "B", "C"}, + }}, + + SentMessages: []Message{{ + ID: 10, + Time: time.Now(), + Size: 50, + Pair: KeyValuePair{ + Key: "hello", + Value: []byte("world"), + Codec: "codec", + }, + Version: 20, + Changes: []string{"A", "B", "C"}, + }}, + })) +} diff --git a/kv/memberlist/kv_init_service.go b/kv/memberlist/kv_init_service.go index 1a8313cde..5b505a548 100644 --- a/kv/memberlist/kv_init_service.go +++ b/kv/memberlist/kv_init_service.go @@ -2,19 +2,10 @@ package memberlist import ( "context" - _ "embed" - "encoding/json" - "fmt" - "html/template" "net/http" - "sort" - "strconv" - "strings" "sync" - "time" "github.com/go-kit/log" - "github.com/hashicorp/memberlist" "github.com/prometheus/client_golang/prometheus" "go.uber.org/atomic" @@ -95,191 +86,5 @@ func (kvs *KVInitService) stopping(_ error) error { } func (kvs *KVInitService) ServeHTTP(w http.ResponseWriter, req *http.Request) { - kv := kvs.getKV() - if kv == nil { - w.Header().Set("Content-Type", "text/plain") - // Ignore inactionable errors. - _, _ = w.Write([]byte("This instance doesn't use memberlist.")) - return - } - - const ( - downloadKeyParam = "downloadKey" - viewKeyParam = "viewKey" - viewMsgParam = "viewMsg" - deleteMessagesParam = "deleteMessages" - ) - - if err := req.ParseForm(); err == nil { - if req.Form[downloadKeyParam] != nil { - downloadKey(w, kv, kv.storeCopy(), req.Form[downloadKeyParam][0]) // Use first value, ignore the rest. - return - } - - if req.Form[viewKeyParam] != nil { - viewKey(w, kv.storeCopy(), req.Form[viewKeyParam][0], getFormat(req)) - return - } - - if req.Form[viewMsgParam] != nil { - msgID, err := strconv.Atoi(req.Form[viewMsgParam][0]) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - sent, received := kv.getSentAndReceivedMessages() - - for _, m := range append(sent, received...) { - if m.ID == msgID { - viewMessage(w, kv, m, getFormat(req)) - return - } - } - - http.Error(w, "message not found", http.StatusNotFound) - return - } - - if len(req.Form[deleteMessagesParam]) > 0 && req.Form[deleteMessagesParam][0] == "true" { - kv.deleteSentReceivedMessages() - - // Redirect back. - w.Header().Set("Location", "?"+deleteMessagesParam+"=false") - w.WriteHeader(http.StatusFound) - return - } - } - - members := kv.memberlist.Members() - sort.Slice(members, func(i, j int) bool { - return members[i].Name < members[j].Name - }) - - sent, received := kv.getSentAndReceivedMessages() - - v := pageData{ - Now: time.Now(), - Memberlist: kv.memberlist, - SortedMembers: members, - Store: kv.storeCopy(), - SentMessages: sent, - ReceivedMessages: received, - } - - accept := req.Header.Get("Accept") - if strings.Contains(accept, "application/json") { - w.Header().Set("Content-Type", "application/json") - - if err := json.NewEncoder(w).Encode(v); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - - w.Header().Set("Content-Type", "text/html") - if err := defaultPageTemplate.Execute(w, v); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func getFormat(req *http.Request) string { - const viewFormat = "format" - - format := "" - if len(req.Form[viewFormat]) > 0 { - format = req.Form[viewFormat][0] - } - return format -} - -func viewMessage(w http.ResponseWriter, kv *KV, msg message, format string) { - c := kv.GetCodec(msg.Pair.Codec) - if c == nil { - http.Error(w, "codec not found", http.StatusNotFound) - return - } - - val, err := c.Decode(msg.Pair.Value) - if err != nil { - http.Error(w, fmt.Sprintf("failed to decode: %v", err), http.StatusInternalServerError) - return - } - - formatValue(w, val, format) -} - -func viewKey(w http.ResponseWriter, store map[string]valueDesc, key string, format string) { - if store[key].value == nil { - http.Error(w, "value not found", http.StatusNotFound) - return - } - - formatValue(w, store[key].value, format) -} - -func formatValue(w http.ResponseWriter, val interface{}, format string) { - w.WriteHeader(200) - w.Header().Add("content-type", "text/plain") - - switch format { - case "json", "json-pretty": - enc := json.NewEncoder(w) - if format == "json-pretty" { - enc.SetIndent("", " ") - } - - err := enc.Encode(val) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - default: - _, _ = fmt.Fprintf(w, "%#v", val) - } -} - -func downloadKey(w http.ResponseWriter, kv *KV, store map[string]valueDesc, key string) { - if store[key].value == nil { - http.Error(w, "value not found", http.StatusNotFound) - return - } - - val := store[key] - - c := kv.GetCodec(store[key].codecID) - if c == nil { - http.Error(w, "codec not found", http.StatusNotFound) - return - } - - encoded, err := c.Encode(val.value) - if err != nil { - http.Error(w, fmt.Sprintf("failed to encode: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Add("content-type", "application/octet-stream") - // Set content-length so that client knows whether it has received full response or not. - w.Header().Add("content-length", strconv.Itoa(len(encoded))) - w.Header().Add("content-disposition", fmt.Sprintf("attachment; filename=%d-%s", val.version, key)) - w.WriteHeader(200) - - // Ignore errors, we cannot do anything about them. - _, _ = w.Write(encoded) + NewHTTPStatusHandler(kvs, defaultPageTemplate).ServeHTTP(w, req) } - -type pageData struct { - Now time.Time - Memberlist *memberlist.Memberlist - SortedMembers []*memberlist.Node - Store map[string]valueDesc - SentMessages []message - ReceivedMessages []message -} - -//go:embed status.gohtml -var defaultPageContent string -var defaultPageTemplate = template.Must(template.New("webpage").Funcs(template.FuncMap{ - "StringsJoin": strings.Join, -}).Parse(defaultPageContent)) diff --git a/kv/memberlist/kv_init_service_test.go b/kv/memberlist/kv_init_service_test.go index 9285ef61a..8ad0d07fa 100644 --- a/kv/memberlist/kv_init_service_test.go +++ b/kv/memberlist/kv_init_service_test.go @@ -1,59 +1,14 @@ package memberlist import ( - "bytes" "testing" - "time" - "github.com/hashicorp/memberlist" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/grafana/dskit/flagext" ) -func TestPage(t *testing.T) { - conf := memberlist.DefaultLANConfig() - ml, err := memberlist.Create(conf) - require.NoError(t, err) - - t.Cleanup(func() { - _ = ml.Shutdown() - }) - - require.NoError(t, defaultPageTemplate.Execute(&bytes.Buffer{}, pageData{ - Now: time.Now(), - Memberlist: ml, - SortedMembers: ml.Members(), - Store: nil, - ReceivedMessages: []message{{ - ID: 10, - Time: time.Now(), - Size: 50, - Pair: KeyValuePair{ - Key: "hello", - Value: []byte("world"), - Codec: "codec", - }, - Version: 20, - Changes: []string{"A", "B", "C"}, - }}, - - SentMessages: []message{{ - ID: 10, - Time: time.Now(), - Size: 50, - Pair: KeyValuePair{ - Key: "hello", - Value: []byte("world"), - Codec: "codec", - }, - Version: 20, - Changes: []string{"A", "B", "C"}, - }}, - })) -} - func TestStop(t *testing.T) { var cfg KVConfig flagext.DefaultValues(&cfg) diff --git a/kv/memberlist/memberlist_client.go b/kv/memberlist/memberlist_client.go index 30f0992d3..23c40ac76 100644 --- a/kv/memberlist/memberlist_client.go +++ b/kv/memberlist/memberlist_client.go @@ -232,7 +232,7 @@ type KV struct { // KV Store. storeMu sync.Mutex - store map[string]valueDesc + store map[string]ValueDesc // Codec registry codecs map[string]codec.Codec @@ -245,9 +245,9 @@ type KV struct { // Buffers with sent and received messages. Used for troubleshooting only. // New messages are appended, old messages (based on configured size limit) removed from the front. messagesMu sync.Mutex - sentMessages []message + sentMessages []Message sentMessagesSize int - receivedMessages []message + receivedMessages []Message receivedMessagesSize int messageCounter int // Used to give each message in the sentMessages and receivedMessages a unique ID, for UI. @@ -285,7 +285,7 @@ type KV struct { // Message describes incoming or outgoing message, and local state after applying incoming message, or state when sending message. // Fields are exported for templating to work. -type message struct { +type Message struct { ID int // Unique local ID of the message. Time time.Time // Time when message was sent or received. Size int // Message size @@ -296,21 +296,22 @@ type message struct { Changes []string // List of changes in this message (as computed by *this* node). } -type valueDesc struct { +// ValueDesc stores the value along with it's codec and local version. +type ValueDesc struct { // We store the decoded value here to prevent decoding the entire state for every // update we receive. Whilst the updates are small and fast to decode, // the total state can be quite large. // The CAS function is passed a deep copy because it modifies in-place. value Mergeable - // version (local only) is used to keep track of what we're gossiping about, and invalidate old messages - version uint + // Version (local only) is used to keep track of what we're gossiping about, and invalidate old messages. + Version uint // ID of codec used to write this value. Only used when sending full state. - codecID string + CodecID string } -func (v valueDesc) Clone() (result valueDesc) { +func (v ValueDesc) Clone() (result ValueDesc) { result = v if v.value != nil { result.value = v.value.Clone() @@ -318,8 +319,8 @@ func (v valueDesc) Clone() (result valueDesc) { return } -func (v valueDesc) String() string { - return fmt.Sprintf("version: %d, codec: %s", v.version, v.codecID) +func (v ValueDesc) String() string { + return fmt.Sprintf("version: %d, codec: %s", v.Version, v.CodecID) } var ( @@ -343,7 +344,7 @@ func NewKV(cfg KVConfig, logger log.Logger, dnsProvider DNSProvider, registerer registerer: registerer, provider: dnsProvider, - store: make(map[string]valueDesc), + store: make(map[string]ValueDesc), codecs: make(map[string]codec.Codec), watchers: make(map[string][]chan string), prefixWatchers: make(map[string][]chan string), @@ -642,7 +643,7 @@ func (m *KV) get(key string, codec codec.Codec) (out interface{}, version uint, _, _ = v.value.RemoveTombstones(time.Time{}) } - return v.value, v.version, nil + return v.value, v.Version, nil } // WatchKey watches for value changes for given key. When value changes, 'f' function is called with the @@ -909,7 +910,7 @@ func (m *KV) broadcastNewValue(key string, change Mergeable, version uint, codec return } - m.addSentMessage(message{ + m.addSentMessage(Message{ Time: time.Now(), Size: len(pairData), Pair: kvPair, @@ -964,7 +965,7 @@ func (m *KV) NotifyMsg(msg []byte) { changes = mod.MergeContent() } - m.addReceivedMessage(message{ + m.addReceivedMessage(Message{ Time: time.Now(), Size: len(msg), Pair: kvPair, @@ -1033,9 +1034,9 @@ func (m *KV) LocalState(join bool) []byte { continue } - codec := m.GetCodec(val.codecID) + codec := m.GetCodec(val.CodecID) if codec == nil { - level.Error(m.logger).Log("msg", "failed to encode remote state: unknown codec for key", "codec", val.codecID, "key", key) + level.Error(m.logger).Log("msg", "failed to encode remote state: unknown codec for key", "codec", val.CodecID, "key", key) continue } @@ -1048,7 +1049,7 @@ func (m *KV) LocalState(join bool) []byte { kvPair.Reset() kvPair.Key = key kvPair.Value = encoded - kvPair.Codec = val.codecID + kvPair.Codec = val.CodecID ser, err := kvPair.Marshal() if err != nil { @@ -1068,11 +1069,11 @@ func (m *KV) LocalState(join bool) []byte { } buf.Write(ser) - m.addSentMessage(message{ + m.addSentMessage(Message{ Time: sent, Size: len(ser), Pair: kvPair, // Makes a copy of kvPair. - Version: val.version, + Version: val.Version, }) } @@ -1136,7 +1137,7 @@ func (m *KV) MergeRemoteState(data []byte, join bool) { changes = change.MergeContent() } - m.addReceivedMessage(message{ + m.addReceivedMessage(Message{ Time: received, Size: int(kvPairLength), Pair: kvPair, // Makes a copy of kvPair. @@ -1184,7 +1185,7 @@ func (m *KV) mergeValueForKey(key string, incomingValue Mergeable, casVersion ui // the full state anywhere as is done elsewhere (i.e. Get/WatchKey/CAS). curr := m.store[key] // if casVersion is 0, then there was no previous value, so we will just do normal merge, without localCAS flag set. - if casVersion > 0 && curr.version != casVersion { + if casVersion > 0 && curr.Version != casVersion { return nil, 0, errVersionMismatch } result, change, err := computeNewValue(incomingValue, curr.value, casVersion > 0) @@ -1215,11 +1216,11 @@ func (m *KV) mergeValueForKey(key string, incomingValue Mergeable, casVersion ui } } - newVersion := curr.version + 1 - m.store[key] = valueDesc{ + newVersion := curr.Version + 1 + m.store[key] = ValueDesc{ value: result, - version: newVersion, - codecID: codec.CodecID(), + Version: newVersion, + CodecID: codec.CodecID(), } // The "changes" returned by Merge() can contain references to the "result" @@ -1240,17 +1241,17 @@ func computeNewValue(incoming Mergeable, oldVal Mergeable, cas bool) (Mergeable, return oldVal, change, err } -func (m *KV) storeCopy() map[string]valueDesc { +func (m *KV) storeCopy() map[string]ValueDesc { m.storeMu.Lock() defer m.storeMu.Unlock() - result := make(map[string]valueDesc, len(m.store)) + result := make(map[string]ValueDesc, len(m.store)) for k, v := range m.store { result[k] = v.Clone() } return result } -func (m *KV) addReceivedMessage(msg message) { +func (m *KV) addReceivedMessage(msg Message) { if m.cfg.MessageHistoryBufferBytes == 0 { return } @@ -1264,7 +1265,7 @@ func (m *KV) addReceivedMessage(msg message) { m.receivedMessages, m.receivedMessagesSize = addMessageToBuffer(m.receivedMessages, m.receivedMessagesSize, m.cfg.MessageHistoryBufferBytes, msg) } -func (m *KV) addSentMessage(msg message) { +func (m *KV) addSentMessage(msg Message) { if m.cfg.MessageHistoryBufferBytes == 0 { return } @@ -1278,12 +1279,12 @@ func (m *KV) addSentMessage(msg message) { m.sentMessages, m.sentMessagesSize = addMessageToBuffer(m.sentMessages, m.sentMessagesSize, m.cfg.MessageHistoryBufferBytes, msg) } -func (m *KV) getSentAndReceivedMessages() (sent, received []message) { +func (m *KV) getSentAndReceivedMessages() (sent, received []Message) { m.messagesMu.Lock() defer m.messagesMu.Unlock() // Make copy of both slices. - return append([]message(nil), m.sentMessages...), append([]message(nil), m.receivedMessages...) + return append([]Message(nil), m.sentMessages...), append([]Message(nil), m.receivedMessages...) } func (m *KV) deleteSentReceivedMessages() { @@ -1296,7 +1297,7 @@ func (m *KV) deleteSentReceivedMessages() { m.receivedMessagesSize = 0 } -func addMessageToBuffer(msgs []message, size int, limit int, msg message) ([]message, int) { +func addMessageToBuffer(msgs []Message, size int, limit int, msg Message) ([]Message, int) { msgs = append(msgs, msg) size += msg.Size diff --git a/kv/memberlist/memberlist_client_test.go b/kv/memberlist/memberlist_client_test.go index a8c6ab590..28fe80b36 100644 --- a/kv/memberlist/memberlist_client_test.go +++ b/kv/memberlist/memberlist_client_test.go @@ -1071,18 +1071,18 @@ func TestRejoin(t *testing.T) { } func TestMessageBuffer(t *testing.T) { - buf := []message(nil) + buf := []Message(nil) size := 0 - buf, size = addMessageToBuffer(buf, size, 100, message{Size: 50}) + buf, size = addMessageToBuffer(buf, size, 100, Message{Size: 50}) assert.Len(t, buf, 1) assert.Equal(t, size, 50) - buf, size = addMessageToBuffer(buf, size, 100, message{Size: 50}) + buf, size = addMessageToBuffer(buf, size, 100, Message{Size: 50}) assert.Len(t, buf, 2) assert.Equal(t, size, 100) - buf, size = addMessageToBuffer(buf, size, 100, message{Size: 25}) + buf, size = addMessageToBuffer(buf, size, 100, Message{Size: 25}) assert.Len(t, buf, 2) assert.Equal(t, size, 75) } diff --git a/kv/memberlist/status.gohtml b/kv/memberlist/status.gohtml index 3ab6d0936..d5c91aed6 100644 --- a/kv/memberlist/status.gohtml +++ b/kv/memberlist/status.gohtml @@ -1,4 +1,4 @@ -{{- /*gotype: github.com/grafana/dskit/kv/memberlist.statusPageData */ -}} +{{- /*gotype: github.com/grafana/dskit/kv/memberlist.StatusPageData */ -}} @@ -20,7 +20,8 @@ Key - Value Details + Codec + Version Actions @@ -29,7 +30,8 @@ {{ range $k, $v := .Store }} {{ $k }} - {{ $v }} + {{ $v.CodecID }} + {{ $v.Version }} json | json-pretty