github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/querier/queryrange/stats.go (about)

     1  package queryrange
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/go-kit/log"
    14  	"github.com/go-kit/log/level"
    15  	promql_parser "github.com/prometheus/prometheus/promql/parser"
    16  	"github.com/weaveworks/common/middleware"
    17  
    18  	"github.com/grafana/loki/pkg/logql"
    19  	"github.com/grafana/loki/pkg/logqlmodel"
    20  	"github.com/grafana/loki/pkg/logqlmodel/stats"
    21  	"github.com/grafana/loki/pkg/querier/queryrange/queryrangebase"
    22  	util_log "github.com/grafana/loki/pkg/util/log"
    23  	"github.com/grafana/loki/pkg/util/spanlogger"
    24  )
    25  
    26  type ctxKeyType string
    27  
    28  const ctxKey ctxKeyType = "stats"
    29  
    30  const (
    31  	queryTypeLog    = "log"
    32  	queryTypeMetric = "metric"
    33  	queryTypeSeries = "series"
    34  	queryTypeLabel  = "label"
    35  )
    36  
    37  var (
    38  	defaultMetricRecorder = metricRecorderFn(func(data *queryData) {
    39  		recordQueryMetrics(data)
    40  	})
    41  
    42  	StatsHTTPMiddleware middleware.Interface = statsHTTPMiddleware(defaultMetricRecorder)
    43  )
    44  
    45  // recordQueryMetrics will be called from Query Frontend middleware chain for any type of query.
    46  func recordQueryMetrics(data *queryData) {
    47  	logger := log.With(util_log.Logger, "component", "frontend")
    48  
    49  	switch data.queryType {
    50  	case queryTypeLog, queryTypeMetric:
    51  		logql.RecordRangeAndInstantQueryMetrics(data.ctx, logger, data.params, data.status, *data.statistics, data.result)
    52  	case queryTypeLabel:
    53  		logql.RecordLabelQueryMetrics(data.ctx, logger, data.params.Start(), data.params.End(), data.label, data.status, *data.statistics)
    54  	case queryTypeSeries:
    55  		logql.RecordSeriesQueryMetrics(data.ctx, logger, data.params.Start(), data.params.End(), data.match, data.status, *data.statistics)
    56  	default:
    57  		level.Error(logger).Log("msg", "failed to record query metrics", "err", fmt.Errorf("expected one of the *LokiRequest, *LokiInstantRequest, *LokiSeriesRequest, *LokiLabelNamesRequest, got %s", data.queryType))
    58  	}
    59  }
    60  
    61  type metricRecorder interface {
    62  	Record(data *queryData)
    63  }
    64  
    65  type metricRecorderFn func(data *queryData)
    66  
    67  func (m metricRecorderFn) Record(data *queryData) {
    68  	m(data)
    69  }
    70  
    71  type queryData struct {
    72  	ctx        context.Context
    73  	params     logql.Params
    74  	statistics *stats.Result
    75  	result     promql_parser.Value
    76  	status     string
    77  	queryType  string
    78  	match      []string // used in `series` query.
    79  	label      string   // used in `labels` query
    80  
    81  	recorded bool
    82  }
    83  
    84  func statsHTTPMiddleware(recorder metricRecorder) middleware.Interface {
    85  	return middleware.Func(func(next http.Handler) http.Handler {
    86  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    87  			data := &queryData{}
    88  			interceptor := &interceptor{ResponseWriter: w, statusCode: http.StatusOK}
    89  			r = r.WithContext(context.WithValue(r.Context(), ctxKey, data))
    90  			next.ServeHTTP(
    91  				interceptor,
    92  				r,
    93  			)
    94  			if data.recorded {
    95  				if data.statistics == nil {
    96  					data.statistics = &stats.Result{}
    97  				}
    98  				data.ctx = r.Context()
    99  				data.status = strconv.Itoa(interceptor.statusCode)
   100  				recorder.Record(data)
   101  			}
   102  		})
   103  	})
   104  }
   105  
   106  // StatsCollectorMiddleware compute the stats summary based on the actual duration of the request and inject it in the request context.
   107  func StatsCollectorMiddleware() queryrangebase.Middleware {
   108  	return queryrangebase.MiddlewareFunc(func(next queryrangebase.Handler) queryrangebase.Handler {
   109  		return queryrangebase.HandlerFunc(func(ctx context.Context, req queryrangebase.Request) (queryrangebase.Response, error) {
   110  			logger := spanlogger.FromContext(ctx)
   111  			start := time.Now()
   112  
   113  			// start a new statistics context to be used by middleware, which we will merge with the response's statistics
   114  			st, statsCtx := stats.NewContext(ctx)
   115  
   116  			// execute the request
   117  			resp, err := next.Do(statsCtx, req)
   118  
   119  			// collect stats and status
   120  			var statistics *stats.Result
   121  			var res promql_parser.Value
   122  			var queryType string
   123  			var totalEntries int
   124  
   125  			if resp != nil {
   126  				switch r := resp.(type) {
   127  				case *LokiResponse:
   128  					statistics = &r.Statistics
   129  					totalEntries = int(logqlmodel.Streams(r.Data.Result).Lines())
   130  					queryType = queryTypeLog
   131  				case *LokiPromResponse:
   132  					statistics = &r.Statistics
   133  					if r.Response != nil {
   134  						totalEntries = len(r.Response.Data.Result)
   135  					}
   136  					queryType = queryTypeMetric
   137  				case *LokiSeriesResponse:
   138  					statistics = &r.Statistics
   139  					totalEntries = len(r.Data)
   140  					queryType = queryTypeSeries
   141  				case *LokiLabelNamesResponse:
   142  					statistics = &r.Statistics
   143  					totalEntries = len(r.Data)
   144  					queryType = queryTypeLabel
   145  				default:
   146  					level.Warn(logger).Log("msg", fmt.Sprintf("cannot compute stats, unexpected type: %T", resp))
   147  				}
   148  			}
   149  
   150  			if statistics != nil {
   151  				// merge the response's statistics with the stats collected by the middleware
   152  				statistics.Merge(st.Result(time.Since(start), 0, totalEntries))
   153  
   154  				// Re-calculate the summary: the queueTime result is already merged so should not be updated
   155  				// Log and record metrics for the current query
   156  				statistics.ComputeSummary(time.Since(start), 0, totalEntries)
   157  				statistics.Log(level.Debug(logger))
   158  			}
   159  			ctxValue := ctx.Value(ctxKey)
   160  			if data, ok := ctxValue.(*queryData); ok {
   161  				data.recorded = true
   162  				data.statistics = statistics
   163  				data.result = res
   164  				data.queryType = queryType
   165  				p, errReq := paramsFromRequest(req)
   166  				if errReq != nil {
   167  					return nil, errReq
   168  				}
   169  				data.params = p
   170  
   171  				// Record information for metadata queries.
   172  				switch r := req.(type) {
   173  				case *LokiLabelNamesRequest:
   174  					data.label = getLabelNameFromLabelsQuery(r.Path)
   175  				case *LokiSeriesRequest:
   176  					data.match = r.Match
   177  				}
   178  
   179  			}
   180  			return resp, err
   181  		})
   182  	})
   183  }
   184  
   185  func getLabelNameFromLabelsQuery(path string) string {
   186  	if strings.HasSuffix(path, "/values") {
   187  
   188  		toks := strings.FieldsFunc(path, func(r rune) bool {
   189  			return r == '/'
   190  		})
   191  
   192  		// now assuming path has suffix `/values` label name should be second last to the suffix
   193  		// **if** there exists the second last.
   194  		length := len(toks)
   195  		if length >= 2 {
   196  			return toks[length-2]
   197  		}
   198  
   199  	}
   200  
   201  	return ""
   202  }
   203  
   204  // interceptor implements WriteHeader to intercept status codes. WriteHeader
   205  // may not be called on success, so initialize statusCode with the status you
   206  // want to report on success, i.e. http.StatusOK.
   207  //
   208  // interceptor also implements net.Hijacker, to let the downstream Handler
   209  // hijack the connection. This is needed, for example, for working with websockets.
   210  type interceptor struct {
   211  	http.ResponseWriter
   212  	statusCode int
   213  	recorded   bool
   214  }
   215  
   216  func (i *interceptor) WriteHeader(code int) {
   217  	if !i.recorded {
   218  		i.statusCode = code
   219  		i.recorded = true
   220  	}
   221  	i.ResponseWriter.WriteHeader(code)
   222  }
   223  
   224  func (i *interceptor) Hijack() (net.Conn, *bufio.ReadWriter, error) {
   225  	hj, ok := i.ResponseWriter.(http.Hijacker)
   226  	if !ok {
   227  		return nil, nil, fmt.Errorf("interceptor: can't cast parent ResponseWriter to Hijacker")
   228  	}
   229  	return hj.Hijack()
   230  }