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  }