github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/tools/querytee/proxy.go (about)

     1  package querytee
     2  
     3  import (
     4  	"context"
     5  	"flag"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"net/http/httputil"
    10  	"net/url"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/go-kit/log"
    17  	"github.com/go-kit/log/level"
    18  	"github.com/gorilla/mux"
    19  	"github.com/pkg/errors"
    20  	"github.com/prometheus/client_golang/prometheus"
    21  )
    22  
    23  var errMinBackends = errors.New("at least 1 backend is required")
    24  
    25  type ProxyConfig struct {
    26  	ServerServicePort              int
    27  	BackendEndpoints               string
    28  	PreferredBackend               string
    29  	BackendReadTimeout             time.Duration
    30  	CompareResponses               bool
    31  	DisableBackendReadProxy        string
    32  	ValueComparisonTolerance       float64
    33  	UseRelativeError               bool
    34  	PassThroughNonRegisteredRoutes bool
    35  	SkipRecentSamples              time.Duration
    36  }
    37  
    38  func (cfg *ProxyConfig) RegisterFlags(f *flag.FlagSet) {
    39  	f.IntVar(&cfg.ServerServicePort, "server.service-port", 80, "The port where the query-tee service listens to.")
    40  	f.StringVar(&cfg.BackendEndpoints, "backend.endpoints", "", "Comma separated list of backend endpoints to query.")
    41  	f.StringVar(&cfg.PreferredBackend, "backend.preferred", "", "The hostname of the preferred backend when selecting the response to send back to the client. If no preferred backend is configured then the query-tee will send back to the client the first successful response received without waiting for other backends.")
    42  	f.DurationVar(&cfg.BackendReadTimeout, "backend.read-timeout", 90*time.Second, "The timeout when reading the response from a backend.")
    43  	f.BoolVar(&cfg.CompareResponses, "proxy.compare-responses", false, "Compare responses between preferred and secondary endpoints for supported routes.")
    44  	f.StringVar(&cfg.DisableBackendReadProxy, "proxy.disable-backend-read", "", "Comma separated list of non-primary backend hostnames to disable their read proxy. Typically used for temporarily not passing any read requests to specified backends.")
    45  	f.Float64Var(&cfg.ValueComparisonTolerance, "proxy.value-comparison-tolerance", 0.000001, "The tolerance to apply when comparing floating point values in the responses. 0 to disable tolerance and require exact match (not recommended).")
    46  	f.BoolVar(&cfg.UseRelativeError, "proxy.compare-use-relative-error", false, "Use relative error tolerance when comparing floating point values.")
    47  	f.DurationVar(&cfg.SkipRecentSamples, "proxy.compare-skip-recent-samples", 60*time.Second, "The window from now to skip comparing samples. 0 to disable.")
    48  	f.BoolVar(&cfg.PassThroughNonRegisteredRoutes, "proxy.passthrough-non-registered-routes", false, "Passthrough requests for non-registered routes to preferred backend.")
    49  }
    50  
    51  type Route struct {
    52  	Path               string
    53  	RouteName          string
    54  	Methods            []string
    55  	ResponseComparator ResponsesComparator
    56  }
    57  
    58  type Proxy struct {
    59  	cfg         ProxyConfig
    60  	backends    []*ProxyBackend
    61  	logger      log.Logger
    62  	metrics     *ProxyMetrics
    63  	readRoutes  []Route
    64  	writeRoutes []Route
    65  
    66  	// The HTTP server used to run the proxy service.
    67  	srv         *http.Server
    68  	srvListener net.Listener
    69  
    70  	// Wait group used to wait until the server has done.
    71  	done sync.WaitGroup
    72  }
    73  
    74  func NewProxy(cfg ProxyConfig, logger log.Logger, readRoutes, writeRoutes []Route, registerer prometheus.Registerer) (*Proxy, error) {
    75  	if cfg.CompareResponses && cfg.PreferredBackend == "" {
    76  		return nil, fmt.Errorf("when enabling comparison of results -backend.preferred flag must be set to hostname of preferred backend")
    77  	}
    78  
    79  	if cfg.PassThroughNonRegisteredRoutes && cfg.PreferredBackend == "" {
    80  		return nil, fmt.Errorf("when enabling passthrough for non-registered routes -backend.preferred flag must be set to hostname of backend where those requests needs to be passed")
    81  	}
    82  
    83  	p := &Proxy{
    84  		cfg:         cfg,
    85  		logger:      logger,
    86  		metrics:     NewProxyMetrics(registerer),
    87  		readRoutes:  readRoutes,
    88  		writeRoutes: writeRoutes,
    89  	}
    90  
    91  	// Parse the backend endpoints (comma separated).
    92  	parts := strings.Split(cfg.BackendEndpoints, ",")
    93  
    94  	for idx, part := range parts {
    95  		// Skip empty ones.
    96  		part = strings.TrimSpace(part)
    97  		if part == "" {
    98  			continue
    99  		}
   100  
   101  		u, err := url.Parse(part)
   102  		if err != nil {
   103  			return nil, errors.Wrapf(err, "invalid backend endpoint %s", part)
   104  		}
   105  
   106  		// The backend name is hardcoded as the backend hostname.
   107  		name := u.Hostname()
   108  		preferred := name == cfg.PreferredBackend
   109  
   110  		// In tests we have the same hostname for all backends, so we also
   111  		// support a numeric preferred backend which is the index in the list
   112  		// of backends.
   113  		if preferredIdx, err := strconv.Atoi(cfg.PreferredBackend); err == nil {
   114  			preferred = preferredIdx == idx
   115  		}
   116  
   117  		p.backends = append(p.backends, NewProxyBackend(name, u, cfg.BackendReadTimeout, preferred))
   118  	}
   119  
   120  	// At least 1 backend is required
   121  	if len(p.backends) < 1 {
   122  		return nil, errMinBackends
   123  	}
   124  
   125  	// If the preferred backend is configured, then it must exists among the actual backends.
   126  	if cfg.PreferredBackend != "" {
   127  		exists := false
   128  		for _, b := range p.backends {
   129  			if b.preferred {
   130  				exists = true
   131  				break
   132  			}
   133  		}
   134  
   135  		if !exists {
   136  			return nil, fmt.Errorf("the preferred backend (hostname) has not been found among the list of configured backends")
   137  		}
   138  	}
   139  
   140  	if cfg.CompareResponses && len(p.backends) < 2 {
   141  		return nil, fmt.Errorf("when enabling comparison of results number of backends should be at least 2")
   142  	}
   143  
   144  	// At least 2 backends are suggested
   145  	if len(p.backends) < 2 {
   146  		level.Warn(p.logger).Log("msg", "The proxy is running with only 1 backend. At least 2 backends are required to fulfil the purpose of the proxy and compare results.")
   147  	}
   148  
   149  	if cfg.DisableBackendReadProxy != "" {
   150  		readDisabledBackendHosts := strings.Split(p.cfg.DisableBackendReadProxy, ",")
   151  		for _, host := range readDisabledBackendHosts {
   152  			if host == cfg.PreferredBackend {
   153  				return nil, fmt.Errorf("the preferred backend cannot be disabled for reading")
   154  			}
   155  		}
   156  	}
   157  
   158  	return p, nil
   159  }
   160  
   161  func (p *Proxy) Start() error {
   162  	// Setup listener first, so we can fail early if the port is in use.
   163  	listener, err := net.Listen("tcp", fmt.Sprintf(":%d", p.cfg.ServerServicePort))
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	router := mux.NewRouter()
   169  
   170  	// Health check endpoint.
   171  	router.Path("/").Methods("GET").Handler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
   172  		w.WriteHeader(http.StatusOK)
   173  	}))
   174  
   175  	// register read routes
   176  	for _, route := range p.readRoutes {
   177  		var comparator ResponsesComparator
   178  		if p.cfg.CompareResponses {
   179  			comparator = route.ResponseComparator
   180  		}
   181  		router.Path(route.Path).Methods(route.Methods...).Handler(NewProxyEndpoint(filterReadDisabledBackends(p.backends, p.cfg.DisableBackendReadProxy), route.RouteName, p.metrics, p.logger, comparator))
   182  	}
   183  
   184  	for _, route := range p.writeRoutes {
   185  		router.Path(route.Path).Methods(route.Methods...).Handler(NewProxyEndpoint(p.backends, route.RouteName, p.metrics, p.logger, nil))
   186  	}
   187  
   188  	if p.cfg.PassThroughNonRegisteredRoutes {
   189  		for _, backend := range p.backends {
   190  			if backend.preferred {
   191  				router.PathPrefix("/").Handler(httputil.NewSingleHostReverseProxy(backend.endpoint))
   192  				break
   193  			}
   194  		}
   195  	}
   196  
   197  	p.srvListener = listener
   198  	p.srv = &http.Server{
   199  		ReadTimeout:  1 * time.Minute,
   200  		WriteTimeout: 2 * time.Minute,
   201  		Handler:      router,
   202  	}
   203  
   204  	// Run in a dedicated goroutine.
   205  	p.done.Add(1)
   206  	go func() {
   207  		defer p.done.Done()
   208  
   209  		if err := p.srv.Serve(p.srvListener); err != nil {
   210  			level.Error(p.logger).Log("msg", "Proxy server failed", "err", err)
   211  		}
   212  	}()
   213  
   214  	level.Info(p.logger).Log("msg", "The proxy is up and running.")
   215  	return nil
   216  }
   217  
   218  func (p *Proxy) Stop() error {
   219  	if p.srv == nil {
   220  		return nil
   221  	}
   222  
   223  	return p.srv.Shutdown(context.Background())
   224  }
   225  
   226  func (p *Proxy) Await() {
   227  	// Wait until terminated.
   228  	p.done.Wait()
   229  }
   230  
   231  func (p *Proxy) Endpoint() string {
   232  	if p.srvListener == nil {
   233  		return ""
   234  	}
   235  
   236  	return p.srvListener.Addr().String()
   237  }
   238  
   239  func filterReadDisabledBackends(backends []*ProxyBackend, disableReadProxyCfg string) []*ProxyBackend {
   240  	readEnabledBackends := make([]*ProxyBackend, 0, len(backends))
   241  	readDisabledBackendNames := strings.Split(disableReadProxyCfg, ",")
   242  	for _, b := range backends {
   243  		if !b.preferred {
   244  			readDisabled := false
   245  			for _, h := range readDisabledBackendNames {
   246  				if strings.TrimSpace(h) == b.name {
   247  					readDisabled = true
   248  					break
   249  				}
   250  			}
   251  			if readDisabled {
   252  				continue
   253  			}
   254  		}
   255  		readEnabledBackends = append(readEnabledBackends, b)
   256  	}
   257  
   258  	return readEnabledBackends
   259  }