github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/influxdb/write_test.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 "context" 27 "fmt" 28 "io" 29 "net/http" 30 "net/http/httptest" 31 "testing" 32 "time" 33 34 "github.com/golang/mock/gomock" 35 imodels "github.com/influxdata/influxdb/models" 36 "github.com/m3db/m3/src/cmd/services/m3coordinator/ingest" 37 "github.com/m3db/m3/src/query/api/v1/options" 38 "github.com/m3db/m3/src/query/models" 39 xtest "github.com/m3db/m3/src/x/test" 40 xtime "github.com/m3db/m3/src/x/time" 41 "github.com/stretchr/testify/assert" 42 "github.com/stretchr/testify/require" 43 ) 44 45 // human-readable string out of what the iterator produces; 46 // they are easiest for human to handle 47 func (self *ingestIterator) pop(t *testing.T) string { 48 if self.Next() { 49 value := self.Current() 50 assert.Equal(t, 1, len(value.Datapoints)) 51 52 return fmt.Sprintf("%s %v %d", value.Tags.String(), value.Datapoints[0].Value, int64(value.Datapoints[0].Timestamp)) 53 } 54 return "" 55 } 56 57 func TestIngestIterator(t *testing.T) { 58 // test prometheus-illegal measure and label components (should be _s) 59 // as well as all value types influxdb supports 60 s := `?measure:!,?tag1:!=tval1,?tag2:!=tval2 ?key1:!=3,?key2:!=2i 1574838670386469800 61 ?measure:!,?tag1:!=tval1,?tag2:!=tval2 ?key3:!="string",?key4:!=T 1574838670386469801 62 ` 63 points, err := imodels.ParsePoints([]byte(s)) 64 require.NoError(t, err) 65 iter := &ingestIterator{points: points, promRewriter: newPromRewriter()} 66 require.NoError(t, iter.Error()) 67 for _, line := range []string{ 68 "__name__: _measure:___key1:_, _tag1__: tval1, _tag2__: tval2 3 1574838670386469800", 69 "__name__: _measure:___key2:_, _tag1__: tval1, _tag2__: tval2 2 1574838670386469800", 70 "__name__: _measure:___key4:_, _tag1__: tval1, _tag2__: tval2 1 1574838670386469801", 71 "", 72 "", 73 } { 74 assert.Equal(t, line, iter.pop(t)) 75 } 76 require.NoError(t, iter.Error()) 77 } 78 79 func TestIngestIteratorDuplicateTag(t *testing.T) { 80 // Ensure that duplicate tag causes error and no metrics entries 81 s := `measure,lab!=2,lab?=3 key=2i 1574838670386469800 82 ` 83 points, err := imodels.ParsePoints([]byte(s)) 84 require.NoError(t, err) 85 iter := &ingestIterator{points: points, promRewriter: newPromRewriter()} 86 require.NoError(t, iter.Error()) 87 for _, line := range []string{ 88 "", 89 } { 90 assert.Equal(t, line, iter.pop(t)) 91 } 92 require.EqualError(t, iter.Error(), "non-unique Prometheus label lab_") 93 } 94 95 func TestIngestIteratorDuplicateNameTag(t *testing.T) { 96 // Ensure that duplicate name tag causes error and no metrics entries 97 s := `measure,__name__=x key=2i 1574838670386469800 98 ` 99 points, err := imodels.ParsePoints([]byte(s)) 100 require.NoError(t, err) 101 iter := &ingestIterator{points: points, promRewriter: newPromRewriter()} 102 require.NoError(t, iter.Error()) 103 for _, line := range []string{ 104 "", 105 } { 106 assert.Equal(t, line, iter.pop(t)) 107 } 108 require.EqualError(t, iter.Error(), "non-unique Prometheus label __name__") 109 } 110 111 func TestIngestIteratorIssue2125(t *testing.T) { 112 // In the issue, the Tags object is reused across Next()+Current() calls 113 s := `measure,lab=foo k1=1,k2=2 1574838670386469800 114 ` 115 points, err := imodels.ParsePoints([]byte(s)) 116 require.NoError(t, err) 117 118 iter := &ingestIterator{points: points, promRewriter: newPromRewriter()} 119 require.NoError(t, iter.Error()) 120 121 assert.True(t, iter.Next()) 122 value1 := iter.Current() 123 124 assert.True(t, iter.Next()) 125 value2 := iter.Current() 126 require.NoError(t, iter.Error()) 127 128 assert.Equal(t, value1.Tags.String(), "__name__: measure_k1, lab: foo") 129 assert.Equal(t, value2.Tags.String(), "__name__: measure_k2, lab: foo") 130 } 131 132 func TestIngestIteratorWriteTags(t *testing.T) { 133 s := `measure,lab=foo k1=1,k2=2 1574838670386469800 134 ` 135 points, err := imodels.ParsePoints([]byte(s)) 136 require.NoError(t, err) 137 138 writeTags := models.EmptyTags(). 139 AddTag(models.Tag{Name: []byte("lab"), Value: []byte("bar")}). 140 AddTag(models.Tag{Name: []byte("new"), Value: []byte("tag")}) 141 142 iter := &ingestIterator{points: points, promRewriter: newPromRewriter(), writeTags: writeTags} 143 144 assert.True(t, iter.Next()) 145 value1 := iter.Current() 146 require.NoError(t, iter.Error()) 147 148 assert.Equal(t, value1.Tags.String(), "__name__: measure_k1, lab: bar, new: tag") 149 150 assert.True(t, iter.Next()) 151 value2 := iter.Current() 152 require.NoError(t, iter.Error()) 153 154 assert.Equal(t, value2.Tags.String(), "__name__: measure_k2, lab: bar, new: tag") 155 } 156 157 func TestDetermineTimeUnit(t *testing.T) { 158 now := time.Now() 159 zerot := now.Add(time.Duration(-now.UnixNano() % int64(time.Second))) 160 assert.Equal(t, determineTimeUnit(zerot.Add(1*time.Second)), xtime.Second) 161 assert.Equal(t, determineTimeUnit(zerot.Add(2*time.Millisecond)), xtime.Millisecond) 162 assert.Equal(t, determineTimeUnit(zerot.Add(3*time.Microsecond)), xtime.Microsecond) 163 assert.Equal(t, determineTimeUnit(zerot.Add(4*time.Nanosecond)), xtime.Nanosecond) 164 } 165 166 func makeOptions(ds ingest.DownsamplerAndWriter) options.HandlerOptions { 167 return options.EmptyHandlerOptions(). 168 SetDownsamplerAndWriter(ds) 169 } 170 171 func makeInfluxDBLineProtocolMessage(t *testing.T, isGzipped bool, time time.Time, precision time.Duration) io.Reader { 172 t.Helper() 173 ts := fmt.Sprintf("%d", time.UnixNano()/precision.Nanoseconds()) 174 line := fmt.Sprintf("weather,location=us-midwest,season=summer temperature=82 %s", ts) 175 var msg bytes.Buffer 176 if isGzipped { 177 gz := gzip.NewWriter(&msg) 178 _, err := gz.Write([]byte(line)) 179 require.NoError(t, err) 180 err = gz.Close() 181 require.NoError(t, err) 182 } else { 183 msg.WriteString(line) 184 } 185 return bytes.NewReader(msg.Bytes()) 186 } 187 188 func TestInfluxDBWrite(t *testing.T) { 189 type checkWriteBatchFunc func(context.Context, *ingestIterator, ingest.WriteOptions) interface{} 190 191 // small helper for tests where we dont want to check the batch 192 dontCheckWriteBatch := checkWriteBatchFunc( 193 func(context.Context, *ingestIterator, ingest.WriteOptions) interface{} { 194 return nil 195 }, 196 ) 197 198 tests := []struct { 199 name string 200 expectedStatus int 201 requestHeaders map[string]string 202 isGzipped bool 203 checkWriteBatch checkWriteBatchFunc 204 }{ 205 { 206 name: "Gzip Encoded Message", 207 expectedStatus: http.StatusNoContent, 208 isGzipped: true, 209 requestHeaders: map[string]string{ 210 "Content-Encoding": "gzip", 211 }, 212 checkWriteBatch: dontCheckWriteBatch, 213 }, 214 { 215 name: "Wrong Content Encoding", 216 expectedStatus: http.StatusBadRequest, 217 isGzipped: false, 218 requestHeaders: map[string]string{ 219 "Content-Encoding": "gzip", 220 }, 221 checkWriteBatch: dontCheckWriteBatch, 222 }, 223 { 224 name: "Plaintext Message", 225 expectedStatus: http.StatusNoContent, 226 isGzipped: false, 227 requestHeaders: map[string]string{}, 228 checkWriteBatch: dontCheckWriteBatch, 229 }, 230 { 231 name: "Map-Tags-JSON Add Tag", 232 expectedStatus: http.StatusNoContent, 233 isGzipped: false, 234 requestHeaders: map[string]string{ 235 "M3-Map-Tags-JSON": `{"tagMappers": [{"write": {"tag": "t", "value": "v"}}]}`, 236 }, 237 checkWriteBatch: checkWriteBatchFunc( 238 func(_ context.Context, iter *ingestIterator, opts ingest.WriteOptions) interface{} { 239 _, found := iter.writeTags.Get([]byte("t")) 240 require.True(t, found, "tag t will be overwritten") 241 return nil 242 }, 243 ), 244 }, 245 } 246 247 ctrl := xtest.NewController(t) 248 defer ctrl.Finish() 249 250 for _, testCase := range tests { 251 testCase := testCase 252 t.Run(testCase.name, func(tt *testing.T) { 253 mockDownsamplerAndWriter := ingest.NewMockDownsamplerAndWriter(ctrl) 254 // For error reponses we don't expect WriteBatch to be called 255 if testCase.expectedStatus != http.StatusBadRequest { 256 mockDownsamplerAndWriter. 257 EXPECT(). 258 WriteBatch(gomock.Any(), gomock.Any(), gomock.Any()). 259 DoAndReturn(testCase.checkWriteBatch). 260 Times(1) 261 } 262 263 opts := makeOptions(mockDownsamplerAndWriter) 264 handler := NewInfluxWriterHandler(opts) 265 msg := makeInfluxDBLineProtocolMessage(t, testCase.isGzipped, time.Now(), time.Nanosecond) 266 req := httptest.NewRequest(InfluxWriteHTTPMethod, InfluxWriteURL, msg) 267 for header, value := range testCase.requestHeaders { 268 req.Header.Set(header, value) 269 } 270 writer := httptest.NewRecorder() 271 handler.ServeHTTP(writer, req) 272 resp := writer.Result() 273 require.Equal(t, testCase.expectedStatus, resp.StatusCode) 274 resp.Body.Close() 275 }) 276 } 277 } 278 279 func TestInfluxDBWritePrecision(t *testing.T) { 280 tests := []struct { 281 name string 282 expectedStatus int 283 precision string 284 }{ 285 { 286 name: "No precision", 287 expectedStatus: http.StatusNoContent, 288 precision: "", 289 }, 290 { 291 name: "Millisecond precision", 292 expectedStatus: http.StatusNoContent, 293 precision: "ms", 294 }, 295 { 296 name: "Second precision", 297 expectedStatus: http.StatusNoContent, 298 precision: "s", 299 }, 300 } 301 302 ctrl := xtest.NewController(t) 303 defer ctrl.Finish() 304 305 for _, testCase := range tests { 306 testCase := testCase 307 t.Run(testCase.name, func(tt *testing.T) { 308 var precision time.Duration 309 switch testCase.precision { 310 case "": 311 precision = time.Nanosecond 312 case "ms": 313 precision = time.Millisecond 314 case "s": 315 precision = time.Second 316 } 317 318 now := time.Now() 319 320 mockDownsamplerAndWriter := ingest.NewMockDownsamplerAndWriter(ctrl) 321 mockDownsamplerAndWriter. 322 EXPECT(). 323 WriteBatch(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func( 324 _ context.Context, 325 iter *ingestIterator, 326 opts ingest.WriteOptions, 327 ) interface{} { 328 require.Equal(tt, now.Truncate(precision).UnixNano(), iter.points[0].UnixNano(), "correct precision") 329 return nil 330 }).Times(1) 331 332 opts := makeOptions(mockDownsamplerAndWriter) 333 handler := NewInfluxWriterHandler(opts) 334 335 msg := makeInfluxDBLineProtocolMessage(t, false, now, precision) 336 var url string 337 if testCase.precision == "" { 338 url = InfluxWriteURL 339 } else { 340 url = InfluxWriteURL + fmt.Sprintf("?precision=%s", testCase.precision) 341 } 342 req := httptest.NewRequest(InfluxWriteHTTPMethod, url, msg) 343 writer := httptest.NewRecorder() 344 handler.ServeHTTP(writer, req) 345 resp := writer.Result() 346 require.Equal(t, testCase.expectedStatus, resp.StatusCode) 347 resp.Body.Close() 348 }) 349 } 350 }