Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Add option to hide tokens from ring statuspage #633

Merged
merged 5 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive-by-fix: In a test of this form, each case needs its own isolated recorder. The recorder does not replace its contents when invoked multiple times, but rather appends the request bodies as the cases proceed.

These tests all still pass, but without this they have less coverage than they appear to.

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:"-"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I switched the name to DisableTokens.

I still feel HideTokens is the better name for the public APIs/config options, but within this file i felt the name HideTokens conflicts with the option ShowTokens and hurts readability.

}

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{
Comment on lines +52 to +54
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I skipped asserting a couple columns here, namely the ones containing heartbeat timestamps and such.

Asserting on these full-precision times felt like a very high vector for test intermittency - plus, it's not really the focus of this PR.

"<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
Loading