Skip to content

Commit

Permalink
chore: Add option to hide tokens from ring statuspage (#633)
Browse files Browse the repository at this point in the history
* Add option to hide tokens from status page

* drive-by: use a per-case recorder

* changelog

* linter

* Rename to HideTokensInStatusPage
  • Loading branch information
alexweav authored Jan 6, 2025
1 parent b54518d commit 3702098
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 26 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
* [FEATURE] Add methods `Increment`, `FlushAll`, `CompareAndSwap`, `Touch` to `cache.MemcachedClient` #477
* [FEATURE] Add `concurrency.ForEachJobMergeResults()` utility function. #486
* [FEATURE] Add `ring.DoMultiUntilQuorumWithoutSuccessfulContextCancellation()`. #495
* [ENHANCEMENT] Add option to hide token information in ring status page #633
* [ENHANCEMENT] Display token information in partition ring status page #631
* [ENHANCEMENT] Add ability to log all source hosts from http header instead of only the first one. #444
* [ENHANCEMENT] Add configuration to customize backoff for the gRPC clients.
Expand Down
4 changes: 3 additions & 1 deletion ring/basic_lifecycler.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type BasicLifecyclerConfig struct {
HeartbeatTimeout time.Duration
TokensObservePeriod time.Duration
NumTokens int
// HideTokensInStatusPage allows tokens to be hidden from management tools e.g. the status page, for use in contexts which do not utilize tokens.
HideTokensInStatusPage bool

// If true lifecycler doesn't unregister instance from the ring when it's stopping. Default value is false,
// which means unregistering.
Expand Down Expand Up @@ -546,5 +548,5 @@ func (l *BasicLifecycler) getRing(ctx context.Context) (*Desc, error) {
}

func (l *BasicLifecycler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
newRingPageHandler(l, l.cfg.HeartbeatTimeout).handle(w, req)
newRingPageHandler(l, l.cfg.HeartbeatTimeout, l.cfg.HideTokensInStatusPage).handle(w, req)
}
4 changes: 3 additions & 1 deletion ring/lifecycler.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type LifecyclerConfig struct {

// Injected internally
ListenPort int `yaml:"-"`
// HideTokensInStatusPage allows tokens to be hidden from management tools e.g. the status page, for use in contexts which do not utilize tokens.
HideTokensInStatusPage bool `yaml:"-"`

// If set, specifies the TokenGenerator implementation that will be used for generating tokens.
// Default value is nil, which means that RandomTokenGenerator is used.
Expand Down Expand Up @@ -1088,7 +1090,7 @@ func (i *Lifecycler) getRing(ctx context.Context) (*Desc, error) {
}

func (i *Lifecycler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
newRingPageHandler(i, i.cfg.HeartbeatTimeout).handle(w, req)
newRingPageHandler(i, i.cfg.HeartbeatTimeout, i.cfg.HideTokensInStatusPage).handle(w, req)
}

// unregister removes our entry from consul.
Expand Down
5 changes: 3 additions & 2 deletions ring/partition_ring_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,8 @@ func TestPartitionRingPageHandler_ViewPage(t *testing.T) {
nil,
)

recorder := httptest.NewRecorder()

t.Run("displays expected partition info", func(t *testing.T) {
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/partition-ring", nil))

assert.Equal(t, http.StatusOK, recorder.Code)
Expand Down Expand Up @@ -97,6 +96,7 @@ func TestPartitionRingPageHandler_ViewPage(t *testing.T) {
})

t.Run("displays Show Tokens button by default", func(t *testing.T) {
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/partition-ring", nil))

assert.Equal(t, http.StatusOK, recorder.Code)
Expand All @@ -108,6 +108,7 @@ func TestPartitionRingPageHandler_ViewPage(t *testing.T) {
})

t.Run("displays tokens when Show Tokens is enabled", func(t *testing.T) {
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/partition-ring?tokens=true", nil))

assert.Equal(t, http.StatusOK, recorder.Code)
Expand Down
5 changes: 4 additions & 1 deletion ring/ring.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ 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:"-"`
// HideTokensInStatusPage allows tokens to be hidden from management tools e.g. the status page, for use in contexts which do not utilize tokens.
// This option is set internally and never exposed to the user.
HideTokensInStatusPage bool `yaml:"-"`
}

// RegisterFlags adds the flags required to config this to the given FlagSet with a specified prefix
Expand Down Expand Up @@ -1223,7 +1226,7 @@ func (r *Ring) getRing(_ 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.HideTokensInStatusPage).handle(w, req)
}

// InstancesCount returns the number of instances in the ring.
Expand Down
20 changes: 13 additions & 7 deletions ring/ring_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ var defaultPageTemplate = template.Must(template.New("webpage").Funcs(template.F
}).Parse(defaultPageContent))

type httpResponse struct {
Ingesters []ingesterDesc `json:"shards"`
Now time.Time `json:"now"`
ShowTokens bool `json:"-"`
Ingesters []ingesterDesc `json:"shards"`
Now time.Time `json:"now"`
// ShowTokens indicates whether the Show Tokens button is clicked.
ShowTokens bool `json:"-"`
// DisableTokens hides the concept of tokens entirely in the page, across all elements.
DisableTokens bool `json:"-"`
}

type ingesterDesc struct {
Expand All @@ -57,12 +60,14 @@ type ringAccess interface {
type ringPageHandler struct {
r ringAccess
heartbeatTimeout time.Duration
disableTokens bool
}

func newRingPageHandler(r ringAccess, heartbeatTimeout time.Duration) *ringPageHandler {
func newRingPageHandler(r ringAccess, heartbeatTimeout time.Duration, disableTokens bool) *ringPageHandler {
return &ringPageHandler{
r: r,
heartbeatTimeout: heartbeatTimeout,
disableTokens: disableTokens,
}
}

Expand Down Expand Up @@ -132,9 +137,10 @@ func (h *ringPageHandler) handle(w http.ResponseWriter, req *http.Request) {
tokensParam := req.URL.Query().Get("tokens")

renderHTTPResponse(w, httpResponse{
Ingesters: ingesters,
Now: now,
ShowTokens: tokensParam == "true",
Ingesters: ingesters,
Now: now,
ShowTokens: tokensParam == "true",
DisableTokens: h.disableTokens,
}, defaultPageTemplate, req)
}

Expand Down
154 changes: 154 additions & 0 deletions ring/ring_http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package ring

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestRingPageHandler_handle(t *testing.T) {
now := time.Now()
ring := fakeRingAccess{
desc: &Desc{
Ingesters: map[string]InstanceDesc{
"1": {
Zone: "zone-a",
State: ACTIVE,
Addr: "addr-a",
Timestamp: now.Unix(),
Tokens: []uint32{1000000, 3000000, 6000000},
},
"2": {
Zone: "zone-b",
State: ACTIVE,
Addr: "addr-b",
Timestamp: now.Unix(),
Tokens: []uint32{2000000, 4000000, 5000000, 7000000},
},
},
},
}
handler := newRingPageHandler(&ring, 10*time.Second, false)

t.Run("displays instance info", func(t *testing.T) {
recorder := httptest.NewRecorder()
handler.handle(recorder, httptest.NewRequest(http.MethodGet, "/ring", nil))

assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "text/html", recorder.Header().Get("Content-Type"))

assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<td>", "1", "</td>",
"<td>", "zone-a", "</td>",
"<td>", "ACTIVE", "</td>",
"<td>", "addr-a", "</td>",
}, `\s*`))), recorder.Body.String())

assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<td>", "3", "</td>",
"<td>", "100%", "</td>",
}, `\s*`))), recorder.Body.String())

assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<td>", "2", "</td>",
"<td>", "zone-b", "</td>",
"<td>", "ACTIVE", "</td>",
"<td>", "addr-b", "</td>",
}, `\s*`))), recorder.Body.String())

assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<td>", "4", "</td>",
"<td>", "100%", "</td>",
}, `\s*`))), recorder.Body.String())
})

t.Run("displays Show Tokens button by default", func(t *testing.T) {
recorder := httptest.NewRecorder()
handler.handle(recorder, httptest.NewRequest(http.MethodGet, "/ring", nil))

assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "text/html", recorder.Header().Get("Content-Type"))

assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
`<input type="button" value="Show Tokens" onclick="window.location.href = '\?tokens=true'"/>`,
}, `\s*`))), recorder.Body.String())
})

t.Run("displays tokens when Show Tokens is enabled", func(t *testing.T) {
recorder := httptest.NewRecorder()
handler.handle(recorder, httptest.NewRequest(http.MethodGet, "/ring?tokens=true", nil))

assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "text/html", recorder.Header().Get("Content-Type"))

assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
`<input type="button" value="Hide Tokens" onclick="window.location.href = '\?tokens=false' "/>`,
}, `\s*`))), recorder.Body.String())

assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<h2>", "Instance: 1", "</h2>",
"<p>", "Tokens:<br/>", "1000000", "3000000", "6000000", "</p>",
}, `\s*`))), recorder.Body.String())

assert.Regexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<h2>", "Instance: 2", "</h2>",
"<p>", "Tokens:<br/>", "2000000", "4000000", "5000000", "7000000", "</p>",
}, `\s*`))), recorder.Body.String())
})

tokenDisabledHandler := newRingPageHandler(&ring, 10*time.Second, true)

t.Run("hides token columns when tokens are disabled", func(t *testing.T) {
recorder := httptest.NewRecorder()
tokenDisabledHandler.handle(recorder, httptest.NewRequest(http.MethodGet, "/ring", nil))

assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "text/html", recorder.Header().Get("Content-Type"))

assert.NotRegexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<th>", "Tokens", "</th>",
"<th>", "Ownership", "</th>",
}, `\s*`))), recorder.Body.String())

assert.NotRegexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<td>", "3", "</td>",
"<td>", "100%", "</td>",
}, `\s*`))), recorder.Body.String())

assert.NotRegexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
"<td>", "4", "</td>",
"<td>", "100%", "</td>",
}, `\s*`))), recorder.Body.String())
})

t.Run("hides Show Tokens button when tokens are disabled", func(t *testing.T) {
recorder := httptest.NewRecorder()
tokenDisabledHandler.handle(recorder, httptest.NewRequest(http.MethodGet, "/ring", nil))

assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "text/html", recorder.Header().Get("Content-Type"))

assert.NotRegexp(t, regexp.MustCompile(fmt.Sprintf("(?m)%s", strings.Join([]string{
`input type="button" value="Show Tokens"`,
}, `\s*`))), recorder.Body.String())
})
}

type fakeRingAccess struct {
desc *Desc
}

func (f *fakeRingAccess) getRing(context.Context) (*Desc, error) {
return f.desc, nil
}

func (f *fakeRingAccess) casRing(_ context.Context, _ func(in interface{}) (out interface{}, retry bool, err error)) error {
return nil
}
35 changes: 21 additions & 14 deletions ring/ring_status.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
<th>Read-Only</th>
<th>Read-Only Updated</th>
<th>Last Heartbeat</th>
{{ if not .DisableTokens }}
<th>Tokens</th>
<th>Ownership</th>
{{ end }}
<th>Actions</th>
</tr>
</thead>
Expand All @@ -46,8 +48,10 @@
<td>{{ .ReadOnlyUpdatedTimestamp | timeOrEmptyString }}</td>
{{ end }}
<td>{{ .HeartbeatTimestamp | durationSince }} ago ({{ .HeartbeatTimestamp.Format "15:04:05.999" }})</td>
{{ if not $.DisableTokens }}
<td>{{ .NumTokens }}</td>
<td>{{ .Ownership | humanFloat }}%</td>
{{ end }}
<td>
<button name="forget" value="{{ .ID }}" type="submit">Forget</button>
</td>
Expand All @@ -56,21 +60,24 @@
</tbody>
</table>
<br>
{{ if .ShowTokens }}
<input type="button" value="Hide Tokens" onclick="window.location.href = '?tokens=false' "/>
{{ else }}
<input type="button" value="Show Tokens" onclick="window.location.href = '?tokens=true'"/>
{{ end }}

{{ if .ShowTokens }}
{{ range $i, $ing := .Ingesters }}
<h2>Instance: {{ .ID }}</h2>
<p>
Tokens:<br/>
{{ range $token := .Tokens }}
{{ $token }}
{{ end }}
</p>
{{ if not .DisableTokens}}
{{ if .ShowTokens }}
<input type="button" value="Hide Tokens" onclick="window.location.href = '?tokens=false' "/>
{{ else }}
<input type="button" value="Show Tokens" onclick="window.location.href = '?tokens=true'"/>
{{ end }}

{{ if .ShowTokens }}
{{ range $i, $ing := .Ingesters }}
<h2>Instance: {{ .ID }}</h2>
<p>
Tokens:<br/>
{{ range $token := .Tokens }}
{{ $token }}
{{ end }}
</p>
{{ end }}
{{ end }}
{{ end }}
</form>
Expand Down

0 comments on commit 3702098

Please sign in to comment.