github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/introspection/worker.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package introspection 5 6 import ( 7 "fmt" 8 "net" 9 "net/http" 10 "runtime" 11 "sort" 12 "time" 13 14 "github.com/juju/errors" 15 "github.com/juju/loggo" 16 "github.com/juju/worker/v3" 17 "github.com/prometheus/client_golang/prometheus" 18 "github.com/prometheus/client_golang/prometheus/promhttp" 19 "gopkg.in/tomb.v2" 20 "gopkg.in/yaml.v2" 21 22 "github.com/juju/juju/cmd/output" 23 "github.com/juju/juju/core/machinelock" 24 "github.com/juju/juju/core/presence" 25 "github.com/juju/juju/pubsub/agent" 26 "github.com/juju/juju/worker/introspection/pprof" 27 ) 28 29 var logger = loggo.GetLogger("juju.worker.introspection") 30 31 // DepEngineReporter provides insight into the running dependency engine of the agent. 32 type DepEngineReporter interface { 33 // Report returns a map describing the state of the receiver. It is expected 34 // to be goroutine-safe. 35 Report() map[string]interface{} 36 } 37 38 // Reporter provides a simple method that the introspection 39 // worker will output for the entity. 40 type Reporter interface { 41 IntrospectionReport() string 42 } 43 44 // Clock represents the ability to wait for a bit. 45 type Clock interface { 46 Now() time.Time 47 After(time.Duration) <-chan time.Time 48 } 49 50 // SimpleHub is a pubsub hub used for internal messaging. 51 type SimpleHub interface { 52 Publish(topic string, data interface{}) func() 53 Subscribe(topic string, handler func(string, interface{})) func() 54 } 55 56 // StructuredHub is a pubsub hub used for messaging within the HA 57 // controller applications. 58 type StructuredHub interface { 59 Publish(topic string, data interface{}) (func(), error) 60 Subscribe(topic string, handler interface{}) (func(), error) 61 } 62 63 // Config describes the arguments required to create the introspection worker. 64 type Config struct { 65 SocketName string 66 DepEngine DepEngineReporter 67 StatePool Reporter 68 PubSub Reporter 69 MachineLock machinelock.Lock 70 PrometheusGatherer prometheus.Gatherer 71 Presence presence.Recorder 72 Clock Clock 73 LocalHub SimpleHub 74 CentralHub StructuredHub 75 } 76 77 // Validate checks the config values to assert they are valid to create the worker. 78 func (c *Config) Validate() error { 79 if c.SocketName == "" { 80 return errors.NotValidf("empty SocketName") 81 } 82 if c.PrometheusGatherer == nil { 83 return errors.NotValidf("nil PrometheusGatherer") 84 } 85 if c.LocalHub != nil && c.Clock == nil { 86 return errors.NotValidf("nil Clock") 87 } 88 return nil 89 } 90 91 // socketListener is a worker and constructed with NewWorker. 92 type socketListener struct { 93 tomb tomb.Tomb 94 listener *net.UnixListener 95 depEngine DepEngineReporter 96 statePool Reporter 97 pubsub Reporter 98 machineLock machinelock.Lock 99 prometheusGatherer prometheus.Gatherer 100 presence presence.Recorder 101 clock Clock 102 localHub SimpleHub 103 centralHub StructuredHub 104 done chan struct{} 105 } 106 107 // NewWorker starts an http server listening on an abstract domain socket 108 // which will be created with the specified name. 109 func NewWorker(config Config) (worker.Worker, error) { 110 if err := config.Validate(); err != nil { 111 return nil, errors.Trace(err) 112 } 113 if runtime.GOOS != "linux" { 114 return nil, errors.NotSupportedf("os %q", runtime.GOOS) 115 } 116 117 path := "@" + config.SocketName 118 addr, err := net.ResolveUnixAddr("unix", path) 119 if err != nil { 120 return nil, errors.Annotate(err, "unable to resolve unix socket") 121 } 122 123 l, err := net.ListenUnix("unix", addr) 124 if err != nil { 125 return nil, errors.Annotate(err, "unable to listen on unix socket") 126 } 127 logger.Debugf("introspection worker listening on %q", path) 128 129 w := &socketListener{ 130 listener: l, 131 depEngine: config.DepEngine, 132 statePool: config.StatePool, 133 pubsub: config.PubSub, 134 machineLock: config.MachineLock, 135 prometheusGatherer: config.PrometheusGatherer, 136 presence: config.Presence, 137 clock: config.Clock, 138 localHub: config.LocalHub, 139 centralHub: config.CentralHub, 140 done: make(chan struct{}), 141 } 142 go w.serve() 143 w.tomb.Go(w.run) 144 return w, nil 145 } 146 147 func (w *socketListener) serve() { 148 mux := http.NewServeMux() 149 w.RegisterHTTPHandlers(mux.Handle) 150 151 srv := http.Server{Handler: mux} 152 logger.Debugf("stats worker now serving") 153 defer logger.Debugf("stats worker serving finished") 154 defer close(w.done) 155 _ = srv.Serve(w.listener) 156 } 157 158 func (w *socketListener) run() error { 159 defer logger.Debugf("stats worker finished") 160 <-w.tomb.Dying() 161 logger.Debugf("stats worker closing listener") 162 w.listener.Close() 163 // Don't mark the worker as done until the serve goroutine has finished. 164 <-w.done 165 return nil 166 } 167 168 // Kill implements worker.Worker. 169 func (w *socketListener) Kill() { 170 w.tomb.Kill(nil) 171 } 172 173 // Wait implements worker.Worker. 174 func (w *socketListener) Wait() error { 175 return w.tomb.Wait() 176 } 177 178 // RegisterHTTPHandlers calls the given function with http.Handlers 179 // that serve agent introspection requests. The function will 180 // be called with a path; the function may alter the path 181 // as it sees fit. 182 func (w *socketListener) RegisterHTTPHandlers( 183 handle func(path string, h http.Handler), 184 ) { 185 handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) 186 handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) 187 handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) 188 handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) 189 handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) 190 handle("/depengine", depengineHandler{w.depEngine}) 191 handle("/metrics", promhttp.HandlerFor(w.prometheusGatherer, promhttp.HandlerOpts{})) 192 handle("/machinelock", machineLockHandler{w.machineLock}) 193 // The trailing slash is kept for metrics because we don't want to 194 // break the metrics exporting that is using the internal charm. Since 195 // we don't know if it is using the exported shell function, or calling 196 // the introspection endpoint directly. 197 handle("/metrics/", promhttp.HandlerFor(w.prometheusGatherer, promhttp.HandlerOpts{})) 198 199 // Only machine or controller agents support the following. 200 if w.statePool != nil { 201 handle("/statepool", introspectionReporterHandler{ 202 name: "State Pool Report", 203 reporter: w.statePool, 204 }) 205 } else { 206 handle("/statepool", notSupportedHandler{"State Pool"}) 207 } 208 if w.pubsub != nil { 209 handle("/pubsub", introspectionReporterHandler{ 210 name: "PubSub Report", 211 reporter: w.pubsub, 212 }) 213 } else { 214 handle("/pubsub", notSupportedHandler{"PubSub Report"}) 215 } 216 if w.presence != nil { 217 handle("/presence", presenceHandler{w.presence}) 218 } else { 219 handle("/presence", notSupportedHandler{"Presence"}) 220 } 221 if w.localHub != nil { 222 handle("/units", unitsHandler{w.clock, w.localHub, w.done}) 223 } else { 224 handle("/units", notSupportedHandler{"Units"}) 225 } 226 // TODO(leases) - add metrics 227 handle("/leases", notSupportedHandler{"Leases"}) 228 } 229 230 type notSupportedHandler struct { 231 name string 232 } 233 234 // ServeHTTP is part of the http.Handler interface. 235 func (h notSupportedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 236 http.Error(w, fmt.Sprintf("%q introspection not supported", h.name), http.StatusNotFound) 237 } 238 239 type depengineHandler struct { 240 reporter DepEngineReporter 241 } 242 243 // ServeHTTP is part of the http.Handler interface. 244 func (h depengineHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 245 if h.reporter == nil { 246 http.Error(w, "missing dependency engine reporter", http.StatusNotFound) 247 return 248 } 249 bytes, err := yaml.Marshal(h.reporter.Report()) 250 if err != nil { 251 http.Error(w, fmt.Sprintf("error: %v", err), http.StatusInternalServerError) 252 return 253 } 254 255 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 256 257 fmt.Fprint(w, "Dependency Engine Report\n\n") 258 _, _ = w.Write(bytes) 259 } 260 261 type machineLockHandler struct { 262 lock machinelock.Lock 263 } 264 265 // ServeHTTP is part of the http.Handler interface. 266 func (h machineLockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 267 if h.lock == nil { 268 http.Error(w, "missing machine lock reporter", http.StatusNotFound) 269 return 270 } 271 var args []machinelock.ReportOption 272 q := r.URL.Query() 273 if v := q.Get("yaml"); v != "" { 274 args = append(args, machinelock.ShowDetailsYAML) 275 } 276 if v := q.Get("history"); v != "" { 277 args = append(args, machinelock.ShowHistory) 278 } 279 if v := q.Get("stack"); v != "" { 280 args = append(args, machinelock.ShowStack) 281 } 282 283 content, err := h.lock.Report(args...) 284 if err != nil { 285 http.Error(w, fmt.Sprintf("error: %v", err), http.StatusInternalServerError) 286 return 287 } 288 289 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 290 fmt.Fprint(w, content) 291 } 292 293 type introspectionReporterHandler struct { 294 name string 295 reporter Reporter 296 } 297 298 // ServeHTTP is part of the http.Handler interface. 299 func (h introspectionReporterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 300 if h.reporter == nil { 301 http.Error(w, fmt.Sprintf("%s: missing reporter", h.name), http.StatusNotFound) 302 return 303 } 304 305 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 306 307 fmt.Fprintf(w, "%s:\n\n", h.name) 308 fmt.Fprint(w, h.reporter.IntrospectionReport()) 309 } 310 311 type presenceHandler struct { 312 presence presence.Recorder 313 } 314 315 // ServeHTTP is part of the http.Handler interface. 316 func (h presenceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 317 if h.presence == nil || !h.presence.IsEnabled() { 318 http.Error(w, "agent is not an apiserver", http.StatusNotFound) 319 return 320 } 321 322 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 323 324 tw := output.TabWriter(w) 325 wrapper := output.Wrapper{TabWriter: tw} 326 327 // Could be smart here and switch on the request accept header. 328 connections := h.presence.Connections() 329 models := connections.Models() 330 sort.Strings(models) 331 332 for _, name := range models { 333 wrapper.Println("[" + name + "]") 334 wrapper.Println() 335 wrapper.Println("AGENT", "SERVER", "CONN ID", "STATUS") 336 values := connections.ForModel(name).Values() 337 sort.Sort(ValueSort(values)) 338 for _, value := range values { 339 agentName := value.Agent 340 if value.ControllerAgent { 341 agentName += " (controller)" 342 } 343 wrapper.Println(agentName, value.Server, value.ConnectionID, value.Status) 344 } 345 wrapper.Println() 346 } 347 tw.Flush() 348 } 349 350 type ValueSort []presence.Value 351 352 func (a ValueSort) Len() int { return len(a) } 353 func (a ValueSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 354 func (a ValueSort) Less(i, j int) bool { 355 // Sort by agent, then server, then connection id 356 if a[i].Agent != a[j].Agent { 357 return a[i].Agent < a[j].Agent 358 } 359 if a[i].Server != a[j].Server { 360 return a[i].Server < a[j].Server 361 } 362 return a[i].ConnectionID < a[j].ConnectionID 363 } 364 365 type unitsHandler struct { 366 clock Clock 367 hub SimpleHub 368 done <-chan struct{} 369 } 370 371 // ServeHTTP is part of the http.Handler interface. 372 func (h unitsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 373 if err := r.ParseForm(); err != nil { 374 http.Error(w, err.Error(), http.StatusBadRequest) 375 return 376 } 377 378 switch action := r.Form.Get("action"); action { 379 case "": 380 http.Error(w, "missing action", http.StatusBadRequest) 381 case "start": 382 h.publishUnitsAction(w, r, "start", agent.StartUnitTopic, agent.StartUnitResponseTopic) 383 case "stop": 384 h.publishUnitsAction(w, r, "stop", agent.StopUnitTopic, agent.StopUnitResponseTopic) 385 case "status": 386 h.status(w, r) 387 default: 388 http.Error(w, fmt.Sprintf("unknown action: %q", action), http.StatusBadRequest) 389 } 390 } 391 392 func (h unitsHandler) publishUnitsAction(w http.ResponseWriter, r *http.Request, 393 action, topic, responseTopic string) { 394 if r.Method != http.MethodPost { 395 http.Error(w, fmt.Sprintf("%s requires a POST request, got %q", action, r.Method), http.StatusMethodNotAllowed) 396 return 397 } 398 399 units := r.Form["unit"] 400 if len(units) == 0 { 401 http.Error(w, "missing unit", http.StatusBadRequest) 402 return 403 } 404 405 h.publishAndAwaitResponse(w, topic, responseTopic, agent.Units{Names: units}) 406 } 407 408 func (h unitsHandler) status(w http.ResponseWriter, r *http.Request) { 409 h.publishAndAwaitResponse(w, agent.UnitStatusTopic, agent.UnitStatusResponseTopic, nil) 410 } 411 412 func (h unitsHandler) publishAndAwaitResponse(w http.ResponseWriter, topic, responseTopic string, data interface{}) { 413 response := make(chan interface{}) 414 unsubscribe := h.hub.Subscribe(responseTopic, func(topic string, body interface{}) { 415 select { 416 case response <- body: 417 case <-h.done: 418 } 419 }) 420 defer unsubscribe() 421 422 h.hub.Publish(topic, data) 423 424 select { 425 case message := <-response: 426 bytes, err := yaml.Marshal(message) 427 if err != nil { 428 http.Error(w, fmt.Sprintf("error: %v", err), http.StatusInternalServerError) 429 return 430 } 431 _, _ = w.Write(bytes) 432 case <-h.done: 433 http.Error(w, "introspection worker stopping", http.StatusServiceUnavailable) 434 case <-h.clock.After(10 * time.Second): 435 http.Error(w, "response timed out", http.StatusInternalServerError) 436 } 437 }