github.com/grafana/pyroscope@v1.18.0/pkg/metastore/admin/admin.go (about) 1 package admin 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "net/http" 8 "slices" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/go-kit/log" 14 "github.com/go-kit/log/level" 15 "github.com/grafana/dskit/services" 16 "google.golang.org/grpc" 17 "google.golang.org/grpc/credentials/insecure" 18 19 metastoreclient "github.com/grafana/pyroscope/pkg/metastore/client" 20 "github.com/grafana/pyroscope/pkg/metastore/discovery" 21 "github.com/grafana/pyroscope/pkg/metastore/raftnode/raftnodepb" 22 httputil "github.com/grafana/pyroscope/pkg/util/http" 23 ) 24 25 type configChangeRequest struct { 26 serverId string 27 currentTerm uint64 28 } 29 30 type formActionHandler func(http.ResponseWriter, *http.Request, configChangeRequest) error 31 32 type Admin struct { 33 service services.Service 34 35 logger log.Logger 36 37 servers []discovery.Server 38 leaderClient raftnodepb.RaftNodeServiceClient // used to make operational calls (e.g., removing nodes) 39 40 metastoreClient *metastoreclient.Client // used to test the metastoreclient.Client implementation 41 42 actionHandlers map[string]formActionHandler 43 } 44 45 func (a *Admin) Service() services.Service { 46 return a.service 47 } 48 49 type raftNodeServiceClient struct { 50 raftnodepb.RaftNodeServiceClient 51 conn *grpc.ClientConn 52 } 53 54 func New( 55 client raftnodepb.RaftNodeServiceClient, 56 logger log.Logger, 57 metastoreAddress string, 58 metastoreClient *metastoreclient.Client, 59 ) (*Admin, error) { 60 adm := &Admin{ 61 leaderClient: client, 62 logger: logger, 63 actionHandlers: make(map[string]formActionHandler), 64 metastoreClient: metastoreClient, 65 } 66 adm.addFormActionHandlers() 67 adm.service = services.NewIdleService(adm.starting, adm.stopping) 68 69 disc, err := discovery.NewDiscovery(logger, metastoreAddress, nil) 70 if err != nil { 71 return nil, err 72 } 73 disc.Subscribe(adm) 74 75 return adm, nil 76 } 77 78 func (a *Admin) starting(context.Context) error { return nil } 79 func (a *Admin) stopping(error) error { return nil } 80 81 func (a *Admin) Servers(servers []discovery.Server) { 82 a.servers = servers 83 slices.SortFunc(a.servers, func(a, b discovery.Server) int { 84 return strings.Compare(string(a.Raft.ID), string(b.Raft.ID)) 85 }) 86 } 87 88 func (a *Admin) NodeListHandler() http.Handler { 89 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 if r.Method == http.MethodPost { 91 for fieldName, handler := range a.actionHandlers { 92 field := r.FormValue(fieldName) 93 if field != "" { 94 changeRequest := configChangeRequest{ 95 serverId: field, 96 } 97 if currentTerm := r.FormValue("current-term"); currentTerm != "" { 98 parsedTerm, err := strconv.ParseUint(currentTerm, 10, 64) 99 if err == nil { 100 changeRequest.currentTerm = parsedTerm 101 } 102 } 103 if err := handler(w, r, changeRequest); err != nil { 104 httputil.Error(w, err) 105 return 106 } 107 w.Header().Set("Location", "#") 108 w.WriteHeader(http.StatusFound) 109 return 110 } 111 } 112 } 113 114 raftState := a.fetchRaftState(r.Context()) 115 err := pageTemplates.nodesTemplate.Execute(w, nodesPageContent{ 116 DiscoveredServers: a.servers, 117 Raft: raftState, 118 Now: time.Now().UTC(), 119 }) 120 if err != nil { 121 httputil.Error(w, err) 122 } 123 }) 124 } 125 126 func (a *Admin) fetchRaftState(ctx context.Context) *raftNodeState { 127 observedLeaders := make(map[string]int) 128 numRaftNodes := 0 129 nodes := make([]*metastoreNode, 0, len(a.servers)) 130 131 for _, s := range a.servers { 132 cl, err := newClient(s.ResolvedAddress) 133 if err != nil { 134 level.Warn(a.logger).Log("msg", "missing client for server", "server", s) 135 continue 136 } 137 node := &metastoreNode{ 138 DiscoveryServerId: string(s.Raft.ID), 139 ResolvedAddress: s.ResolvedAddress, 140 } 141 nodes = append(nodes, node) 142 143 res, err := cl.NodeInfo(ctx, &raftnodepb.NodeInfoRequest{}) 144 _ = cl.conn.Close() 145 146 if err != nil { 147 level.Warn(a.logger).Log("msg", "error fetching node info", "server", s, "err", err) 148 continue 149 } 150 nInfo := res.Node 151 152 node.RaftServerId = nInfo.ServerId 153 node.Member = nInfo.LeaderId != "" 154 node.State = nInfo.State 155 node.LeaderId = nInfo.LeaderId 156 node.ConfigIndex = nInfo.ConfigurationIndex 157 node.NumPeers = len(nInfo.Peers) 158 node.CurrentTerm = nInfo.CurrentTerm 159 node.LastIndex = nInfo.LastIndex 160 node.CommitIndex = nInfo.CommitIndex 161 node.AppliedIndex = nInfo.AppliedIndex 162 node.BuildVersion = nInfo.BuildVersion 163 node.BuildRevision = nInfo.BuildRevision 164 node.Stats = make(map[string]string) 165 for i, n := range nInfo.Stats.Name { 166 node.Stats[n] = nInfo.Stats.Value[i] 167 } 168 169 if node.Member { 170 numRaftNodes++ 171 observedLeaders[node.LeaderId]++ 172 } 173 } 174 175 currentTerm := findCurrentTerm(nodes) 176 177 return &raftNodeState{ 178 Nodes: nodes, 179 ObservedLeaders: observedLeaders, 180 CurrentTerm: currentTerm, 181 NumNodes: numRaftNodes, 182 } 183 } 184 185 func findCurrentTerm(nodes []*metastoreNode) uint64 { 186 terms := make(map[uint64]int) 187 for _, node := range nodes { 188 if node.Member { 189 terms[node.CurrentTerm]++ 190 } 191 } 192 // TODO aleks-p: in case of a mismatch in reported current terms, we bypass any validation 193 term := uint64(math.MaxUint64) 194 if len(terms) == 1 { 195 for k := range terms { 196 term = k 197 } 198 } 199 return term 200 } 201 202 func newClient(address string) (*raftNodeServiceClient, error) { 203 conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) 204 if err != nil { 205 return nil, err 206 } 207 return &raftNodeServiceClient{ 208 RaftNodeServiceClient: raftnodepb.NewRaftNodeServiceClient(conn), 209 conn: conn, 210 }, nil 211 } 212 213 func (a *Admin) addFormActionHandlers() { 214 a.actionHandlers["add"] = func(w http.ResponseWriter, r *http.Request, cr configChangeRequest) error { 215 _, err := a.leaderClient.AddNode(r.Context(), &raftnodepb.AddNodeRequest{ 216 ServerId: cr.serverId, 217 CurrentTerm: cr.currentTerm, 218 }) 219 return err 220 } 221 a.actionHandlers["remove"] = func(w http.ResponseWriter, r *http.Request, cr configChangeRequest) error { 222 _, err := a.leaderClient.RemoveNode(r.Context(), &raftnodepb.RemoveNodeRequest{ 223 ServerId: cr.serverId, 224 CurrentTerm: cr.currentTerm, 225 }) 226 return err 227 } 228 a.actionHandlers["promote"] = func(w http.ResponseWriter, r *http.Request, cr configChangeRequest) error { 229 _, err := a.leaderClient.PromoteToLeader(r.Context(), &raftnodepb.PromoteToLeaderRequest{ 230 ServerId: cr.serverId, 231 CurrentTerm: cr.currentTerm, 232 }) 233 return err 234 } 235 a.actionHandlers["demote"] = func(w http.ResponseWriter, r *http.Request, cr configChangeRequest) error { 236 _, err := a.leaderClient.DemoteLeader(r.Context(), &raftnodepb.DemoteLeaderRequest{ 237 ServerId: cr.serverId, 238 CurrentTerm: cr.currentTerm, 239 }) 240 return err 241 } 242 } 243 244 func (a *Admin) ClientTestHandler() http.Handler { 245 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 246 raftState := a.fetchRaftState(r.Context()) 247 content := clientTestPageContent{ 248 Raft: raftState, 249 Now: time.Now().UTC(), 250 } 251 252 if r.Method == http.MethodPost { 253 start := time.Now() 254 res, err := a.metastoreClient.ReadIndex(r.Context(), &raftnodepb.ReadIndexRequest{}) 255 content.TestResponseTime = time.Since(start) 256 if err != nil { 257 content.TestResponse = err.Error() 258 } else { 259 content.TestResponse = fmt.Sprintf("Success! (index: %d, term: %d)", res.CommitIndex, res.Term) 260 } 261 } 262 263 err := pageTemplates.clientTestTemplate.Execute(w, content) 264 if err != nil { 265 httputil.Error(w, err) 266 } 267 }) 268 }