mirror of https://github.com/grafana/loki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
6.8 KiB
221 lines
6.8 KiB
|
6 years ago
|
package querytee
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"net"
|
||
|
|
"net/http"
|
||
|
6 years ago
|
"net/http/httputil"
|
||
|
6 years ago
|
"net/url"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
5 years ago
|
"github.com/go-kit/log"
|
||
|
|
"github.com/go-kit/log/level"
|
||
|
6 years ago
|
"github.com/gorilla/mux"
|
||
|
|
"github.com/pkg/errors"
|
||
|
|
"github.com/prometheus/client_golang/prometheus"
|
||
|
|
)
|
||
|
|
|
||
|
4 years ago
|
var errMinBackends = errors.New("at least 1 backend is required")
|
||
|
6 years ago
|
|
||
|
|
type ProxyConfig struct {
|
||
|
6 years ago
|
ServerServicePort int
|
||
|
|
BackendEndpoints string
|
||
|
|
PreferredBackend string
|
||
|
|
BackendReadTimeout time.Duration
|
||
|
|
CompareResponses bool
|
||
|
|
ValueComparisonTolerance float64
|
||
|
4 years ago
|
UseRelativeError bool
|
||
|
6 years ago
|
PassThroughNonRegisteredRoutes bool
|
||
|
4 years ago
|
SkipRecentSamples time.Duration
|
||
|
6 years ago
|
}
|
||
|
|
|
||
|
|
func (cfg *ProxyConfig) RegisterFlags(f *flag.FlagSet) {
|
||
|
|
f.IntVar(&cfg.ServerServicePort, "server.service-port", 80, "The port where the query-tee service listens to.")
|
||
|
|
f.StringVar(&cfg.BackendEndpoints, "backend.endpoints", "", "Comma separated list of backend endpoints to query.")
|
||
|
6 years ago
|
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.")
|
||
|
6 years ago
|
f.DurationVar(&cfg.BackendReadTimeout, "backend.read-timeout", 90*time.Second, "The timeout when reading the response from a backend.")
|
||
|
|
f.BoolVar(&cfg.CompareResponses, "proxy.compare-responses", false, "Compare responses between preferred and secondary endpoints for supported routes.")
|
||
|
6 years ago
|
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).")
|
||
|
4 years ago
|
f.BoolVar(&cfg.UseRelativeError, "proxy.compare-use-relative-error", false, "Use relative error tolerance when comparing floating point values.")
|
||
|
|
f.DurationVar(&cfg.SkipRecentSamples, "proxy.compare-skip-recent-samples", 60*time.Second, "The window from now to skip comparing samples. 0 to disable.")
|
||
|
6 years ago
|
f.BoolVar(&cfg.PassThroughNonRegisteredRoutes, "proxy.passthrough-non-registered-routes", false, "Passthrough requests for non-registered routes to preferred backend.")
|
||
|
6 years ago
|
}
|
||
|
|
|
||
|
|
type Route struct {
|
||
|
|
Path string
|
||
|
|
RouteName string
|
||
|
6 years ago
|
Methods []string
|
||
|
6 years ago
|
ResponseComparator ResponsesComparator
|
||
|
|
}
|
||
|
|
|
||
|
|
type Proxy struct {
|
||
|
|
cfg ProxyConfig
|
||
|
|
backends []*ProxyBackend
|
||
|
|
logger log.Logger
|
||
|
|
metrics *ProxyMetrics
|
||
|
|
routes []Route
|
||
|
|
|
||
|
|
// The HTTP server used to run the proxy service.
|
||
|
|
srv *http.Server
|
||
|
|
srvListener net.Listener
|
||
|
|
|
||
|
|
// Wait group used to wait until the server has done.
|
||
|
|
done sync.WaitGroup
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewProxy(cfg ProxyConfig, logger log.Logger, routes []Route, registerer prometheus.Registerer) (*Proxy, error) {
|
||
|
|
if cfg.CompareResponses && cfg.PreferredBackend == "" {
|
||
|
6 years ago
|
return nil, fmt.Errorf("when enabling comparison of results -backend.preferred flag must be set to hostname of preferred backend")
|
||
|
6 years ago
|
}
|
||
|
|
|
||
|
6 years ago
|
if cfg.PassThroughNonRegisteredRoutes && cfg.PreferredBackend == "" {
|
||
|
|
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")
|
||
|
|
}
|
||
|
|
|
||
|
6 years ago
|
p := &Proxy{
|
||
|
|
cfg: cfg,
|
||
|
|
logger: logger,
|
||
|
|
metrics: NewProxyMetrics(registerer),
|
||
|
|
routes: routes,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse the backend endpoints (comma separated).
|
||
|
|
parts := strings.Split(cfg.BackendEndpoints, ",")
|
||
|
|
|
||
|
|
for idx, part := range parts {
|
||
|
|
// Skip empty ones.
|
||
|
|
part = strings.TrimSpace(part)
|
||
|
|
if part == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
u, err := url.Parse(part)
|
||
|
|
if err != nil {
|
||
|
|
return nil, errors.Wrapf(err, "invalid backend endpoint %s", part)
|
||
|
|
}
|
||
|
|
|
||
|
|
// The backend name is hardcoded as the backend hostname.
|
||
|
|
name := u.Hostname()
|
||
|
|
preferred := name == cfg.PreferredBackend
|
||
|
|
|
||
|
|
// In tests we have the same hostname for all backends, so we also
|
||
|
|
// support a numeric preferred backend which is the index in the list
|
||
|
|
// of backends.
|
||
|
|
if preferredIdx, err := strconv.Atoi(cfg.PreferredBackend); err == nil {
|
||
|
|
preferred = preferredIdx == idx
|
||
|
|
}
|
||
|
|
|
||
|
|
p.backends = append(p.backends, NewProxyBackend(name, u, cfg.BackendReadTimeout, preferred))
|
||
|
|
}
|
||
|
|
|
||
|
|
// At least 1 backend is required
|
||
|
|
if len(p.backends) < 1 {
|
||
|
|
return nil, errMinBackends
|
||
|
|
}
|
||
|
|
|
||
|
6 years ago
|
// If the preferred backend is configured, then it must exists among the actual backends.
|
||
|
|
if cfg.PreferredBackend != "" {
|
||
|
|
exists := false
|
||
|
|
for _, b := range p.backends {
|
||
|
|
if b.preferred {
|
||
|
|
exists = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !exists {
|
||
|
|
return nil, fmt.Errorf("the preferred backend (hostname) has not been found among the list of configured backends")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
6 years ago
|
if cfg.CompareResponses && len(p.backends) != 2 {
|
||
|
|
return nil, fmt.Errorf("when enabling comparison of results number of backends should be 2 exactly")
|
||
|
|
}
|
||
|
|
|
||
|
|
// At least 2 backends are suggested
|
||
|
|
if len(p.backends) < 2 {
|
||
|
|
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.")
|
||
|
|
}
|
||
|
|
|
||
|
|
return p, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Proxy) Start() error {
|
||
|
|
// Setup listener first, so we can fail early if the port is in use.
|
||
|
|
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", p.cfg.ServerServicePort))
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
router := mux.NewRouter()
|
||
|
|
|
||
|
|
// Health check endpoint.
|
||
|
|
router.Path("/").Methods("GET").Handler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
}))
|
||
|
|
|
||
|
|
// register routes
|
||
|
|
for _, route := range p.routes {
|
||
|
6 years ago
|
var comparator ResponsesComparator
|
||
|
6 years ago
|
if p.cfg.CompareResponses {
|
||
|
|
comparator = route.ResponseComparator
|
||
|
|
}
|
||
|
6 years ago
|
router.Path(route.Path).Methods(route.Methods...).Handler(NewProxyEndpoint(p.backends, route.RouteName, p.metrics, p.logger, comparator))
|
||
|
6 years ago
|
}
|
||
|
|
|
||
|
6 years ago
|
if p.cfg.PassThroughNonRegisteredRoutes {
|
||
|
|
for _, backend := range p.backends {
|
||
|
|
if backend.preferred {
|
||
|
|
router.PathPrefix("/").Handler(httputil.NewSingleHostReverseProxy(backend.endpoint))
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
6 years ago
|
p.srvListener = listener
|
||
|
|
p.srv = &http.Server{
|
||
|
|
ReadTimeout: 1 * time.Minute,
|
||
|
|
WriteTimeout: 2 * time.Minute,
|
||
|
|
Handler: router,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run in a dedicated goroutine.
|
||
|
|
p.done.Add(1)
|
||
|
|
go func() {
|
||
|
|
defer p.done.Done()
|
||
|
|
|
||
|
|
if err := p.srv.Serve(p.srvListener); err != nil {
|
||
|
|
level.Error(p.logger).Log("msg", "Proxy server failed", "err", err)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
6 years ago
|
level.Info(p.logger).Log("msg", "The proxy is up and running.")
|
||
|
6 years ago
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Proxy) Stop() error {
|
||
|
|
if p.srv == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
return p.srv.Shutdown(context.Background())
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Proxy) Await() {
|
||
|
|
// Wait until terminated.
|
||
|
|
p.done.Wait()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *Proxy) Endpoint() string {
|
||
|
|
if p.srvListener == nil {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
return p.srvListener.Addr().String()
|
||
|
|
}
|