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 }