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

     1  package querytee
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"strconv"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/go-kit/log"
    14  	"github.com/go-kit/log/level"
    15  
    16  	util_log "github.com/grafana/loki/pkg/util/log"
    17  )
    18  
    19  type ResponsesComparator interface {
    20  	Compare(expected, actual []byte) error
    21  }
    22  
    23  type ProxyEndpoint struct {
    24  	backends   []*ProxyBackend
    25  	metrics    *ProxyMetrics
    26  	logger     log.Logger
    27  	comparator ResponsesComparator
    28  
    29  	// Whether for this endpoint there's a preferred backend configured.
    30  	hasPreferredBackend bool
    31  
    32  	// The route name used to track metrics.
    33  	routeName string
    34  }
    35  
    36  func NewProxyEndpoint(backends []*ProxyBackend, routeName string, metrics *ProxyMetrics, logger log.Logger, comparator ResponsesComparator) *ProxyEndpoint {
    37  	hasPreferredBackend := false
    38  	for _, backend := range backends {
    39  		if backend.preferred {
    40  			hasPreferredBackend = true
    41  			break
    42  		}
    43  	}
    44  
    45  	return &ProxyEndpoint{
    46  		backends:            backends,
    47  		routeName:           routeName,
    48  		metrics:             metrics,
    49  		logger:              logger,
    50  		comparator:          comparator,
    51  		hasPreferredBackend: hasPreferredBackend,
    52  	}
    53  }
    54  
    55  func (p *ProxyEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    56  	// Send the same request to all backends.
    57  	resCh := make(chan *backendResponse, len(p.backends))
    58  	go p.executeBackendRequests(r, resCh)
    59  
    60  	// Wait for the first response that's feasible to be sent back to the client.
    61  	downstreamRes := p.waitBackendResponseForDownstream(resCh)
    62  
    63  	if downstreamRes.err != nil {
    64  		http.Error(w, downstreamRes.err.Error(), http.StatusInternalServerError)
    65  	} else {
    66  		w.WriteHeader(downstreamRes.status)
    67  		if _, err := w.Write(downstreamRes.body); err != nil {
    68  			level.Warn(p.logger).Log("msg", "Unable to write response", "err", err)
    69  		}
    70  	}
    71  
    72  	p.metrics.responsesTotal.WithLabelValues(downstreamRes.backend.name, r.Method, p.routeName).Inc()
    73  }
    74  
    75  func (p *ProxyEndpoint) executeBackendRequests(r *http.Request, resCh chan *backendResponse) {
    76  	var (
    77  		wg                  = sync.WaitGroup{}
    78  		err                 error
    79  		body                []byte
    80  		expectedResponseIdx int
    81  		responses           = make([]*backendResponse, len(p.backends))
    82  		query               = r.URL.RawQuery
    83  	)
    84  
    85  	if r.Body != nil {
    86  		body, err = ioutil.ReadAll(r.Body)
    87  		if err != nil {
    88  			level.Warn(p.logger).Log("msg", "Unable to read request body", "err", err)
    89  			return
    90  		}
    91  		if err := r.Body.Close(); err != nil {
    92  			level.Warn(p.logger).Log("msg", "Unable to close request body", "err", err)
    93  		}
    94  
    95  		r.Body = ioutil.NopCloser(bytes.NewReader(body))
    96  		if err := r.ParseForm(); err != nil {
    97  			level.Warn(p.logger).Log("msg", "Unable to parse form", "err", err)
    98  		}
    99  		query = r.Form.Encode()
   100  	}
   101  
   102  	level.Debug(p.logger).Log("msg", "Received request", "path", r.URL.Path, "query", query)
   103  
   104  	wg.Add(len(p.backends))
   105  	for i, b := range p.backends {
   106  		i := i
   107  		b := b
   108  
   109  		go func() {
   110  			defer wg.Done()
   111  			var (
   112  				bodyReader io.ReadCloser
   113  				start      = time.Now()
   114  			)
   115  			if len(body) > 0 {
   116  				bodyReader = ioutil.NopCloser(bytes.NewReader(body))
   117  			}
   118  
   119  			status, body, err := b.ForwardRequest(r, bodyReader)
   120  			elapsed := time.Since(start)
   121  
   122  			res := &backendResponse{
   123  				backend: b,
   124  				status:  status,
   125  				body:    body,
   126  				err:     err,
   127  			}
   128  
   129  			// Log with a level based on the backend response.
   130  			lvl := level.Debug
   131  			if !res.succeeded() {
   132  				lvl = level.Warn
   133  			}
   134  
   135  			lvl(p.logger).Log("msg", "Backend response", "path", r.URL.Path, "query", query, "backend", b.name, "status", status, "elapsed", elapsed)
   136  			p.metrics.requestDuration.WithLabelValues(res.backend.name, r.Method, p.routeName, strconv.Itoa(res.statusCode())).Observe(elapsed.Seconds())
   137  
   138  			// Keep track of the response if required.
   139  			if p.comparator != nil {
   140  				if b.preferred {
   141  					expectedResponseIdx = i
   142  				}
   143  				responses[i] = res
   144  			}
   145  
   146  			resCh <- res
   147  		}()
   148  	}
   149  
   150  	// Wait until all backend requests completed.
   151  	wg.Wait()
   152  	close(resCh)
   153  
   154  	// Compare responses.
   155  	if p.comparator != nil {
   156  		expectedResponse := responses[expectedResponseIdx]
   157  		for i := range responses {
   158  			if i == expectedResponseIdx {
   159  				continue
   160  			}
   161  			actualResponse := responses[i]
   162  
   163  			result := comparisonSuccess
   164  			err := p.compareResponses(expectedResponse, actualResponse)
   165  			if err != nil {
   166  				level.Error(util_log.Logger).Log("msg", "response comparison failed",
   167  					"backend-name", p.backends[i].name,
   168  					"route-name", p.routeName,
   169  					"query", r.URL.RawQuery, "err", err)
   170  				result = comparisonFailed
   171  			}
   172  
   173  			p.metrics.responsesComparedTotal.WithLabelValues(p.backends[i].name, p.routeName, result).Inc()
   174  		}
   175  	}
   176  }
   177  
   178  func (p *ProxyEndpoint) waitBackendResponseForDownstream(resCh chan *backendResponse) *backendResponse {
   179  	var (
   180  		responses                 = make([]*backendResponse, 0, len(p.backends))
   181  		preferredResponseReceived = false
   182  	)
   183  
   184  	for res := range resCh {
   185  		// If the response is successful we can immediately return it if:
   186  		// - There's no preferred backend configured
   187  		// - Or this response is from the preferred backend
   188  		// - Or the preferred backend response has already been received and wasn't successful
   189  		if res.succeeded() && (!p.hasPreferredBackend || res.backend.preferred || preferredResponseReceived) {
   190  			return res
   191  		}
   192  
   193  		// If we received a non successful response from the preferred backend, then we can
   194  		// return the first successful response received so far (if any).
   195  		if res.backend.preferred && !res.succeeded() {
   196  			preferredResponseReceived = true
   197  
   198  			for _, prevRes := range responses {
   199  				if prevRes.succeeded() {
   200  					return prevRes
   201  				}
   202  			}
   203  		}
   204  
   205  		// Otherwise we keep track of it for later.
   206  		responses = append(responses, res)
   207  	}
   208  
   209  	// No successful response, so let's pick the first one.
   210  	return responses[0]
   211  }
   212  
   213  func (p *ProxyEndpoint) compareResponses(expectedResponse, actualResponse *backendResponse) error {
   214  	// compare response body only if we get a 200
   215  	if expectedResponse.status != 200 {
   216  		return fmt.Errorf("skipped comparison of response because we got status code %d from preferred backend's response", expectedResponse.status)
   217  	}
   218  
   219  	if actualResponse.status != 200 {
   220  		return fmt.Errorf("skipped comparison of response because we got status code %d from secondary backend's response", actualResponse.status)
   221  	}
   222  
   223  	if expectedResponse.status != actualResponse.status {
   224  		return fmt.Errorf("expected status code %d but got %d", expectedResponse.status, actualResponse.status)
   225  	}
   226  
   227  	return p.comparator.Compare(expectedResponse.body, actualResponse.body)
   228  }
   229  
   230  type backendResponse struct {
   231  	backend *ProxyBackend
   232  	status  int
   233  	body    []byte
   234  	err     error
   235  }
   236  
   237  func (r *backendResponse) succeeded() bool {
   238  	if r.err != nil {
   239  		return false
   240  	}
   241  
   242  	// We consider the response successful if it's a 2xx or 4xx (but not 429).
   243  	return (r.status >= 200 && r.status < 300) || (r.status >= 400 && r.status < 500 && r.status != 429)
   244  }
   245  
   246  func (r *backendResponse) statusCode() int {
   247  	if r.err != nil || r.status <= 0 {
   248  		return 500
   249  	}
   250  
   251  	return r.status
   252  }