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 }