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 }