github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/influxdb/write.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 influxdb
    22  
    23  import (
    24  	"bytes"
    25  	"compress/gzip"
    26  	"encoding/json"
    27  	"errors"
    28  	"fmt"
    29  	"io"
    30  	"io/ioutil"
    31  	"net/http"
    32  	"time"
    33  
    34  	"github.com/m3db/m3/src/cmd/services/m3coordinator/ingest"
    35  	"github.com/m3db/m3/src/dbnode/client"
    36  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    37  	"github.com/m3db/m3/src/query/api/v1/options"
    38  	"github.com/m3db/m3/src/query/api/v1/route"
    39  	"github.com/m3db/m3/src/query/models"
    40  	"github.com/m3db/m3/src/query/ts"
    41  	"github.com/m3db/m3/src/query/util/logging"
    42  
    43  	imodels "github.com/influxdata/influxdb/models"
    44  	"go.uber.org/zap"
    45  
    46  	xerrors "github.com/m3db/m3/src/x/errors"
    47  	"github.com/m3db/m3/src/x/headers"
    48  	xhttp "github.com/m3db/m3/src/x/net/http"
    49  	xtime "github.com/m3db/m3/src/x/time"
    50  )
    51  
    52  const (
    53  	// InfluxWriteURL is the Influx DB write handler URL
    54  	InfluxWriteURL = route.Prefix + "/influxdb/write"
    55  
    56  	// InfluxWriteHTTPMethod is the HTTP method used with this resource
    57  	InfluxWriteHTTPMethod = http.MethodPost
    58  )
    59  
    60  var defaultValue = ingest.IterValue{
    61  	Tags:       models.EmptyTags(),
    62  	Attributes: ts.DefaultSeriesAttributes(),
    63  	Metadata:   ts.Metadata{},
    64  }
    65  
    66  type ingestWriteHandler struct {
    67  	handlerOpts  options.HandlerOptions
    68  	tagOpts      models.TagOptions
    69  	promRewriter *promRewriter
    70  }
    71  
    72  type ingestField struct {
    73  	name  []byte // to be stored in __name__; rest of tags stay constant for the Point
    74  	value float64
    75  }
    76  
    77  type ingestIterator struct {
    78  	// what is being iterated (comes from outside)
    79  	points       []imodels.Point
    80  	tagOpts      models.TagOptions
    81  	promRewriter *promRewriter
    82  	writeTags    models.Tags
    83  
    84  	// internal
    85  	pointIndex int
    86  	err        xerrors.MultiError
    87  	metadatas  []ts.Metadata
    88  
    89  	// following entries are within current point, and initialized
    90  	// when we go to the first entry in the current point
    91  	fields         []*ingestField
    92  	nextFieldIndex int
    93  	tags           models.Tags
    94  }
    95  
    96  func (ii *ingestIterator) populateFields() bool {
    97  	point := ii.points[ii.pointIndex]
    98  	it := point.FieldIterator()
    99  	n := 0
   100  	ii.fields = make([]*ingestField, 0, 10)
   101  	bname := make([]byte, 0, len(point.Name())+1)
   102  	bname = append(bname, point.Name()...)
   103  	bname = append(bname, byte('_'))
   104  	bnamelen := len(bname)
   105  	ii.promRewriter.rewriteMetric(bname)
   106  	for it.Next() {
   107  		var value float64 = 0
   108  		n += 1
   109  		switch it.Type() {
   110  		case imodels.Boolean:
   111  			v, err := it.BooleanValue()
   112  			if err != nil {
   113  				ii.err = ii.err.Add(err)
   114  				continue
   115  			}
   116  			if v {
   117  				value = 1.0
   118  			}
   119  		case imodels.Integer:
   120  			v, err := it.IntegerValue()
   121  			if err != nil {
   122  				ii.err = ii.err.Add(err)
   123  				continue
   124  			}
   125  			value = float64(v)
   126  		case imodels.Unsigned:
   127  			v, err := it.UnsignedValue()
   128  			if err != nil {
   129  				ii.err = ii.err.Add(err)
   130  				continue
   131  			}
   132  			value = float64(v)
   133  		case imodels.Float:
   134  			v, err := it.FloatValue()
   135  			if err != nil {
   136  				ii.err = ii.err.Add(err)
   137  				continue
   138  			}
   139  			value = v
   140  		default:
   141  			// TBD if we should stick strings as
   142  			// tags or not; to prevent cardinality
   143  			// explosion, we drop them for now
   144  			continue
   145  		}
   146  		tail := it.FieldKey()
   147  		name := make([]byte, 0, bnamelen+len(tail))
   148  		name = append(name, bname...)
   149  		name = append(name, tail...)
   150  		ii.promRewriter.rewriteMetricTail(name[bnamelen:])
   151  		ii.fields = append(ii.fields, &ingestField{name: name, value: value})
   152  	}
   153  	return n > 0
   154  }
   155  
   156  func (ii *ingestIterator) Next() bool {
   157  	for len(ii.points) > ii.pointIndex {
   158  		if ii.nextFieldIndex == 0 {
   159  			// Populate tags only if we have fields we care about
   160  			if ii.populateFields() {
   161  				point := ii.points[ii.pointIndex]
   162  				ptags := point.Tags()
   163  				tags := models.NewTags(len(ptags), ii.tagOpts)
   164  				for _, tag := range ptags {
   165  					name := make([]byte, len(tag.Key))
   166  					copy(name, tag.Key)
   167  					ii.promRewriter.rewriteLabel(name)
   168  					tags = tags.AddTagWithoutNormalizing(models.Tag{Name: name, Value: tag.Value})
   169  				}
   170  				// Add or update tags given in Map-Tags-JSON header
   171  				for _, t := range ii.writeTags.Tags {
   172  					tags = tags.AddOrUpdateTag(t)
   173  				}
   174  				// sanity check no duplicate Name's;
   175  				// after Normalize, they are sorted so
   176  				// can just check them sequentially
   177  				valid := true
   178  				if len(tags.Tags) > 0 {
   179  					// Dummy w/o value set; used for dupe check and value is rewrittein in-place in SetName later on
   180  					tags = tags.AddTag(models.Tag{Name: tags.Opts.MetricName()})
   181  					name := tags.Tags[0].Name
   182  					for i := 1; i < len(tags.Tags); i++ {
   183  						iname := tags.Tags[i].Name
   184  						if bytes.Equal(name, iname) {
   185  							ii.err = ii.err.Add(fmt.Errorf("non-unique Prometheus label %v", string(iname)))
   186  							valid = false
   187  							break
   188  						}
   189  						name = iname
   190  					}
   191  				}
   192  				if !valid {
   193  					ii.pointIndex += 1
   194  					continue
   195  				}
   196  				ii.tags = tags
   197  			}
   198  		}
   199  		ii.nextFieldIndex += 1
   200  		if ii.nextFieldIndex > len(ii.fields) {
   201  			ii.pointIndex += 1
   202  			ii.nextFieldIndex = 0
   203  			continue
   204  		}
   205  		return true
   206  	}
   207  	return false
   208  }
   209  
   210  func copyTagsWithNewName(t models.Tags, name []byte) models.Tags {
   211  	copiedTags := make([]models.Tag, t.Len())
   212  	metricName := t.Opts.MetricName()
   213  	nameHandled := false
   214  	for i, tag := range t.Tags {
   215  		if !nameHandled && bytes.Equal(tag.Name, metricName) {
   216  			copiedTags[i] = models.Tag{Name: metricName, Value: name}
   217  			nameHandled = true
   218  		} else {
   219  			copiedTags[i] = tag
   220  		}
   221  	}
   222  	return models.Tags{Tags: copiedTags, Opts: t.Opts}
   223  }
   224  
   225  func determineTimeUnit(t time.Time) xtime.Unit {
   226  	ns := t.UnixNano()
   227  	if ns%int64(time.Second) == 0 {
   228  		return xtime.Second
   229  	}
   230  	if ns%int64(time.Millisecond) == 0 {
   231  		return xtime.Millisecond
   232  	}
   233  	if ns%int64(time.Microsecond) == 0 {
   234  		return xtime.Microsecond
   235  	}
   236  	return xtime.Nanosecond
   237  }
   238  
   239  func (ii *ingestIterator) Current() ingest.IterValue {
   240  	if ii.pointIndex < len(ii.points) && ii.nextFieldIndex > 0 && len(ii.fields) > (ii.nextFieldIndex-1) {
   241  		point := ii.points[ii.pointIndex]
   242  		field := ii.fields[ii.nextFieldIndex-1]
   243  		tags := copyTagsWithNewName(ii.tags, field.name)
   244  
   245  		t := xtime.ToUnixNano(point.Time())
   246  
   247  		value := ingest.IterValue{
   248  			Tags:       tags,
   249  			Datapoints: []ts.Datapoint{{Timestamp: t, Value: field.value}},
   250  			Attributes: ts.DefaultSeriesAttributes(),
   251  			Unit:       determineTimeUnit(point.Time()),
   252  		}
   253  		if ii.pointIndex < len(ii.metadatas) {
   254  			value.Metadata = ii.metadatas[ii.pointIndex]
   255  		}
   256  		return value
   257  	}
   258  	return defaultValue
   259  }
   260  
   261  func (ii *ingestIterator) Reset() error {
   262  	ii.pointIndex = 0
   263  	ii.nextFieldIndex = 0
   264  	ii.err = xerrors.NewMultiError()
   265  	return nil
   266  }
   267  
   268  func (ii *ingestIterator) Error() error {
   269  	return ii.err.FinalError()
   270  }
   271  
   272  func (ii *ingestIterator) SetCurrentMetadata(metadata ts.Metadata) {
   273  	if len(ii.metadatas) == 0 {
   274  		ii.metadatas = make([]ts.Metadata, len(ii.points))
   275  	}
   276  	if ii.pointIndex < len(ii.points) {
   277  		ii.metadatas[ii.pointIndex] = metadata
   278  	}
   279  }
   280  
   281  func (ii *ingestIterator) CurrentMetadata() ts.Metadata {
   282  	if len(ii.metadatas) == 0 || ii.pointIndex >= len(ii.metadatas) {
   283  		return ts.Metadata{}
   284  	}
   285  	return ii.metadatas[ii.pointIndex]
   286  }
   287  
   288  // NewInfluxWriterHandler returns a new influx write handler.
   289  func NewInfluxWriterHandler(options options.HandlerOptions) http.Handler {
   290  	return &ingestWriteHandler{
   291  		handlerOpts:  options,
   292  		tagOpts:      options.TagOptions(),
   293  		promRewriter: newPromRewriter(),
   294  	}
   295  }
   296  
   297  func (iwh *ingestWriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   298  	if r.Body == http.NoBody {
   299  		xhttp.WriteError(w, xhttp.NewError(errors.New("empty request body"), http.StatusBadRequest))
   300  		return
   301  	}
   302  
   303  	var bytes []byte
   304  	var err error
   305  	var reader io.ReadCloser
   306  
   307  	if r.Header.Get("Content-Encoding") == "gzip" {
   308  		reader, err = gzip.NewReader(r.Body)
   309  		if err != nil {
   310  			xhttp.WriteError(w, xhttp.NewError(err, http.StatusBadRequest))
   311  			return
   312  		}
   313  	} else {
   314  		reader = r.Body
   315  	}
   316  
   317  	bytes, err = ioutil.ReadAll(reader)
   318  	if err != nil {
   319  		xhttp.WriteError(w, err)
   320  		return
   321  	}
   322  
   323  	err = reader.Close()
   324  	if err != nil {
   325  		xhttp.WriteError(w, err)
   326  		return
   327  	}
   328  
   329  	// InfluxDB line protocol v1.8 supports following precision values ns, u, ms, s, m and h
   330  	// If precision is not given, nanosecond precision is assumed
   331  	precision := r.URL.Query().Get("precision")
   332  	points, err := imodels.ParsePointsWithPrecision(bytes, time.Now().UTC(), precision)
   333  	if err != nil {
   334  		xhttp.WriteError(w, err)
   335  		return
   336  	}
   337  
   338  	// Apply tags from "M3-Map-Tags-JSON" header
   339  	writeTags := models.NewTags(0, iwh.tagOpts)
   340  	var mapTagsOpts handleroptions.MapTagsOptions
   341  	if mapStr := r.Header.Get(headers.MapTagsByJSONHeader); mapStr != "" {
   342  		if err := json.Unmarshal([]byte(mapStr), &mapTagsOpts); err != nil {
   343  			xhttp.WriteError(w, xhttp.NewError(err, http.StatusBadRequest))
   344  			return
   345  		}
   346  		for _, mapper := range mapTagsOpts.TagMappers {
   347  			if err := mapper.Validate(); err != nil {
   348  				xhttp.WriteError(w, xhttp.NewError(err, http.StatusBadRequest))
   349  				return
   350  			}
   351  
   352  			if op := mapper.Write; !op.IsEmpty() {
   353  				tag := []byte(op.Tag)
   354  				value := []byte(op.Value)
   355  				writeTags = writeTags.AddTag(models.Tag{Name: tag, Value: value})
   356  			}
   357  
   358  			if op := mapper.Drop; !op.IsEmpty() {
   359  				err := errors.New("'drop' operation is not yet supported")
   360  				xhttp.WriteError(w, xhttp.NewError(err, http.StatusBadRequest))
   361  				return
   362  			}
   363  
   364  			if op := mapper.DropWithValue; !op.IsEmpty() {
   365  				err := errors.New("'dropWithValue' operation is not yet supported")
   366  				xhttp.WriteError(w, xhttp.NewError(err, http.StatusBadRequest))
   367  				return
   368  			}
   369  
   370  			if op := mapper.Replace; !op.IsEmpty() {
   371  				err := errors.New("'replace' operation is not yet supported")
   372  				xhttp.WriteError(w, xhttp.NewError(err, http.StatusBadRequest))
   373  				return
   374  			}
   375  		}
   376  	}
   377  
   378  	opts := ingest.WriteOptions{}
   379  	iter := &ingestIterator{points: points, tagOpts: iwh.tagOpts, promRewriter: iwh.promRewriter, writeTags: writeTags}
   380  	batchErr := iwh.handlerOpts.DownsamplerAndWriter().WriteBatch(r.Context(), iter, opts)
   381  	if batchErr == nil {
   382  		w.WriteHeader(http.StatusNoContent)
   383  		return
   384  	}
   385  	var (
   386  		errs              = batchErr.Errors()
   387  		lastRegularErr    string
   388  		lastBadRequestErr string
   389  		numRegular        int
   390  		numBadRequest     int
   391  	)
   392  	for _, err := range errs {
   393  		switch {
   394  		case client.IsBadRequestError(err):
   395  			numBadRequest++
   396  			lastBadRequestErr = err.Error()
   397  		case xerrors.IsInvalidParams(err):
   398  			numBadRequest++
   399  			lastBadRequestErr = err.Error()
   400  		default:
   401  			numRegular++
   402  			lastRegularErr = err.Error()
   403  		}
   404  	}
   405  
   406  	var status int
   407  	switch {
   408  	case numBadRequest == len(errs):
   409  		status = http.StatusBadRequest
   410  	default:
   411  		status = http.StatusInternalServerError
   412  	}
   413  
   414  	logger := logging.WithContext(r.Context(), iwh.handlerOpts.InstrumentOpts())
   415  	logger.Error("write error",
   416  		zap.String("remoteAddr", r.RemoteAddr),
   417  		zap.Int("httpResponseStatusCode", status),
   418  		zap.Int("numRegularErrors", numRegular),
   419  		zap.Int("numBadRequestErrors", numBadRequest),
   420  		zap.String("lastRegularError", lastRegularErr),
   421  		zap.String("lastBadRequestErr", lastBadRequestErr))
   422  
   423  	var resultErr string
   424  	if lastRegularErr != "" {
   425  		resultErr = fmt.Sprintf("retryable_errors: count=%d, last=%s",
   426  			numRegular, lastRegularErr)
   427  	}
   428  	if lastBadRequestErr != "" {
   429  		var sep string
   430  		if lastRegularErr != "" {
   431  			sep = ", "
   432  		}
   433  		resultErr = fmt.Sprintf("%s%sbad_request_errors: count=%d, last=%s",
   434  			resultErr, sep, numBadRequest, lastBadRequestErr)
   435  	}
   436  	xhttp.WriteError(w, xhttp.NewError(errors.New(resultErr), status))
   437  }