trpc.group/trpc-go/trpc-go@v1.0.3/admin/admin.go (about)

     1  //
     2  //
     3  // Tencent is pleased to support the open source community by making tRPC available.
     4  //
     5  // Copyright (C) 2023 THL A29 Limited, a Tencent company.
     6  // All rights reserved.
     7  //
     8  // If you have downloaded a copy of the tRPC source code from Tencent,
     9  // please note that tRPC source code is licensed under the  Apache 2.0 License,
    10  // A copy of the Apache 2.0 License is included in this file.
    11  //
    12  //
    13  
    14  // Package admin provides management capabilities for trpc services,
    15  // including but not limited to health checks, logging, performance monitoring, RPCZ, etc.
    16  package admin
    17  
    18  import (
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"net"
    23  	"net/http"
    24  	"net/http/pprof"
    25  	"net/url"
    26  	"os"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  
    31  	"trpc.group/trpc-go/trpc-go/internal/reuseport"
    32  	trpcpb "trpc.group/trpc/trpc-protocol/pb/go/trpc"
    33  
    34  	"trpc.group/trpc-go/trpc-go/config"
    35  	"trpc.group/trpc-go/trpc-go/errs"
    36  	"trpc.group/trpc-go/trpc-go/healthcheck"
    37  	"trpc.group/trpc-go/trpc-go/log"
    38  	"trpc.group/trpc-go/trpc-go/rpcz"
    39  	"trpc.group/trpc-go/trpc-go/transport"
    40  )
    41  
    42  // ServiceName is the service name of admin service.
    43  const ServiceName = "admin"
    44  
    45  // Patterns.
    46  const (
    47  	patternCmds          = "/cmds"
    48  	patternVersion       = "/version"
    49  	patternLoglevel      = "/cmds/loglevel"
    50  	patternConfig        = "/cmds/config"
    51  	patternHealthCheck   = "/is_healthy/"
    52  	patternRPCZSpansList = "/cmds/rpcz/spans"
    53  	patternRPCZSpanGet   = "/cmds/rpcz/spans/"
    54  )
    55  
    56  // Pprof patterns.
    57  const (
    58  	pprofPprof   = "/debug/pprof/"
    59  	pprofCmdline = "/debug/pprof/cmdline"
    60  	pprofProfile = "/debug/pprof/profile"
    61  	pprofSymbol  = "/debug/pprof/symbol"
    62  	pprofTrace   = "/debug/pprof/trace"
    63  )
    64  
    65  // Return parameters.
    66  const (
    67  	retErrCode    = "errorcode"
    68  	retMessage    = "message"
    69  	errCodeServer = 1
    70  )
    71  
    72  // Server structure provides utilities related to administration.
    73  // It implements the server.Service interface.
    74  type Server struct {
    75  	config *configuration
    76  	server *http.Server
    77  
    78  	router      *router
    79  	healthCheck *healthcheck.HealthCheck
    80  
    81  	closeOnce sync.Once
    82  	closeErr  error
    83  }
    84  
    85  // NewServer returns a new admin Server.
    86  func NewServer(opts ...Option) *Server {
    87  	cfg := newDefaultConfig()
    88  	for _, opt := range opts {
    89  		opt(cfg)
    90  	}
    91  
    92  	s := &Server{
    93  		config:      cfg,
    94  		healthCheck: healthcheck.New(healthcheck.WithStatusWatchers(healthcheck.GetWatchers())),
    95  	}
    96  	if !cfg.skipServe {
    97  		s.router = s.configRouter(newRouter())
    98  	}
    99  	return s
   100  }
   101  
   102  func (s *Server) configRouter(r *router) *router {
   103  	r.add(patternCmds, s.handleCmds)         // Admin Command List.
   104  	r.add(patternVersion, s.handleVersion)   // Framework version.
   105  	r.add(patternLoglevel, s.handleLogLevel) // View/Set the log level of the framework.
   106  	r.add(patternConfig, s.handleConfig)     // View framework configuration files.
   107  	r.add(patternHealthCheck,
   108  		http.StripPrefix(patternHealthCheck,
   109  			http.HandlerFunc(s.handleHealthCheck),
   110  		).ServeHTTP,
   111  	) // Health check.
   112  
   113  	r.add(patternRPCZSpansList, s.handleRPCZSpansList)
   114  	r.add(patternRPCZSpanGet, s.handleRPCZSpanGet)
   115  
   116  	r.add(pprofPprof, pprof.Index)
   117  	r.add(pprofCmdline, pprof.Cmdline)
   118  	r.add(pprofProfile, pprof.Profile)
   119  	r.add(pprofSymbol, pprof.Symbol)
   120  	r.add(pprofTrace, pprof.Trace)
   121  
   122  	for pattern, handler := range pattern2Handler {
   123  		r.add(pattern, handler)
   124  	}
   125  
   126  	// Delete the router registered with http.DefaultServeMux.
   127  	// Avoid causing security problems: https://github.com/golang/go/issues/22085.
   128  	err := unregisterHandlers(
   129  		[]string{
   130  			pprofPprof,
   131  			pprofCmdline,
   132  			pprofProfile,
   133  			pprofSymbol,
   134  			pprofTrace,
   135  		},
   136  	)
   137  	if err != nil {
   138  		log.Errorf("failed to unregister pprof handlers from http.DefaultServeMux, err: %+v", err)
   139  	}
   140  	return r
   141  }
   142  
   143  // Register implements server.Service.
   144  func (s *Server) Register(serviceDesc interface{}, serviceImpl interface{}) error {
   145  	// The admin service does not need to do anything in this registration function.
   146  	return nil
   147  }
   148  
   149  // RegisterHealthCheck registers a new service and returns two functions, one for unregistering the service and one for
   150  // updating the status of the service.
   151  func (s *Server) RegisterHealthCheck(
   152  	serviceName string,
   153  ) (unregister func(), update func(healthcheck.Status), err error) {
   154  	update, err = s.healthCheck.Register(serviceName)
   155  	return func() {
   156  		s.healthCheck.Unregister(serviceName)
   157  	}, update, err
   158  }
   159  
   160  // Serve starts the admin HTTP server.
   161  func (s *Server) Serve() error {
   162  	cfg := s.config
   163  	if cfg.skipServe {
   164  		return nil
   165  	}
   166  	if cfg.enableTLS {
   167  		return errors.New("admin service does not support tls")
   168  	}
   169  
   170  	const network = "tcp"
   171  	ln, err := s.listen(network, cfg.addr)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	s.server = &http.Server{
   177  		Addr:         ln.Addr().String(),
   178  		ReadTimeout:  cfg.readTimeout,
   179  		WriteTimeout: cfg.writeTimeout,
   180  		Handler:      s.router,
   181  	}
   182  	if err := s.server.Serve(ln); err != nil && err != http.ErrServerClosed {
   183  		return err
   184  	}
   185  	return nil
   186  }
   187  
   188  // Close shuts down server.
   189  func (s *Server) Close(ch chan struct{}) error {
   190  	pid := os.Getpid()
   191  	s.closeOnce.Do(s.close)
   192  	log.Infof("process:%d, admin server, closed", pid)
   193  	if ch != nil {
   194  		ch <- struct{}{}
   195  	}
   196  	return s.closeErr
   197  }
   198  
   199  // WatchStatus HealthCheck proxy, registers health status watcher for service.
   200  func (s *Server) WatchStatus(serviceName string, onStatusChanged func(healthcheck.Status)) {
   201  	s.healthCheck.Watch(serviceName, onStatusChanged)
   202  }
   203  
   204  // HandleFunc registers the handler function for the given pattern.
   205  func (s *Server) HandleFunc(pattern string, handler http.HandlerFunc) {
   206  	_ = s.router.add(pattern, handler)
   207  }
   208  
   209  func (s *Server) listen(network, addr string) (net.Listener, error) {
   210  	ln, err := s.obtainListener(network, addr)
   211  	if err != nil {
   212  		return nil, fmt.Errorf("get admin listener error: %w", err)
   213  	}
   214  	if ln == nil {
   215  		ln, err = reuseport.Listen(network, addr)
   216  		if err != nil {
   217  			return nil, fmt.Errorf("admin reuseport listen error: %w", err)
   218  		}
   219  	}
   220  	if err := transport.SaveListener(ln); err != nil {
   221  		return nil, fmt.Errorf("save admin listener error: %w", err)
   222  	}
   223  	return ln, nil
   224  }
   225  
   226  func (s *Server) obtainListener(network, addr string) (net.Listener, error) {
   227  	ok, _ := strconv.ParseBool(os.Getenv(transport.EnvGraceRestart)) // Ignore error caused by messy values.
   228  	if !ok {
   229  		return nil, nil
   230  	}
   231  	pln, err := transport.GetPassedListener(network, addr)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	ln, ok := pln.(net.Listener)
   236  	if !ok {
   237  		return nil, fmt.Errorf("the passed listener %T is not of type net.Listener", pln)
   238  	}
   239  	return ln, nil
   240  }
   241  
   242  func (s *Server) close() {
   243  	if s.server == nil {
   244  		return
   245  	}
   246  	s.closeErr = s.server.Close()
   247  }
   248  
   249  var pattern2Handler = make(map[string]http.HandlerFunc)
   250  
   251  // HandleFunc registers the handler function for the given pattern.
   252  // Each time NewServer is called, all handlers registered through HandleFunc will be in effect.
   253  // Therefore, please prioritize using Server.HandleFunc.
   254  func HandleFunc(pattern string, handler http.HandlerFunc) {
   255  	pattern2Handler[pattern] = handler
   256  }
   257  
   258  // ErrorOutput normalizes the error output.
   259  func ErrorOutput(w http.ResponseWriter, error string, code int) {
   260  	ret := newDefaultRes()
   261  	ret[retErrCode] = code
   262  	ret[retMessage] = error
   263  	_ = json.NewEncoder(w).Encode(ret)
   264  }
   265  
   266  // handleCmds gives a list of all currently available administrative commands.
   267  func (s *Server) handleCmds(w http.ResponseWriter, r *http.Request) {
   268  	setCommonHeaders(w)
   269  
   270  	list := s.router.list()
   271  	cmds := make([]string, 0, len(list))
   272  	for _, item := range list {
   273  		cmds = append(cmds, item.pattern)
   274  	}
   275  	ret := newDefaultRes()
   276  	ret["cmds"] = cmds
   277  	_ = json.NewEncoder(w).Encode(ret)
   278  }
   279  
   280  // newDefaultRes returns admin Default output format.
   281  func newDefaultRes() map[string]interface{} {
   282  	return map[string]interface{}{
   283  		retErrCode: 0,
   284  		retMessage: "",
   285  	}
   286  }
   287  
   288  // handleVersion gives the current version number.
   289  func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
   290  	setCommonHeaders(w)
   291  
   292  	ret := newDefaultRes()
   293  	ret["version"] = s.config.version
   294  	_ = json.NewEncoder(w).Encode(ret)
   295  }
   296  
   297  // getLevel returns the level of logger's output stream.
   298  func getLevel(logger log.Logger, output string) string {
   299  	return log.LevelStrings[logger.GetLevel(output)]
   300  }
   301  
   302  // handleLogLevel returns the output level of the current logger.
   303  func (s *Server) handleLogLevel(w http.ResponseWriter, r *http.Request) {
   304  	setCommonHeaders(w)
   305  
   306  	if err := r.ParseForm(); err != nil {
   307  		ErrorOutput(w, err.Error(), errCodeServer)
   308  		return
   309  	}
   310  
   311  	name := r.Form.Get("logger")
   312  	if name == "" {
   313  		name = "default"
   314  	}
   315  	output := r.Form.Get("output")
   316  	if output == "" {
   317  		output = "0" // If no output is given in the request parameters, the first output is used.
   318  	}
   319  
   320  	logger := log.Get(name)
   321  	if logger == nil {
   322  		ErrorOutput(w, fmt.Sprintf("logger %s not found", name), errCodeServer)
   323  		return
   324  	}
   325  
   326  	ret := newDefaultRes()
   327  	if r.Method == http.MethodGet {
   328  		ret["level"] = getLevel(logger, output)
   329  		_ = json.NewEncoder(w).Encode(ret)
   330  	} else if r.Method == http.MethodPut {
   331  		ret["prelevel"] = getLevel(logger, output)
   332  		level := r.PostForm.Get("value")
   333  		logger.SetLevel(output, log.LevelNames[level])
   334  		ret["level"] = getLevel(logger, output)
   335  		_ = json.NewEncoder(w).Encode(ret)
   336  	}
   337  }
   338  
   339  // handleConfig outputs the content of the current configuration file.
   340  func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
   341  	w.Header().Set("X-Content-Type-Options", "nosniff")
   342  	w.Header().Set("Content-Type", "application/json; charset=utf-8")
   343  
   344  	buf, err := os.ReadFile(s.config.configPath)
   345  	if err != nil {
   346  		ErrorOutput(w, err.Error(), errCodeServer)
   347  		return
   348  	}
   349  
   350  	unmarshaler := config.GetUnmarshaler("yaml")
   351  	if unmarshaler == nil {
   352  		ErrorOutput(w, "cannot find yaml unmarshaler", errCodeServer)
   353  		return
   354  	}
   355  
   356  	conf := make(map[string]interface{})
   357  	if err = unmarshaler.Unmarshal(buf, &conf); err != nil {
   358  		ErrorOutput(w, err.Error(), errCodeServer)
   359  		return
   360  	}
   361  	ret := newDefaultRes()
   362  	ret["content"] = conf
   363  	_ = json.NewEncoder(w).Encode(ret)
   364  }
   365  
   366  // handleHealthCheck handles health check requests.
   367  func (s *Server) handleHealthCheck(w http.ResponseWriter, r *http.Request) {
   368  	check := s.healthCheck.CheckServer
   369  	if service := r.URL.Path; service != "" {
   370  		check = func() healthcheck.Status {
   371  			return s.healthCheck.CheckService(service)
   372  		}
   373  	}
   374  	switch check() {
   375  	case healthcheck.Serving:
   376  		w.WriteHeader(http.StatusOK)
   377  	case healthcheck.NotServing:
   378  		w.WriteHeader(http.StatusServiceUnavailable)
   379  	default:
   380  		w.WriteHeader(http.StatusNotFound)
   381  	}
   382  }
   383  
   384  // handleRPCZSpansList returns #xxx span from r by url "http://ip:port/cmds/rpcz/spans?num=xxx".
   385  func (s *Server) handleRPCZSpansList(w http.ResponseWriter, r *http.Request) {
   386  	num, err := parseNumParameter(r.URL)
   387  	if err != nil {
   388  		newResponse("", err).print(w)
   389  		return
   390  	}
   391  	var content string
   392  	for i, span := range rpcz.GlobalRPCZ.BatchQuery(num) {
   393  		content += fmt.Sprintf("%d:\n", i+1)
   394  		content += span.PrintSketch("  ")
   395  	}
   396  	newResponse(content, nil).print(w)
   397  }
   398  
   399  // handleRPCZSpanGet returns span with id from r by url "http://ip:port/cmds/rpcz/span/{id}".
   400  func (s *Server) handleRPCZSpanGet(w http.ResponseWriter, r *http.Request) {
   401  	id, err := parseIDParameter(r.URL)
   402  	if err != nil {
   403  		newResponse("", err).print(w)
   404  		return
   405  	}
   406  
   407  	span, ok := rpcz.GlobalRPCZ.Query(rpcz.SpanID(id))
   408  	if !ok {
   409  		newResponse("", errs.New(errCodeServer, fmt.Sprintf("cannot find span-id: %d", id))).print(w)
   410  		return
   411  	}
   412  	newResponse(span.PrintDetail(""), nil).print(w)
   413  }
   414  
   415  func parseIDParameter(url *url.URL) (id int64, err error) {
   416  	id, err = strconv.ParseInt(strings.TrimPrefix(url.Path, patternRPCZSpanGet), 10, 64)
   417  	if err != nil {
   418  		return id, fmt.Errorf("undefined command, please follow http://ip:port/cmds/rpcz/span/{id}), %w", err)
   419  	}
   420  	if id < 0 {
   421  		return id, fmt.Errorf("span_id: %d can not be negative", id)
   422  	}
   423  	return id, err
   424  }
   425  
   426  func parseNumParameter(url *url.URL) (int, error) {
   427  	queryNum := url.Query().Get("num")
   428  	if queryNum == "" {
   429  		const defaultNum = 10
   430  		return defaultNum, nil
   431  	}
   432  
   433  	num, err := strconv.Atoi(queryNum)
   434  	if err != nil {
   435  		return num, fmt.Errorf("http://ip:port/cmds/rpcz?num=xxx, xxx must be a integer, %w", err)
   436  	}
   437  	if num < 0 {
   438  		return num, errors.New("http://ip:port/cmds/rpcz?num=xxx, xxx must be a non-negative integer")
   439  	}
   440  	return num, nil
   441  }
   442  
   443  type response struct {
   444  	content string
   445  	err     error
   446  }
   447  
   448  func newResponse(content string, err error) response {
   449  	return response{
   450  		content: content,
   451  		err:     err,
   452  	}
   453  }
   454  func (r response) print(w http.ResponseWriter) {
   455  	w.Header().Set("X-Content-Type-Options", "nosniff")
   456  	if r.err != nil {
   457  		e := struct {
   458  			ErrCode    trpcpb.TrpcRetCode `json:"err-code"`
   459  			ErrMessage string             `json:"err-message"`
   460  		}{
   461  			ErrCode:    errs.Code(r.err),
   462  			ErrMessage: errs.Msg(r.err),
   463  		}
   464  		w.Header().Set("Content-Type", "application/json; charset=utf-8")
   465  		if err := json.NewEncoder(w).Encode(e); err != nil {
   466  			log.Trace("json.Encode failed when write to http.ResponseWriter")
   467  		}
   468  		return
   469  	}
   470  
   471  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   472  	if _, err := w.Write([]byte(r.content)); err != nil {
   473  		log.Trace("http.ResponseWriter write error")
   474  	}
   475  }
   476  
   477  func setCommonHeaders(w http.ResponseWriter) {
   478  	w.Header().Set("X-Content-Type-Options", "nosniff")
   479  	w.Header().Set("Content-Type", "application/json; charset=utf-8")
   480  }