mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* fix(deps): update github.com/grafana/dskit digest to 10fce42 Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> * make generate-manifest --------- Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com> Co-authored-by: A. Stoewer <[email protected]>
201 lines
6.5 KiB
Go
201 lines
6.5 KiB
Go
package ring
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"math"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
//go:embed ring_status.gohtml
|
|
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("%.3g", f)
|
|
},
|
|
"timeOrEmptyString": func(t time.Time) string {
|
|
if t.IsZero() {
|
|
return ""
|
|
}
|
|
return t.Format(time.RFC3339)
|
|
},
|
|
"durationSince": func(t time.Time) string { return time.Since(t).Truncate(time.Second).String() },
|
|
}).Parse(defaultPageContent))
|
|
|
|
type httpResponse struct {
|
|
Ingesters []ingesterDesc `json:"shards"`
|
|
Now time.Time `json:"now"`
|
|
// TokensToggledOn indicates whether the Show Tokens button is clicked.
|
|
TokensToggledOn bool `json:"-"`
|
|
Config StatusPageConfig `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"`
|
|
ReadOnly bool `json:"read_only"`
|
|
ReadOnlyUpdatedTimestamp time.Time `json:"read_only_updated_timestamp"`
|
|
Zone string `json:"zone"`
|
|
Tokens []uint32 `json:"tokens"`
|
|
NumTokens int `json:"-"`
|
|
Ownership float64 `json:"-"`
|
|
Versions map[uint64]uint64 `json:"versions,omitempty"`
|
|
}
|
|
|
|
// StatusPageConfig configures the ring status page.
|
|
//
|
|
// Given the values are only used to configure the status page, it is safe to change them between
|
|
// releases or have different values on different members of the same ring.
|
|
type StatusPageConfig struct {
|
|
// HideTokensUIElements allows tokens to be hidden from the status page, for use in contexts which do not utilize tokens.
|
|
HideTokensUIElements bool `yaml:"-" json:"-"`
|
|
|
|
// ShowVersions enables displaying versions on the status page.
|
|
ShowVersions bool `yaml:"-" json:"-"`
|
|
|
|
// ComponentNames are the names of the components in InstanceDesc.Versions, used only for display on the status page.
|
|
// If a component in Versions has no name in ComponentNames, then the version will be shown on the status page
|
|
// without a name.
|
|
ComponentNames map[uint64]string `yaml:"-" json:"-"`
|
|
}
|
|
|
|
type ringAccess interface {
|
|
casRing(ctx context.Context, f func(in interface{}) (out interface{}, retry bool, err error)) error
|
|
getRing(context.Context) (*Desc, error)
|
|
}
|
|
|
|
type ringPageHandler struct {
|
|
r ringAccess
|
|
heartbeatTimeout time.Duration
|
|
config StatusPageConfig
|
|
}
|
|
|
|
func newRingPageHandler(r ringAccess, heartbeatTimeout time.Duration, config StatusPageConfig) *ringPageHandler {
|
|
return &ringPageHandler{
|
|
r: r,
|
|
heartbeatTimeout: heartbeatTimeout,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
func (h *ringPageHandler) handle(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method == http.MethodPost {
|
|
ingesterID := req.FormValue("forget")
|
|
if err := h.forget(req.Context(), ingesterID); err != nil {
|
|
http.Error(
|
|
w,
|
|
fmt.Errorf("error forgetting instance '%s': %w", ingesterID, err).Error(),
|
|
http.StatusInternalServerError,
|
|
)
|
|
return
|
|
}
|
|
|
|
// Implement PRG pattern to prevent double-POST and work with CSRF middleware.
|
|
// https://en.wikipedia.org/wiki/Post/Redirect/Get
|
|
|
|
// http.Redirect() would convert our relative URL to absolute, which is not what we want.
|
|
// Browser knows how to do that, and it also knows real URL. Furthermore it will also preserve tokens parameter.
|
|
// Note that relative Location URLs are explicitly allowed by specification, so we're not doing anything wrong here.
|
|
w.Header().Set("Location", "#")
|
|
w.WriteHeader(http.StatusFound)
|
|
|
|
return
|
|
}
|
|
|
|
ringDesc, err := h.r.getRing(req.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
ownedTokens := ringDesc.CountTokens()
|
|
|
|
var ingesterIDs []string
|
|
for id := range ringDesc.Ingesters {
|
|
ingesterIDs = append(ingesterIDs, id)
|
|
}
|
|
sort.Strings(ingesterIDs)
|
|
|
|
now := time.Now()
|
|
var ingesters []ingesterDesc
|
|
for _, id := range ingesterIDs {
|
|
ing := ringDesc.Ingesters[id]
|
|
state := ing.State.String()
|
|
if !ing.IsHealthy(Reporting, h.heartbeatTimeout, now) {
|
|
state = "UNHEALTHY"
|
|
}
|
|
|
|
ro, rots := ing.GetReadOnlyState()
|
|
|
|
ingesters = append(ingesters, ingesterDesc{
|
|
ID: id,
|
|
State: state,
|
|
Address: ing.Addr,
|
|
HeartbeatTimestamp: time.Unix(ing.Timestamp, 0).UTC(),
|
|
RegisteredTimestamp: ing.GetRegisteredAt().UTC(),
|
|
ReadOnly: ro,
|
|
ReadOnlyUpdatedTimestamp: rots.UTC(),
|
|
Tokens: ing.Tokens,
|
|
Zone: ing.Zone,
|
|
NumTokens: len(ing.Tokens),
|
|
Ownership: (float64(ownedTokens[id]) / float64(math.MaxUint32)) * 100,
|
|
Versions: ing.Versions,
|
|
})
|
|
}
|
|
|
|
tokensParam := req.URL.Query().Get("tokens")
|
|
|
|
renderHTTPResponse(w, httpResponse{
|
|
Ingesters: ingesters,
|
|
Now: now,
|
|
TokensToggledOn: tokensParam == "true",
|
|
Config: h.config,
|
|
}, defaultPageTemplate, req)
|
|
}
|
|
|
|
// RenderHTTPResponse either responds with json or a rendered html page using the passed in template
|
|
// by checking the Accepts header
|
|
func renderHTTPResponse(w http.ResponseWriter, v any, t *template.Template, r *http.Request) {
|
|
accept := r.Header.Get("Accept")
|
|
if strings.Contains(accept, "application/json") {
|
|
writeJSONResponse(w, v)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := t.Execute(w, v); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *ringPageHandler) forget(ctx context.Context, id string) error {
|
|
unregister := func(in interface{}) (out interface{}, retry bool, err error) {
|
|
if in == nil {
|
|
return nil, false, fmt.Errorf("found empty ring when trying to unregister")
|
|
}
|
|
|
|
ringDesc := in.(*Desc)
|
|
ringDesc.RemoveIngester(id)
|
|
return ringDesc, true, nil
|
|
}
|
|
return h.r.casRing(ctx, unregister)
|
|
}
|
|
|
|
// WriteJSONResponse writes some JSON as a HTTP response.
|
|
func writeJSONResponse(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if err := json.NewEncoder(w).Encode(v); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|