github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/graphite/render_parser.go (about)

     1  // Copyright (c) 2019 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package graphite
    22  
    23  import (
    24  	"context"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"math"
    29  	"net/http"
    30  	"strconv"
    31  	"time"
    32  
    33  	"github.com/m3db/m3/src/query/api/v1/handler/graphite/pickle"
    34  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    35  	"github.com/m3db/m3/src/query/graphite/graphite"
    36  	"github.com/m3db/m3/src/query/graphite/ts"
    37  	"github.com/m3db/m3/src/query/storage"
    38  	"github.com/m3db/m3/src/query/util/json"
    39  	xerrors "github.com/m3db/m3/src/x/errors"
    40  	xhttp "github.com/m3db/m3/src/x/net/http"
    41  )
    42  
    43  const (
    44  	realTimeQueryThreshold   = time.Minute
    45  	queryRangeShiftThreshold = 55 * time.Minute
    46  	queryRangeShift          = 15 * time.Second
    47  	pickleFormat             = "pickle"
    48  )
    49  
    50  var (
    51  	errNoTarget           = xerrors.NewInvalidParamsError(errors.New("no 'target' specified"))
    52  	errFromNotBeforeUntil = xerrors.NewInvalidParamsError(errors.New("'from' must come before 'until'"))
    53  )
    54  
    55  // WriteRenderResponse writes the response to a render request
    56  func WriteRenderResponse(
    57  	w http.ResponseWriter,
    58  	series ts.SeriesList,
    59  	format string,
    60  	opts renderResultsJSONOptions,
    61  ) error {
    62  	if format == pickleFormat {
    63  		w.Header().Set(xhttp.HeaderContentType, xhttp.ContentTypeOctetStream)
    64  		return renderResultsPickle(w, series.Values)
    65  	}
    66  
    67  	// NB: return json unless requesting specifically `pickleFormat`
    68  	w.Header().Set(xhttp.HeaderContentType, xhttp.ContentTypeJSON)
    69  	return renderResultsJSON(w, series.Values, opts)
    70  }
    71  
    72  const (
    73  	tzOffsetForAbsoluteTime = time.Duration(0)
    74  	maxTimeout              = time.Minute
    75  )
    76  
    77  // RenderRequest are the arguments to a render call.
    78  type RenderRequest struct {
    79  	Targets       []string
    80  	Format        string
    81  	From          time.Time
    82  	Until         time.Time
    83  	MaxDataPoints int64
    84  	Compare       time.Duration
    85  	Timeout       time.Duration
    86  }
    87  
    88  // ParseRenderRequest parses the arguments to a render call from an incoming request.
    89  func ParseRenderRequest(
    90  	ctx context.Context,
    91  	r *http.Request,
    92  	fetchOptsBuilder handleroptions.FetchOptionsBuilder,
    93  ) (context.Context, RenderRequest, *storage.FetchOptions, error) {
    94  	ctx, fetchOpts, err := fetchOptsBuilder.NewFetchOptions(ctx, r)
    95  	if err != nil {
    96  		return nil, RenderRequest{}, nil, err
    97  	}
    98  
    99  	if err := r.ParseForm(); err != nil {
   100  		return nil, RenderRequest{}, nil, err
   101  	}
   102  
   103  	var (
   104  		p = RenderRequest{
   105  			Timeout: fetchOpts.Timeout,
   106  		}
   107  		now = time.Now()
   108  	)
   109  	p.Targets = r.Form["target"]
   110  	if len(p.Targets) == 0 {
   111  		return nil, p, nil, errNoTarget
   112  	}
   113  
   114  	fromString, untilString := r.FormValue("from"), r.FormValue("until")
   115  	if len(fromString) == 0 {
   116  		fromString = "-30min"
   117  	}
   118  
   119  	if len(untilString) == 0 {
   120  		untilString = "now"
   121  	}
   122  
   123  	if p.From, err = graphite.ParseTime(
   124  		fromString,
   125  		now,
   126  		tzOffsetForAbsoluteTime,
   127  	); err != nil {
   128  		return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'from': %s", fromString))
   129  	}
   130  
   131  	if p.Until, err = graphite.ParseTime(
   132  		untilString,
   133  		now,
   134  		tzOffsetForAbsoluteTime,
   135  	); err != nil {
   136  		return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'until': %s", untilString))
   137  	}
   138  
   139  	if !p.From.Before(p.Until) {
   140  		return nil, p, nil, errFromNotBeforeUntil
   141  	}
   142  
   143  	// If this is a real-time query, and the query range is large enough, we shift the query
   144  	// range slightly to take into account the clock skew between the client's local time and
   145  	// the server's local time in order to take advantage of possibly higher-resolution data.
   146  	// In the future we could potentially distinguish absolute time and relative time and only
   147  	// use the time range for policy resolution, although we need to be careful when passing
   148  	// the range for cross-DC queries.
   149  	isRealTimeQuery := now.Sub(p.Until) < realTimeQueryThreshold
   150  	isLargeRangeQuery := p.Until.Sub(p.From) > queryRangeShiftThreshold
   151  	if isRealTimeQuery && isLargeRangeQuery {
   152  		p.From = p.From.Add(queryRangeShift)
   153  		p.Until = p.Until.Add(queryRangeShift)
   154  	}
   155  
   156  	offset := r.FormValue("offset")
   157  	if len(offset) > 0 {
   158  		dur, err := graphite.ParseDuration(offset)
   159  		if err != nil {
   160  			err = xerrors.NewInvalidParamsError(err)
   161  			return nil, p, nil, xerrors.NewRenamedError(err, fmt.Errorf("invalid 'offset': %w", err))
   162  		}
   163  
   164  		p.Until = p.Until.Add(dur)
   165  		p.From = p.From.Add(dur)
   166  	}
   167  
   168  	maxDataPointsString := r.FormValue("maxDataPoints")
   169  	if len(maxDataPointsString) != 0 {
   170  		p.MaxDataPoints, err = strconv.ParseInt(maxDataPointsString, 10, 64)
   171  
   172  		if err != nil || p.MaxDataPoints < 1 {
   173  			return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'maxDataPoints': %s", maxDataPointsString))
   174  		}
   175  	} else {
   176  		p.MaxDataPoints = math.MaxInt64
   177  	}
   178  
   179  	compareString := r.FormValue("compare")
   180  
   181  	if compareFrom, err := graphite.ParseTime(
   182  		compareString,
   183  		p.From,
   184  		tzOffsetForAbsoluteTime,
   185  	); err != nil && len(compareString) != 0 {
   186  		return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'compare': %s", compareString))
   187  	} else if p.From.Before(compareFrom) {
   188  		return nil, p, nil, xerrors.NewInvalidParamsError(fmt.Errorf("'compare' must be in the past"))
   189  	} else {
   190  		p.Compare = compareFrom.Sub(p.From)
   191  	}
   192  
   193  	return ctx, p, fetchOpts, nil
   194  }
   195  
   196  type renderResultsJSONOptions struct {
   197  	renderSeriesAllNaNs bool
   198  }
   199  
   200  func renderResultsJSON(
   201  	w io.Writer,
   202  	series []*ts.Series,
   203  	opts renderResultsJSONOptions,
   204  ) error {
   205  	jw := json.NewWriter(w)
   206  	jw.BeginArray()
   207  	for _, s := range series {
   208  		jw.BeginObject()
   209  		jw.BeginObjectField("target")
   210  		jw.WriteString(s.Name())
   211  		jw.BeginObjectField("datapoints")
   212  		jw.BeginArray()
   213  
   214  		if !s.AllNaN() || opts.renderSeriesAllNaNs {
   215  			for i := 0; i < s.Len(); i++ {
   216  				timestamp, val := s.StartTimeForStep(i), s.ValueAt(i)
   217  				jw.BeginArray()
   218  				jw.WriteFloat64(val)
   219  				jw.WriteInt(int(timestamp.Unix()))
   220  				jw.EndArray()
   221  			}
   222  		}
   223  
   224  		jw.EndArray()
   225  		jw.BeginObjectField("step_size_ms")
   226  		jw.WriteInt(s.MillisPerStep())
   227  
   228  		jw.EndObject()
   229  	}
   230  	jw.EndArray()
   231  	return jw.Close()
   232  }
   233  
   234  func renderResultsPickle(w io.Writer, series []*ts.Series) error {
   235  	pw := pickle.NewWriter(w)
   236  	pw.BeginList()
   237  
   238  	for _, s := range series {
   239  		pw.BeginDict()
   240  		pw.WriteDictKey("name")
   241  		pw.WriteString(s.Name())
   242  
   243  		pw.WriteDictKey("start")
   244  		pw.WriteInt(int(s.StartTime().UTC().Unix()))
   245  
   246  		pw.WriteDictKey("end")
   247  		pw.WriteInt(int(s.EndTime().UTC().Unix()))
   248  
   249  		pw.WriteDictKey("step")
   250  		pw.WriteInt(s.MillisPerStep() / 1000)
   251  
   252  		pw.WriteDictKey("values")
   253  		pw.BeginList()
   254  		for i := 0; i < s.Len(); i++ {
   255  			pw.WriteFloat64(s.ValueAt(i))
   256  		}
   257  		pw.EndList()
   258  
   259  		pw.EndDict()
   260  	}
   261  
   262  	pw.EndList()
   263  
   264  	return pw.Close()
   265  }