github.com/grafana/synthetic-monitoring-agent@v0.64.2/cmd/synthetic-monitoring-agent/http.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"net/http/pprof"
     8  	"os"
     9  	"strconv"
    10  	"strings"
    11  	"sync/atomic"
    12  	"syscall"
    13  
    14  	"github.com/felixge/httpsnoop"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"github.com/prometheus/client_golang/prometheus/promhttp"
    17  	"github.com/rs/zerolog"
    18  )
    19  
    20  type Mux struct {
    21  	logger  zerolog.Logger
    22  	router  *http.ServeMux
    23  	metrics metrics
    24  }
    25  
    26  type metrics struct {
    27  	inFlightRequests       prometheus.Gauge
    28  	requestDurationVec     *prometheus.SummaryVec
    29  	requestWrittenBytesVec *prometheus.SummaryVec
    30  }
    31  
    32  type MuxOpts struct {
    33  	Logger         zerolog.Logger
    34  	PromRegisterer interface {
    35  		prometheus.Registerer
    36  		prometheus.Gatherer
    37  	}
    38  	isReady               *readynessHandler
    39  	changeLogLevelEnabled bool
    40  	disconnectEnabled     bool
    41  	pprofEnabled          bool
    42  }
    43  
    44  func NewMux(opts MuxOpts) *Mux {
    45  	router := http.NewServeMux()
    46  
    47  	router.Handle("/", defaultHandler())
    48  
    49  	promHandler := promhttp.InstrumentMetricHandler(
    50  		opts.PromRegisterer,
    51  		promhttp.HandlerFor(opts.PromRegisterer,
    52  			promhttp.HandlerOpts{
    53  				Registry: opts.PromRegisterer,
    54  			}),
    55  	)
    56  
    57  	router.Handle("/metrics", promHandler)
    58  
    59  	isReady := &atomic.Value{}
    60  	isReady.Store(false)
    61  	router.Handle("/ready", opts.isReady)
    62  
    63  	if opts.changeLogLevelEnabled {
    64  		router.Handle("/logger", loggerHandler(opts.Logger))
    65  	}
    66  
    67  	// disconnect this agent from the API
    68  	if opts.disconnectEnabled {
    69  		router.Handle("/disconnect", http.HandlerFunc(disconnectHandler))
    70  	}
    71  
    72  	// Register pprof handlers
    73  	if opts.pprofEnabled {
    74  		router.HandleFunc("/debug/pprof/", pprof.Index)
    75  		router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
    76  		router.HandleFunc("/debug/pprof/profile", pprof.Profile)
    77  		router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
    78  		router.HandleFunc("/debug/pprof/trace", pprof.Trace)
    79  	}
    80  
    81  	requestDurationVec := prometheus.NewSummaryVec(prometheus.SummaryOpts{
    82  		Namespace: "http",
    83  		Subsystem: "requests",
    84  		Name:      "duration_seconds",
    85  		Help:      "request duration",
    86  	}, []string{
    87  		"code",
    88  		"method",
    89  	})
    90  
    91  	if err := opts.PromRegisterer.Register(requestDurationVec); err != nil {
    92  		return nil
    93  	}
    94  
    95  	requestWrittenBytesVec := prometheus.NewSummaryVec(prometheus.SummaryOpts{
    96  		Namespace: "http",
    97  		Subsystem: "requests",
    98  		Name:      "written_bytes",
    99  		Help:      "total number of bytes written by code and method",
   100  	}, []string{
   101  		"code",
   102  		"method",
   103  	})
   104  
   105  	if err := opts.PromRegisterer.Register(requestWrittenBytesVec); err != nil {
   106  		return nil
   107  	}
   108  
   109  	inFlightRequests := prometheus.NewGauge(prometheus.GaugeOpts{
   110  		Namespace: "http",
   111  		Subsystem: "requests",
   112  		Name:      "in_flight",
   113  		Help:      "number of requests in flight",
   114  	})
   115  
   116  	if err := opts.PromRegisterer.Register(inFlightRequests); err != nil {
   117  		return nil
   118  	}
   119  
   120  	return &Mux{
   121  		logger: opts.Logger,
   122  		router: router,
   123  		metrics: metrics{
   124  			inFlightRequests:       inFlightRequests,
   125  			requestDurationVec:     requestDurationVec,
   126  			requestWrittenBytesVec: requestWrittenBytesVec,
   127  		},
   128  	}
   129  }
   130  
   131  func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   132  	mux.metrics.inFlightRequests.Inc()
   133  	m := httpsnoop.CaptureMetrics(mux.router, w, r)
   134  	mux.metrics.inFlightRequests.Dec()
   135  
   136  	mux.metrics.requestDurationVec.With(prometheus.Labels{
   137  		"code":   strconv.Itoa(m.Code),
   138  		"method": r.Method,
   139  	}).Observe(m.Duration.Seconds())
   140  
   141  	mux.metrics.requestWrittenBytesVec.With(prometheus.Labels{
   142  		"code":   strconv.Itoa(m.Code),
   143  		"method": r.Method,
   144  	}).Observe(float64(m.Written))
   145  
   146  	var level zerolog.Level
   147  
   148  	switch m.Code / 100 {
   149  	case 1, 2, 3:
   150  		level = zerolog.InfoLevel
   151  	case 4:
   152  		level = zerolog.WarnLevel
   153  	case 5:
   154  		level = zerolog.ErrorLevel
   155  	default:
   156  		level = zerolog.WarnLevel
   157  	}
   158  
   159  	mux.logger.WithLevel(level).
   160  		Str("method", r.Method).
   161  		Stringer("url", r.URL).
   162  		Int("code", m.Code).
   163  		Dur("duration", m.Duration).
   164  		Int64("bytes_written", m.Written).
   165  		Msg("handled request")
   166  }
   167  
   168  func defaultHandler() http.Handler {
   169  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   170  		if r.URL.Path != "/" {
   171  			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   172  			return
   173  		}
   174  
   175  		w.WriteHeader(http.StatusOK)
   176  
   177  		fmt.Fprintln(w, "hello, world!")
   178  	})
   179  }
   180  
   181  // readynessHandler records whether the agent is ready to serve requests.
   182  //
   183  // readyness is defined by calling the method Set(true) on the handler
   184  // at least once. Once the ready state is set, the handler never goes
   185  // back to the unready state.
   186  type readynessHandler int32
   187  
   188  // NewReadynessHandler returns a new readynessHandler set to the unready
   189  // state.
   190  func NewReadynessHandler() *readynessHandler {
   191  	return new(readynessHandler)
   192  }
   193  
   194  // Set should be called once with an argument of true to indicate that
   195  // the agent is ready to serve requests. Calling it again, no matter the
   196  // value of the argument, has no effect.
   197  func (h *readynessHandler) Set(v bool) {
   198  	if v {
   199  		atomic.StoreInt32((*int32)(h), 1)
   200  	}
   201  }
   202  
   203  // ServeHTTP implements http.Handler.
   204  func (h *readynessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   205  	if atomic.LoadInt32((*int32)(h)) == 0 {
   206  		http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
   207  		return
   208  	}
   209  
   210  	// Signal readiness when the agent has connected once to the API.
   211  	w.WriteHeader(http.StatusOK)
   212  	fmt.Fprint(w, "ready")
   213  }
   214  
   215  // disconnectHandler triggers a disconnection from the API.
   216  func disconnectHandler(w http.ResponseWriter, r *http.Request) {
   217  	// TODO(mem): this is a hack to trigger a disconnection from the
   218  	// API, it would be cleaner to do this through a channel that
   219  	// communicates the request to the checks updater.
   220  	p, err := os.FindProcess(os.Getpid())
   221  	if err != nil {
   222  		msg := fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error())
   223  		http.Error(w, msg, http.StatusInternalServerError)
   224  
   225  		return
   226  	}
   227  
   228  	// SIGUSR1 will disconnect agent from API for 1 minute
   229  	err = p.Signal(syscall.SIGUSR1)
   230  	if err != nil {
   231  		msg := fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error())
   232  		http.Error(w, msg, http.StatusInternalServerError)
   233  
   234  		return
   235  	}
   236  
   237  	w.WriteHeader(http.StatusOK)
   238  	fmt.Fprintln(w, "disconnecting this agent from the API for 1 minute.")
   239  }
   240  
   241  func loggerHandler(logger zerolog.Logger) http.Handler {
   242  	defaultLevel := logger.GetLevel()
   243  
   244  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   245  		if r.Method != http.MethodPost {
   246  			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
   247  			return
   248  		}
   249  
   250  		level, err := io.ReadAll(&io.LimitedReader{R: r.Body, N: 10})
   251  		if err != nil {
   252  			http.Error(w, err.Error(), http.StatusInternalServerError)
   253  			return
   254  		}
   255  
   256  		// Consume the rest of the request if there's anything there.
   257  		_, _ = io.Copy(io.Discard, r.Body)
   258  
   259  		_ = r.Body.Close()
   260  
   261  		switch strings.TrimSpace(strings.ToLower(string(level))) {
   262  		case "debug":
   263  			zerolog.SetGlobalLevel(zerolog.DebugLevel)
   264  			logger.Warn().Stringer("level", zerolog.DebugLevel).Msg("changed log level")
   265  
   266  		case "default":
   267  			zerolog.SetGlobalLevel(defaultLevel)
   268  			logger.Warn().Stringer("level", defaultLevel).Msg("changed log level")
   269  
   270  		default:
   271  			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   272  		}
   273  	})
   274  }