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  }