istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/ctrlz/ctrlz.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package ctrlz implements Istio's introspection facility. When components
    16  // integrate with ControlZ, they automatically gain an IP port which allows operators
    17  // to visualize and control a number of aspects of each process, including controlling
    18  // logging scopes, viewing command-line options, memory use, etc. Additionally,
    19  // the port implements a REST API allowing access and control over the same state.
    20  //
    21  // ControlZ is designed around the idea of "topics". A topic corresponds to the different
    22  // parts of the UI. There are a set of built-in topics representing the core introspection
    23  // functionality, and each component that uses ControlZ can add new topics specialized
    24  // for their purpose.
    25  package ctrlz
    26  
    27  import (
    28  	"fmt"
    29  	"html/template"
    30  	"net"
    31  	"net/http"
    32  	"net/http/pprof"
    33  	"os"
    34  	"strings"
    35  	"sync"
    36  	"time"
    37  
    38  	"github.com/gorilla/mux"
    39  
    40  	"istio.io/istio/pkg/ctrlz/assets"
    41  	"istio.io/istio/pkg/ctrlz/fw"
    42  	"istio.io/istio/pkg/ctrlz/topics"
    43  	"istio.io/istio/pkg/log"
    44  )
    45  
    46  var coreTopics = []fw.Topic{
    47  	topics.ScopeTopic(),
    48  	topics.MemTopic(),
    49  	topics.EnvTopic(),
    50  	topics.ProcTopic(),
    51  	topics.ArgsTopic(),
    52  	topics.VersionTopic(),
    53  	topics.SignalsTopic(),
    54  }
    55  
    56  var (
    57  	allTopics          []fw.Topic
    58  	topicMutex         sync.Mutex
    59  	listeningTestProbe func()
    60  )
    61  
    62  // Server represents a running ControlZ instance.
    63  type Server struct {
    64  	listener   net.Listener
    65  	shutdown   sync.WaitGroup
    66  	httpServer http.Server
    67  }
    68  
    69  func augmentLayout(layout *template.Template, page string) *template.Template {
    70  	return assets.ParseTemplate(layout, page)
    71  }
    72  
    73  func registerTopic(router *mux.Router, layout *template.Template, t fw.Topic) {
    74  	htmlRouter := router.NewRoute().PathPrefix("/" + t.Prefix() + "z").Subrouter()
    75  	jsonRouter := router.NewRoute().PathPrefix("/" + t.Prefix() + "j").Subrouter()
    76  
    77  	tmpl := template.Must(template.Must(layout.Clone()).Parse("{{ define \"title\" }}" + t.Title() + "{{ end }}"))
    78  	t.Activate(fw.NewContext(htmlRouter, jsonRouter, tmpl))
    79  }
    80  
    81  // getLocalIP returns a non loopback local IP of the host
    82  func getLocalIP() string {
    83  	addrs, err := net.InterfaceAddrs()
    84  	if err != nil {
    85  		return ""
    86  	}
    87  
    88  	for _, address := range addrs {
    89  		// check the address type and if it is not a loopback then return it
    90  		if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
    91  			if ipnet.IP.To4() != nil {
    92  				return ipnet.IP.String()
    93  			}
    94  		}
    95  	}
    96  	return ""
    97  }
    98  
    99  type topic struct {
   100  	Name string
   101  	URL  string
   102  }
   103  
   104  func getTopics() []topic {
   105  	topicMutex.Lock()
   106  	defer topicMutex.Unlock()
   107  
   108  	topics := make([]topic, 0, len(allTopics))
   109  	for _, t := range allTopics {
   110  		topics = append(topics, topic{Name: t.Title(), URL: "/" + t.Prefix() + "z/"})
   111  	}
   112  
   113  	return topics
   114  }
   115  
   116  func normalize(input string) string {
   117  	return strings.Replace(input, "/", "-", -1)
   118  }
   119  
   120  // Run starts up the ControlZ listeners.
   121  //
   122  // ControlZ uses the set of standard core topics.
   123  func Run(o *Options, customTopics []fw.Topic) (*Server, error) {
   124  	topicMutex.Lock()
   125  	allTopics = append(allTopics, coreTopics...)
   126  	allTopics = append(allTopics, customTopics...)
   127  	topicMutex.Unlock()
   128  
   129  	exec, _ := os.Executable()
   130  	instance := exec + " - " + getLocalIP()
   131  
   132  	funcs := template.FuncMap{
   133  		"getTopics": getTopics,
   134  		"normalize": normalize,
   135  	}
   136  
   137  	baseLayout := assets.ParseTemplate(template.New("base"), "templates/layouts/base.html")
   138  	baseLayout = baseLayout.Funcs(funcs)
   139  	baseLayout = template.Must(baseLayout.Parse("{{ define \"instance\" }}" + instance + "{{ end }}"))
   140  	_ = augmentLayout(baseLayout, "templates/modules/header.html")
   141  	_ = augmentLayout(baseLayout, "templates/modules/sidebar.html")
   142  	_ = augmentLayout(baseLayout, "templates/modules/last-refresh.html")
   143  	mainLayout := augmentLayout(template.Must(baseLayout.Clone()), "templates/layouts/main.html")
   144  
   145  	router := mux.NewRouter()
   146  	for _, t := range allTopics {
   147  		registerTopic(router, mainLayout, t)
   148  	}
   149  
   150  	if o.EnablePprof && o.Address == "localhost" {
   151  		router.NewRoute().PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
   152  		router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
   153  		router.HandleFunc("/debug/pprof/profile", pprof.Profile)
   154  		router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
   155  		router.HandleFunc("/debug/pprof/trace", pprof.Trace)
   156  	}
   157  	registerHome(router, mainLayout)
   158  
   159  	addr := o.Address
   160  	if addr == "*" {
   161  		addr = ""
   162  	}
   163  
   164  	// Canonicalize the address and resolve a dynamic port if necessary
   165  	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addr, o.Port))
   166  	if err != nil {
   167  		log.Errorf("Unable to start ControlZ: %v", err)
   168  		return nil, err
   169  	}
   170  
   171  	s := &Server{
   172  		listener: listener,
   173  		httpServer: http.Server{
   174  			Addr:           listener.Addr().(*net.TCPAddr).String(),
   175  			ReadTimeout:    10 * time.Second,
   176  			WriteTimeout:   10 * time.Second,
   177  			MaxHeaderBytes: 1 << 20,
   178  			Handler:        router,
   179  		},
   180  	}
   181  
   182  	s.shutdown.Add(1)
   183  	go s.listen()
   184  
   185  	return s, nil
   186  }
   187  
   188  func (s *Server) listen() {
   189  	log.Infof("ControlZ available at %s", s.httpServer.Addr)
   190  	if listeningTestProbe != nil {
   191  		go listeningTestProbe()
   192  	}
   193  	err := s.httpServer.Serve(s.listener)
   194  	log.Infof("ControlZ terminated: %v", err)
   195  	s.shutdown.Done()
   196  }
   197  
   198  // Close terminates ControlZ.
   199  //
   200  // Close is not normally used by programs that expose ControlZ, it is primarily intended to be
   201  // used by tests.
   202  func (s *Server) Close() {
   203  	log.Info("Closing ControlZ")
   204  
   205  	if s.listener != nil {
   206  		if err := s.listener.Close(); err != nil {
   207  			log.Warnf("Error closing ControlZ: %v", err)
   208  		}
   209  		s.shutdown.Wait()
   210  	}
   211  }
   212  
   213  func (s *Server) Address() string {
   214  	return s.httpServer.Addr
   215  }