github.com/go-graphite/carbonapi@v0.17.0/cmd/carbonapi/http/render_handler.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/gob"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/ansel1/merry"
    15  	pb "github.com/go-graphite/protocol/carbonapi_v3_pb"
    16  	"github.com/lomik/zapwriter"
    17  	"github.com/msaf1980/go-stringutils"
    18  	uuid "github.com/satori/go.uuid"
    19  	"go.uber.org/zap"
    20  
    21  	"github.com/go-graphite/carbonapi/carbonapipb"
    22  	"github.com/go-graphite/carbonapi/cmd/carbonapi/config"
    23  	"github.com/go-graphite/carbonapi/date"
    24  	"github.com/go-graphite/carbonapi/expr"
    25  	"github.com/go-graphite/carbonapi/expr/functions/cairo/png"
    26  	"github.com/go-graphite/carbonapi/expr/types"
    27  	"github.com/go-graphite/carbonapi/pkg/parser"
    28  	utilctx "github.com/go-graphite/carbonapi/util/ctx"
    29  	"github.com/go-graphite/carbonapi/zipper/helper"
    30  )
    31  
    32  func cleanupParams(r *http.Request) {
    33  	// make sure the cache key doesn't say noCache, because it will never hit
    34  	r.Form.Del("noCache")
    35  
    36  	// jsonp callback names are frequently autogenerated and hurt our cache
    37  	r.Form.Del("jsonp")
    38  
    39  	// Strip some cache-busters.  If you don't want to cache, use noCache=1
    40  	r.Form.Del("_salt")
    41  	r.Form.Del("_ts")
    42  	r.Form.Del("_t") // Used by jquery.graphite.js
    43  }
    44  
    45  func getCacheTimeout(logger *zap.Logger, r *http.Request, now32, until32 int64, duration time.Duration, cacheConfig *config.CacheConfig) int32 {
    46  	if tstr := r.FormValue("cacheTimeout"); tstr != "" {
    47  		t, err := strconv.Atoi(tstr)
    48  		if err != nil {
    49  			logger.Error("failed to parse cacheTimeout",
    50  				zap.String("cache_string", tstr),
    51  				zap.Error(err),
    52  			)
    53  		} else {
    54  			return int32(t)
    55  		}
    56  	}
    57  	if now32 == 0 || cacheConfig.ShortTimeoutSec == 0 || cacheConfig.ShortDuration == 0 {
    58  		return cacheConfig.DefaultTimeoutSec
    59  	}
    60  	if duration > cacheConfig.ShortDuration || now32-until32 > cacheConfig.ShortUntilOffsetSec {
    61  		return cacheConfig.DefaultTimeoutSec
    62  	}
    63  	// short cache ttl
    64  	return cacheConfig.ShortTimeoutSec
    65  }
    66  
    67  func renderHandler(w http.ResponseWriter, r *http.Request) {
    68  	t0 := time.Now()
    69  	uid := uuid.NewV4()
    70  
    71  	// TODO: Migrate to context.WithTimeout
    72  	// ctx, _ := context.WithTimeout(context.TODO(), config.Config.ZipperTimeout)
    73  	ctx := utilctx.SetUUID(r.Context(), uid.String())
    74  	username, _, _ := r.BasicAuth()
    75  	requestHeaders := utilctx.GetLogHeaders(ctx)
    76  
    77  	logger := zapwriter.Logger("render").With(
    78  		zap.String("carbonapi_uuid", uid.String()),
    79  		zap.String("username", username),
    80  		zap.Any("request_headers", requestHeaders),
    81  	)
    82  
    83  	srcIP, srcPort := splitRemoteAddr(r.RemoteAddr)
    84  
    85  	accessLogger := zapwriter.Logger("access")
    86  	var accessLogDetails = &carbonapipb.AccessLogDetails{
    87  		Handler:        "render",
    88  		Username:       username,
    89  		CarbonapiUUID:  uid.String(),
    90  		URL:            r.URL.RequestURI(),
    91  		PeerIP:         srcIP,
    92  		PeerPort:       srcPort,
    93  		Host:           r.Host,
    94  		Referer:        r.Referer(),
    95  		URI:            r.RequestURI,
    96  		RequestHeaders: requestHeaders,
    97  	}
    98  
    99  	logAsError := false
   100  	defer func() {
   101  		deferredAccessLogging(accessLogger, accessLogDetails, t0, logAsError)
   102  	}()
   103  
   104  	err := r.ParseForm()
   105  	if err != nil {
   106  		setError(w, accessLogDetails, err.Error(), http.StatusBadRequest, uid.String())
   107  		logAsError = true
   108  		return
   109  	}
   110  
   111  	targets := r.Form["target"]
   112  	from := r.FormValue("from")
   113  	until := r.FormValue("until")
   114  	template := r.FormValue("template")
   115  	maxDataPoints, _ := strconv.ParseInt(r.FormValue("maxDataPoints"), 10, 64)
   116  	ctx = utilctx.SetMaxDatapoints(ctx, maxDataPoints)
   117  	useCache := !parser.TruthyBool(r.FormValue("noCache"))
   118  	noNullPoints := parser.TruthyBool(r.FormValue("noNullPoints"))
   119  	// status will be checked later after we'll setup everything else
   120  	format, ok, formatRaw := getFormat(r, pngFormat)
   121  
   122  	var jsonp string
   123  
   124  	if format == jsonFormat {
   125  		// TODO(dgryski): check jsonp only has valid characters
   126  		jsonp = r.FormValue("jsonp")
   127  	}
   128  
   129  	timestampFormat := strings.ToLower(r.FormValue("timestampFormat"))
   130  	if timestampFormat == "" {
   131  		timestampFormat = "s"
   132  	}
   133  
   134  	timestampMultiplier := int64(1)
   135  	switch timestampFormat {
   136  	case "s":
   137  		timestampMultiplier = 1
   138  	case "ms", "millisecond", "milliseconds":
   139  		timestampMultiplier = 1000
   140  	case "us", "microsecond", "microseconds":
   141  		timestampMultiplier = 1000000
   142  	case "ns", "nanosecond", "nanoseconds":
   143  		timestampMultiplier = 1000000000
   144  	default:
   145  		setError(w, accessLogDetails, "unsupported timestamp format, supported: 's', 'ms', 'us', 'ns'", http.StatusBadRequest, uid.String())
   146  		logAsError = true
   147  		return
   148  	}
   149  
   150  	now := timeNow()
   151  	now32 := now.Unix()
   152  
   153  	cleanupParams(r)
   154  
   155  	// normalize from and until values
   156  	qtz := r.FormValue("tz")
   157  	from32 := date.DateParamToEpoch(from, qtz, now.Add(-24*time.Hour).Unix(), config.Config.DefaultTimeZone)
   158  	until32 := date.DateParamToEpoch(until, qtz, now.Unix(), config.Config.DefaultTimeZone)
   159  
   160  	var (
   161  		responseCacheKey     string
   162  		responseCacheTimeout int32
   163  		backendCacheTimeout  int32
   164  	)
   165  
   166  	duration := time.Second * time.Duration(until32-from32)
   167  	if len(config.Config.TruncateTime) > 0 {
   168  		from32 = timestampTruncate(from32, duration, config.Config.TruncateTime)
   169  		until32 = timestampTruncate(until32, duration, config.Config.TruncateTime)
   170  		// recalc duration
   171  		duration = time.Second * time.Duration(until32-from32)
   172  		responseCacheKey = responseCacheComputeKey(from32, until32, targets, formatRaw, maxDataPoints, noNullPoints, template)
   173  		if useCache {
   174  			responseCacheTimeout = getCacheTimeout(logger, r, now32, until32, duration, &config.Config.ResponseCacheConfig)
   175  			backendCacheTimeout = getCacheTimeout(logger, r, now32, until32, duration, &config.Config.BackendCacheConfig)
   176  		}
   177  	} else {
   178  		responseCacheKey = r.Form.Encode()
   179  		if useCache {
   180  			responseCacheTimeout = getCacheTimeout(logger, r, now32, until32, duration, &config.Config.ResponseCacheConfig)
   181  			backendCacheTimeout = getCacheTimeout(logger, r, now32, until32, duration, &config.Config.BackendCacheConfig)
   182  		}
   183  	}
   184  
   185  	accessLogDetails.UseCache = useCache
   186  	accessLogDetails.FromRaw = from
   187  	accessLogDetails.From = from32
   188  	accessLogDetails.UntilRaw = until
   189  	accessLogDetails.Until = until32
   190  	accessLogDetails.Tz = qtz
   191  	accessLogDetails.CacheTimeout = responseCacheTimeout
   192  	accessLogDetails.Format = formatRaw
   193  	accessLogDetails.Targets = targets
   194  
   195  	if !ok || !format.ValidRenderFormat() {
   196  		setError(w, accessLogDetails, "unsupported format specified: "+formatRaw, http.StatusBadRequest, uid.String())
   197  		logAsError = true
   198  		return
   199  	}
   200  
   201  	if format == protoV3Format {
   202  		body, err := io.ReadAll(r.Body)
   203  		if err != nil {
   204  			setError(w, accessLogDetails, "failed to parse message body: "+err.Error(), http.StatusBadRequest, uid.String())
   205  			return
   206  		}
   207  
   208  		var pv3Request pb.MultiFetchRequest
   209  		err = pv3Request.Unmarshal(body)
   210  
   211  		if err != nil {
   212  			setError(w, accessLogDetails, "failed to parse message body: "+err.Error(), http.StatusBadRequest, uid.String())
   213  			return
   214  		}
   215  
   216  		from32 = pv3Request.Metrics[0].StartTime
   217  		until32 = pv3Request.Metrics[0].StopTime
   218  		targets = make([]string, len(pv3Request.Metrics))
   219  		for i, r := range pv3Request.Metrics {
   220  			targets[i] = r.PathExpression
   221  		}
   222  	}
   223  
   224  	if queryLengthLimitExceeded(targets, config.Config.MaxQueryLength) {
   225  		setError(w, accessLogDetails, "total target length limit exceeded", http.StatusBadRequest, uid.String())
   226  		logAsError = true
   227  		return
   228  	}
   229  
   230  	if useCache {
   231  		tc := time.Now()
   232  		response, err := config.Config.ResponseCache.Get(responseCacheKey)
   233  		td := time.Since(tc).Nanoseconds()
   234  		ApiMetrics.RequestsCacheOverheadNS.Add(uint64(td))
   235  
   236  		accessLogDetails.CarbonzipperResponseSizeBytes = 0
   237  		accessLogDetails.CarbonapiResponseSizeBytes = int64(len(response))
   238  
   239  		if err == nil {
   240  			ApiMetrics.RequestCacheHits.Add(1)
   241  			w.Header().Set("X-Carbonapi-Request-Cached", strconv.FormatInt(int64(responseCacheTimeout), 10))
   242  			writeResponse(w, http.StatusOK, response, format, jsonp, uid.String())
   243  			accessLogDetails.FromCache = true
   244  			return
   245  		}
   246  		ApiMetrics.RequestCacheMisses.Add(1)
   247  	}
   248  
   249  	if from32 >= until32 {
   250  		setError(w, accessLogDetails, "Invalid or empty time range", http.StatusBadRequest, uid.String())
   251  		logAsError = true
   252  		return
   253  	}
   254  
   255  	defer func() {
   256  		if r := recover(); r != nil {
   257  			logger.Error("panic during eval:",
   258  				zap.String("cache_key", responseCacheKey),
   259  				zap.Any("reason", r),
   260  				zap.Stack("stack"),
   261  			)
   262  			logAsError = true
   263  			var answer string
   264  			if config.Config.HTTPResponseStackTrace {
   265  				answer = fmt.Sprintf("%v\nStack trace: %v", r, zap.Stack("").String)
   266  			} else {
   267  				answer = fmt.Sprint(r)
   268  			}
   269  			setError(w, accessLogDetails, answer, http.StatusInternalServerError, uid.String())
   270  		}
   271  	}()
   272  
   273  	errors := make(map[string]merry.Error)
   274  
   275  	var backendCacheKey string
   276  	if len(config.Config.TruncateTime) > 0 {
   277  		backendCacheKey = backendCacheComputeKeyAbs(from32, until32, targets, maxDataPoints, noNullPoints)
   278  	} else {
   279  		backendCacheKey = backendCacheComputeKey(from, until, targets, maxDataPoints, noNullPoints)
   280  	}
   281  
   282  	results, err := backendCacheFetchResults(logger, useCache, backendCacheKey, accessLogDetails)
   283  
   284  	if err != nil {
   285  		ApiMetrics.BackendCacheMisses.Add(1)
   286  
   287  		results = make([]*types.MetricData, 0)
   288  		values := make(map[parser.MetricRequest][]*types.MetricData)
   289  
   290  		if config.Config.CombineMultipleTargetsInOne && len(targets) > 0 {
   291  			exprs := make([]parser.Expr, 0, len(targets))
   292  			for _, target := range targets {
   293  				exp, e, err := parser.ParseExpr(target)
   294  				if err != nil || e != "" {
   295  					msg := buildParseErrorString(target, e, err)
   296  					setError(w, accessLogDetails, msg, http.StatusBadRequest, uid.String())
   297  					logAsError = true
   298  					return
   299  				}
   300  				exprs = append(exprs, exp)
   301  			}
   302  
   303  			ApiMetrics.RenderRequests.Add(1)
   304  
   305  			result, errs := expr.FetchAndEvalExprs(ctx, config.Config.Evaluator, exprs, from32, until32, values)
   306  			if errs != nil {
   307  				errors = errs
   308  			}
   309  
   310  			results = append(results, result...)
   311  		} else {
   312  			for _, target := range targets {
   313  				exp, e, err := parser.ParseExpr(target)
   314  				if err != nil || e != "" {
   315  					msg := buildParseErrorString(target, e, err)
   316  					setError(w, accessLogDetails, msg, http.StatusBadRequest, uid.String())
   317  					logAsError = true
   318  					return
   319  				}
   320  
   321  				ApiMetrics.RenderRequests.Add(1)
   322  
   323  				result, err := expr.FetchAndEvalExp(ctx, config.Config.Evaluator, exp, from32, until32, values)
   324  				if err != nil {
   325  					errors[target] = merry.Wrap(err)
   326  					if config.Config.Upstreams.RequireSuccessAll {
   327  						code := merry.HTTPCode(err)
   328  						if code != http.StatusOK && code != http.StatusNotFound {
   329  							break
   330  						}
   331  					}
   332  				}
   333  
   334  				results = append(results, result...)
   335  			}
   336  		}
   337  
   338  		if len(errors) == 0 && backendCacheTimeout > 0 {
   339  			w.Header().Set("X-Carbonapi-Backend-Cached", strconv.FormatInt(int64(backendCacheTimeout), 10))
   340  			backendCacheStoreResults(logger, backendCacheKey, results, backendCacheTimeout)
   341  		}
   342  	}
   343  
   344  	size := 0
   345  	for _, result := range results {
   346  		size += result.Size()
   347  	}
   348  
   349  	var body []byte
   350  
   351  	returnCode := http.StatusOK
   352  	if len(results) == 0 || (len(errors) > 0 && config.Config.Upstreams.RequireSuccessAll) {
   353  		// Obtain error code from the errors
   354  		// In case we have only "Not Found" errors, result should be 404
   355  		// Otherwise it should be 500
   356  		var errMsgs map[string]string
   357  		returnCode, errMsgs = helper.MergeHttpErrorMap(errors)
   358  		logger.Debug("error response or no response", zap.Any("error", errMsgs))
   359  		// Allow override status code for 404-not-found replies.
   360  		if returnCode == http.StatusNotFound {
   361  			returnCode = config.Config.NotFoundStatusCode
   362  		}
   363  
   364  		if returnCode == http.StatusBadRequest || returnCode == http.StatusNotFound || returnCode == http.StatusForbidden || returnCode >= 500 {
   365  			setErrors(w, accessLogDetails, errMsgs, returnCode, uid.String())
   366  			logAsError = true
   367  			return
   368  		}
   369  	}
   370  
   371  	switch format {
   372  	case jsonFormat:
   373  		if maxDataPoints != 0 {
   374  			types.ConsolidateJSON(maxDataPoints, results)
   375  			accessLogDetails.MaxDataPoints = maxDataPoints
   376  		}
   377  
   378  		body = types.MarshalJSON(results, timestampMultiplier, noNullPoints)
   379  	case protoV2Format:
   380  		body, err = types.MarshalProtobufV2(results)
   381  		if err != nil {
   382  			setError(w, accessLogDetails, err.Error(), http.StatusInternalServerError, uid.String())
   383  			logAsError = true
   384  			return
   385  		}
   386  	case protoV3Format:
   387  		body, err = types.MarshalProtobufV3(results)
   388  		if err != nil {
   389  			setError(w, accessLogDetails, err.Error(), http.StatusInternalServerError, uid.String())
   390  			logAsError = true
   391  			return
   392  		}
   393  	case rawFormat:
   394  		body = types.MarshalRaw(results)
   395  	case csvFormat:
   396  		body = types.MarshalCSV(results)
   397  	case pickleFormat:
   398  		body = types.MarshalPickle(results)
   399  	case pngFormat:
   400  		body = png.MarshalPNGRequest(r, results, template)
   401  	case svgFormat:
   402  		body = png.MarshalSVGRequest(r, results, template)
   403  	}
   404  
   405  	accessLogDetails.Metrics = targets
   406  	accessLogDetails.CarbonzipperResponseSizeBytes = int64(size)
   407  	accessLogDetails.CarbonapiResponseSizeBytes = int64(len(body))
   408  
   409  	writeResponse(w, returnCode, body, format, jsonp, uid.String())
   410  
   411  	if len(results) != 0 {
   412  		tc := time.Now()
   413  		config.Config.ResponseCache.Set(responseCacheKey, body, responseCacheTimeout)
   414  		td := time.Since(tc).Nanoseconds()
   415  		ApiMetrics.RequestsCacheOverheadNS.Add(uint64(td))
   416  	}
   417  
   418  	gotErrors := len(errors) > 0
   419  	accessLogDetails.HaveNonFatalErrors = gotErrors
   420  }
   421  
   422  func responseCacheComputeKey(from, until int64, targets []string, format string, maxDataPoints int64, noNullPoints bool, template string) string {
   423  	var responseCacheKey stringutils.Builder
   424  	responseCacheKey.Grow(256)
   425  	responseCacheKey.WriteString("from:")
   426  	responseCacheKey.WriteInt(from, 10)
   427  	responseCacheKey.WriteString(" until:")
   428  	responseCacheKey.WriteInt(until, 10)
   429  	responseCacheKey.WriteString(" targets:")
   430  	responseCacheKey.WriteString(strings.Join(targets, ","))
   431  	responseCacheKey.WriteString(" format:")
   432  	responseCacheKey.WriteString(format)
   433  	if maxDataPoints > 0 {
   434  		responseCacheKey.WriteString(" maxDataPoints:")
   435  		responseCacheKey.WriteInt(maxDataPoints, 10)
   436  	}
   437  	if noNullPoints {
   438  		responseCacheKey.WriteString(" noNullPoints")
   439  	}
   440  	if len(template) > 0 {
   441  		responseCacheKey.WriteString(" template:")
   442  		responseCacheKey.WriteString(template)
   443  	}
   444  	return responseCacheKey.String()
   445  }
   446  
   447  func backendCacheComputeKey(from, until string, targets []string, maxDataPoints int64, noNullPoints bool) string {
   448  	var backendCacheKey stringutils.Builder
   449  	backendCacheKey.WriteString("from:")
   450  	backendCacheKey.WriteString(from)
   451  	backendCacheKey.WriteString(" until:")
   452  	backendCacheKey.WriteString(until)
   453  	backendCacheKey.WriteString(" targets:")
   454  	backendCacheKey.WriteString(strings.Join(targets, ","))
   455  	if maxDataPoints > 0 {
   456  		backendCacheKey.WriteString(" maxDataPoints:")
   457  		backendCacheKey.WriteInt(maxDataPoints, 10)
   458  	}
   459  	if noNullPoints {
   460  		backendCacheKey.WriteString(" noNullPoints")
   461  	}
   462  	return backendCacheKey.String()
   463  }
   464  
   465  func backendCacheComputeKeyAbs(from, until int64, targets []string, maxDataPoints int64, noNullPoints bool) string {
   466  	var backendCacheKey stringutils.Builder
   467  	backendCacheKey.Grow(128)
   468  	backendCacheKey.WriteString("from:")
   469  	backendCacheKey.WriteInt(from, 10)
   470  	backendCacheKey.WriteString(" until:")
   471  	backendCacheKey.WriteInt(until, 10)
   472  	backendCacheKey.WriteString(" targets:")
   473  	backendCacheKey.WriteString(strings.Join(targets, ","))
   474  	if maxDataPoints > 0 {
   475  		backendCacheKey.WriteString(" maxDataPoints:")
   476  		backendCacheKey.WriteInt(maxDataPoints, 10)
   477  	}
   478  	if noNullPoints {
   479  		backendCacheKey.WriteString(" noNullPoints")
   480  	}
   481  	return backendCacheKey.String()
   482  }
   483  
   484  func backendCacheFetchResults(logger *zap.Logger, useCache bool, backendCacheKey string, accessLogDetails *carbonapipb.AccessLogDetails) ([]*types.MetricData, error) {
   485  	if !useCache {
   486  		return nil, errors.New("useCache is false")
   487  	}
   488  
   489  	backendCacheResults, err := config.Config.BackendCache.Get(backendCacheKey)
   490  
   491  	if err != nil {
   492  		return nil, err
   493  	}
   494  
   495  	var results []*types.MetricData
   496  	cacheDecodingBuf := bytes.NewBuffer(backendCacheResults)
   497  	dec := gob.NewDecoder(cacheDecodingBuf)
   498  	err = dec.Decode(&results)
   499  
   500  	if err != nil {
   501  		logger.Error("Error decoding cached backend results")
   502  		return nil, err
   503  	}
   504  
   505  	accessLogDetails.UsedBackendCache = true
   506  	ApiMetrics.BackendCacheHits.Add(uint64(1))
   507  
   508  	return results, nil
   509  }
   510  
   511  func backendCacheStoreResults(logger *zap.Logger, backendCacheKey string, results []*types.MetricData, backendCacheTimeout int32) {
   512  	var serializedResults bytes.Buffer
   513  	enc := gob.NewEncoder(&serializedResults)
   514  	err := enc.Encode(results)
   515  
   516  	if err != nil {
   517  		logger.Error("Error encoding backend results for caching")
   518  		return
   519  	}
   520  
   521  	config.Config.BackendCache.Set(backendCacheKey, serializedResults.Bytes(), backendCacheTimeout)
   522  }