github.com/google/cloudprober@v0.11.3/servers/http/http.go (about)

     1  // Copyright 2017-2019 The Cloudprober 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 http implements an HTTP server that simply returns 'ok' for any URL and sends stats
    16  // on a string channel. This is used by cloudprober to act as the backend for the HTTP based
    17  // probes.
    18  package http
    19  
    20  import (
    21  	"context"
    22  	"crypto/tls"
    23  	"errors"
    24  	"fmt"
    25  	"net"
    26  	"net/http"
    27  	"strconv"
    28  	"time"
    29  
    30  	"github.com/google/cloudprober/logger"
    31  	"github.com/google/cloudprober/metrics"
    32  	"github.com/google/cloudprober/probes/probeutils"
    33  	configpb "github.com/google/cloudprober/servers/http/proto"
    34  	"github.com/google/cloudprober/sysvars"
    35  	"github.com/google/cloudprober/targets/endpoint"
    36  	"github.com/google/cloudprober/targets/lameduck"
    37  )
    38  
    39  const statsExportInterval = 10 * time.Second
    40  
    41  // OK is the response returned as successful indication by "/", and "/healthcheck".
    42  var OK = "ok"
    43  
    44  // statsKeeper manages the stats and exports those stats at a regular basis.
    45  // Currently we only maintain the number of requests received per URL.
    46  func (s *Server) statsKeeper(name string) {
    47  	doExport := time.Tick(s.statsInterval)
    48  	for {
    49  		select {
    50  		case ts := <-doExport:
    51  			em := metrics.NewEventMetrics(ts).
    52  				AddMetric("req", s.reqMetric).
    53  				AddLabel("module", name)
    54  			s.dataChan <- em
    55  		}
    56  	}
    57  }
    58  
    59  // lameduckStatus fetches the global list of lameduck targets and returns:
    60  // - (true, nil) if this machine is in that list
    61  // - (false, nil) if not
    62  // - (false, error) upon failure to fetch the list.
    63  func (s *Server) lameduckStatus() (bool, error) {
    64  	if s.ldLister == nil {
    65  		return false, errors.New("lameduck lister not initialized")
    66  	}
    67  
    68  	lameducksList := s.ldLister.ListEndpoints()
    69  	for _, ep := range lameducksList {
    70  		if s.instanceName == ep.Name {
    71  			return true, nil
    72  		}
    73  	}
    74  	return false, nil
    75  }
    76  
    77  func (s *Server) lameduckHandler(w http.ResponseWriter) {
    78  	if lameduck, err := s.lameduckStatus(); err != nil {
    79  		fmt.Fprintf(w, "HTTP Server: Error getting lameduck status: %v", err)
    80  	} else {
    81  		w.Write([]byte(strconv.FormatBool(lameduck)))
    82  	}
    83  }
    84  
    85  func (s *Server) healthcheckHandler(w http.ResponseWriter) {
    86  	lameduck, err := s.lameduckStatus()
    87  	if err != nil {
    88  		s.l.Error(err.Error())
    89  	}
    90  	if lameduck {
    91  		http.Error(w, "lameduck", http.StatusServiceUnavailable)
    92  	} else {
    93  		w.Write([]byte(OK))
    94  	}
    95  }
    96  
    97  func (s *Server) metadataHandler(w http.ResponseWriter, r *http.Request) {
    98  	varNames, ok := r.URL.Query()["var"]
    99  	if !ok || len(varNames) == 0 {
   100  		http.Error(w, "not found", http.StatusNotFound)
   101  		return
   102  	}
   103  	val, ok := s.sysVars[varNames[0]]
   104  	if !ok {
   105  		http.Error(w, fmt.Sprintf("'%s' not found", varNames[0]), http.StatusNotFound)
   106  		return
   107  	}
   108  	w.Write([]byte(val))
   109  }
   110  
   111  func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
   112  	switch r.URL.Path {
   113  	case "/lameduck":
   114  		s.lameduckHandler(w)
   115  	case "/healthcheck":
   116  		s.healthcheckHandler(w)
   117  	case "/metadata":
   118  		s.metadataHandler(w, r)
   119  	default:
   120  		res, ok := s.staticURLResTable[r.URL.Path]
   121  		if !ok {
   122  			http.Error(w, "not found", http.StatusNotFound)
   123  			return
   124  		}
   125  		w.Write(res)
   126  	}
   127  	s.reqMetric.IncKey(r.URL.Path)
   128  }
   129  
   130  // Server implements a basic single-threaded, fast response web server.
   131  type Server struct {
   132  	c                 *configpb.ServerConf
   133  	ln                net.Listener
   134  	instanceName      string
   135  	sysVars           map[string]string
   136  	staticURLResTable map[string][]byte
   137  	reqMetric         *metrics.Map
   138  	dataChan          chan<- *metrics.EventMetrics
   139  	statsInterval     time.Duration
   140  	ldLister          endpoint.Lister // Lameduck lister
   141  	l                 *logger.Logger
   142  }
   143  
   144  // New returns a Server.
   145  func New(initCtx context.Context, c *configpb.ServerConf, l *logger.Logger) (*Server, error) {
   146  	ln, err := net.Listen("tcp", fmt.Sprintf(":%d", int(c.GetPort())))
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	// If we are not able get the default lameduck lister, we only log a warning.
   152  	ldLister, err := lameduck.GetDefaultLister()
   153  	if err != nil {
   154  		l.Warning(err.Error())
   155  	}
   156  
   157  	if c.GetProtocol() == configpb.ServerConf_HTTPS {
   158  		if c.GetTlsCertFile() == "" || c.GetTlsKeyFile() == "" {
   159  			return nil, errors.New("tls_cert_file and tls_key_file are required for HTTPS servers")
   160  		}
   161  	}
   162  
   163  	// Cleanup listener if initCtx is canceled.
   164  	go func() {
   165  		<-initCtx.Done()
   166  		ln.Close()
   167  	}()
   168  
   169  	sysVars := sysvars.Vars()
   170  
   171  	return &Server{
   172  		c:             c,
   173  		l:             l,
   174  		ln:            ln,
   175  		ldLister:      ldLister,
   176  		sysVars:       sysVars,
   177  		reqMetric:     metrics.NewMap("url", metrics.NewInt(0)),
   178  		statsInterval: statsExportInterval,
   179  		instanceName:  sysvars.Vars()["instance"],
   180  		staticURLResTable: map[string][]byte{
   181  			"/":         []byte(OK),
   182  			"/instance": []byte(sysVars["instance"]),
   183  		},
   184  	}, nil
   185  }
   186  
   187  // Start starts a simple HTTP server on a given port. This function returns
   188  // only if there is an error.
   189  func (s *Server) Start(ctx context.Context, dataChan chan<- *metrics.EventMetrics) error {
   190  	s.dataChan = dataChan
   191  
   192  	laddr := s.ln.Addr().String()
   193  	go s.statsKeeper(fmt.Sprintf("http-server-%s", laddr))
   194  
   195  	for _, dh := range s.c.GetPatternDataHandler() {
   196  		payload := make([]byte, int(dh.GetResponseSize()))
   197  		probeutils.PatternPayload(payload, []byte(dh.GetPattern()))
   198  		s.staticURLResTable[fmt.Sprintf("/data_%d", dh.GetResponseSize())] = payload
   199  	}
   200  
   201  	// Not using default server mux as we may run multiple HTTP servers, e.g. for testing.
   202  	serverMux := http.NewServeMux()
   203  	serverMux.HandleFunc("/", s.handler)
   204  	s.l.Infof("Starting HTTP server at: %s", laddr)
   205  	srv := &http.Server{
   206  		Addr:         laddr,
   207  		Handler:      serverMux,
   208  		ReadTimeout:  time.Duration(s.c.GetReadTimeoutMs()) * time.Millisecond,
   209  		WriteTimeout: time.Duration(s.c.GetWriteTimeoutMs()) * time.Millisecond,
   210  		IdleTimeout:  time.Duration(s.c.GetIdleTimeoutMs()) * time.Millisecond,
   211  	}
   212  
   213  	// Setup a background function to close server if context is canceled.
   214  	go func() {
   215  		<-ctx.Done()
   216  		srv.Close()
   217  	}()
   218  
   219  	// HTTP/2 is enabled by default for HTTPS servers. To disable it, TLSNextProto
   220  	// should be non-nil and set to an empty dict.
   221  	if s.c.GetDisableHttp2() {
   222  		srv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
   223  	}
   224  
   225  	// Following returns only in case of an error.
   226  	if s.c.GetProtocol() == configpb.ServerConf_HTTP {
   227  		return srv.Serve(s.ln)
   228  	}
   229  	return srv.ServeTLS(s.ln, s.c.GetTlsCertFile(), s.c.GetTlsKeyFile())
   230  }