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  }