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 }