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 }