github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/clients/pkg/promtail/client/client_test.go (about)

     1  package client
     2  
     3  import (
     4  	"io"
     5  	"math"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"net/url"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/go-kit/log"
    14  	"github.com/grafana/dskit/backoff"
    15  	"github.com/grafana/dskit/flagext"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/prometheus/client_golang/prometheus/testutil"
    18  	"github.com/prometheus/common/config"
    19  	"github.com/prometheus/common/model"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  
    23  	"github.com/grafana/loki/clients/pkg/promtail/api"
    24  
    25  	"github.com/grafana/loki/pkg/logproto"
    26  	"github.com/grafana/loki/pkg/util"
    27  	lokiflag "github.com/grafana/loki/pkg/util/flagext"
    28  )
    29  
    30  var logEntries = []api.Entry{
    31  	{Labels: model.LabelSet{}, Entry: logproto.Entry{Timestamp: time.Unix(1, 0).UTC(), Line: "line1"}},
    32  	{Labels: model.LabelSet{}, Entry: logproto.Entry{Timestamp: time.Unix(2, 0).UTC(), Line: "line2"}},
    33  	{Labels: model.LabelSet{}, Entry: logproto.Entry{Timestamp: time.Unix(3, 0).UTC(), Line: "line3"}},
    34  	{Labels: model.LabelSet{"__tenant_id__": "tenant-1"}, Entry: logproto.Entry{Timestamp: time.Unix(4, 0).UTC(), Line: "line4"}},
    35  	{Labels: model.LabelSet{"__tenant_id__": "tenant-1"}, Entry: logproto.Entry{Timestamp: time.Unix(5, 0).UTC(), Line: "line5"}},
    36  	{Labels: model.LabelSet{"__tenant_id__": "tenant-2"}, Entry: logproto.Entry{Timestamp: time.Unix(6, 0).UTC(), Line: "line6"}},
    37  }
    38  
    39  type receivedReq struct {
    40  	tenantID string
    41  	pushReq  logproto.PushRequest
    42  }
    43  
    44  func TestClient_Handle(t *testing.T) {
    45  	tests := map[string]struct {
    46  		clientBatchSize      int
    47  		clientBatchWait      time.Duration
    48  		clientMaxRetries     int
    49  		clientTenantID       string
    50  		serverResponseStatus int
    51  		inputEntries         []api.Entry
    52  		inputDelay           time.Duration
    53  		expectedReqs         []receivedReq
    54  		expectedMetrics      string
    55  	}{
    56  		"batch log entries together until the batch size is reached": {
    57  			clientBatchSize:      10,
    58  			clientBatchWait:      100 * time.Millisecond,
    59  			clientMaxRetries:     3,
    60  			serverResponseStatus: 200,
    61  			inputEntries:         []api.Entry{logEntries[0], logEntries[1], logEntries[2]},
    62  			expectedReqs: []receivedReq{
    63  				{
    64  					tenantID: "",
    65  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry, logEntries[1].Entry}}}},
    66  				},
    67  				{
    68  					tenantID: "",
    69  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[2].Entry}}}},
    70  				},
    71  			},
    72  			expectedMetrics: `
    73  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
    74  				# TYPE promtail_sent_entries_total counter
    75  				promtail_sent_entries_total{host="__HOST__"} 3.0
    76  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
    77  				# TYPE promtail_dropped_entries_total counter
    78  				promtail_dropped_entries_total{host="__HOST__"} 0
    79  			`,
    80  		},
    81  		"batch log entries together until the batch wait time is reached": {
    82  			clientBatchSize:      10,
    83  			clientBatchWait:      100 * time.Millisecond,
    84  			clientMaxRetries:     3,
    85  			serverResponseStatus: 200,
    86  			inputEntries:         []api.Entry{logEntries[0], logEntries[1]},
    87  			inputDelay:           110 * time.Millisecond,
    88  			expectedReqs: []receivedReq{
    89  				{
    90  					tenantID: "",
    91  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
    92  				},
    93  				{
    94  					tenantID: "",
    95  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[1].Entry}}}},
    96  				},
    97  			},
    98  			expectedMetrics: `
    99  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
   100  				# TYPE promtail_sent_entries_total counter
   101  				promtail_sent_entries_total{host="__HOST__"} 2.0
   102  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
   103  				# TYPE promtail_dropped_entries_total counter
   104  				promtail_dropped_entries_total{host="__HOST__"} 0
   105  			`,
   106  		},
   107  		"retry send a batch up to backoff's max retries in case the server responds with a 5xx": {
   108  			clientBatchSize:      10,
   109  			clientBatchWait:      10 * time.Millisecond,
   110  			clientMaxRetries:     3,
   111  			serverResponseStatus: 500,
   112  			inputEntries:         []api.Entry{logEntries[0]},
   113  			expectedReqs: []receivedReq{
   114  				{
   115  					tenantID: "",
   116  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   117  				},
   118  				{
   119  					tenantID: "",
   120  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   121  				},
   122  				{
   123  					tenantID: "",
   124  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   125  				},
   126  			},
   127  			expectedMetrics: `
   128  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
   129  				# TYPE promtail_dropped_entries_total counter
   130  				promtail_dropped_entries_total{host="__HOST__"} 1.0
   131  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
   132  				# TYPE promtail_sent_entries_total counter
   133  				promtail_sent_entries_total{host="__HOST__"} 0
   134  			`,
   135  		},
   136  		"do not retry send a batch in case the server responds with a 4xx": {
   137  			clientBatchSize:      10,
   138  			clientBatchWait:      10 * time.Millisecond,
   139  			clientMaxRetries:     3,
   140  			serverResponseStatus: 400,
   141  			inputEntries:         []api.Entry{logEntries[0]},
   142  			expectedReqs: []receivedReq{
   143  				{
   144  					tenantID: "",
   145  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   146  				},
   147  			},
   148  			expectedMetrics: `
   149  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
   150  				# TYPE promtail_dropped_entries_total counter
   151  				promtail_dropped_entries_total{host="__HOST__"} 1.0
   152  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
   153  				# TYPE promtail_sent_entries_total counter
   154  				promtail_sent_entries_total{host="__HOST__"} 0
   155  			`,
   156  		},
   157  		"do retry sending a batch in case the server responds with a 429": {
   158  			clientBatchSize:      10,
   159  			clientBatchWait:      10 * time.Millisecond,
   160  			clientMaxRetries:     3,
   161  			serverResponseStatus: 429,
   162  			inputEntries:         []api.Entry{logEntries[0]},
   163  			expectedReqs: []receivedReq{
   164  				{
   165  					tenantID: "",
   166  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   167  				},
   168  				{
   169  					tenantID: "",
   170  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   171  				},
   172  				{
   173  					tenantID: "",
   174  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   175  				},
   176  			},
   177  			expectedMetrics: `
   178  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
   179  				# TYPE promtail_dropped_entries_total counter
   180  				promtail_dropped_entries_total{host="__HOST__"} 1.0
   181  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
   182  				# TYPE promtail_sent_entries_total counter
   183  				promtail_sent_entries_total{host="__HOST__"} 0
   184  			`,
   185  		},
   186  		"batch log entries together honoring the client tenant ID": {
   187  			clientBatchSize:      100,
   188  			clientBatchWait:      100 * time.Millisecond,
   189  			clientMaxRetries:     3,
   190  			clientTenantID:       "tenant-default",
   191  			serverResponseStatus: 200,
   192  			inputEntries:         []api.Entry{logEntries[0], logEntries[1]},
   193  			expectedReqs: []receivedReq{
   194  				{
   195  					tenantID: "tenant-default",
   196  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry, logEntries[1].Entry}}}},
   197  				},
   198  			},
   199  			expectedMetrics: `
   200  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
   201  				# TYPE promtail_sent_entries_total counter
   202  				promtail_sent_entries_total{host="__HOST__"} 2.0
   203  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
   204  				# TYPE promtail_dropped_entries_total counter
   205  				promtail_dropped_entries_total{host="__HOST__"} 0
   206  			`,
   207  		},
   208  		"batch log entries together honoring the tenant ID overridden while processing the pipeline stages": {
   209  			clientBatchSize:      100,
   210  			clientBatchWait:      100 * time.Millisecond,
   211  			clientMaxRetries:     3,
   212  			clientTenantID:       "tenant-default",
   213  			serverResponseStatus: 200,
   214  			inputEntries:         []api.Entry{logEntries[0], logEntries[3], logEntries[4], logEntries[5]},
   215  			expectedReqs: []receivedReq{
   216  				{
   217  					tenantID: "tenant-default",
   218  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   219  				},
   220  				{
   221  					tenantID: "tenant-1",
   222  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[3].Entry, logEntries[4].Entry}}}},
   223  				},
   224  				{
   225  					tenantID: "tenant-2",
   226  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[5].Entry}}}},
   227  				},
   228  			},
   229  			expectedMetrics: `
   230  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
   231  				# TYPE promtail_sent_entries_total counter
   232  				promtail_sent_entries_total{host="__HOST__"} 4.0
   233  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
   234  				# TYPE promtail_dropped_entries_total counter
   235  				promtail_dropped_entries_total{host="__HOST__"} 0
   236  			`,
   237  		},
   238  	}
   239  
   240  	for testName, testData := range tests {
   241  		t.Run(testName, func(t *testing.T) {
   242  			reg := prometheus.NewRegistry()
   243  
   244  			// Create a buffer channel where we do enqueue received requests
   245  			receivedReqsChan := make(chan receivedReq, 10)
   246  
   247  			// Start a local HTTP server
   248  			server := httptest.NewServer(createServerHandler(receivedReqsChan, testData.serverResponseStatus))
   249  			require.NotNil(t, server)
   250  			defer server.Close()
   251  
   252  			// Get the URL at which the local test server is listening to
   253  			serverURL := flagext.URLValue{}
   254  			err := serverURL.Set(server.URL)
   255  			require.NoError(t, err)
   256  
   257  			// Instance the client
   258  			cfg := Config{
   259  				URL:            serverURL,
   260  				BatchWait:      testData.clientBatchWait,
   261  				BatchSize:      testData.clientBatchSize,
   262  				Client:         config.HTTPClientConfig{},
   263  				BackoffConfig:  backoff.Config{MinBackoff: 1 * time.Millisecond, MaxBackoff: 2 * time.Millisecond, MaxRetries: testData.clientMaxRetries},
   264  				ExternalLabels: lokiflag.LabelSet{},
   265  				Timeout:        1 * time.Second,
   266  				TenantID:       testData.clientTenantID,
   267  			}
   268  
   269  			m := NewMetrics(reg, nil)
   270  			c, err := New(m, cfg, nil, log.NewNopLogger())
   271  			require.NoError(t, err)
   272  
   273  			// Send all the input log entries
   274  			for i, logEntry := range testData.inputEntries {
   275  				c.Chan() <- logEntry
   276  
   277  				if testData.inputDelay > 0 && i < len(testData.inputEntries)-1 {
   278  					time.Sleep(testData.inputDelay)
   279  				}
   280  			}
   281  
   282  			// Wait until the expected push requests are received (with a timeout)
   283  			deadline := time.Now().Add(1 * time.Second)
   284  			for len(receivedReqsChan) < len(testData.expectedReqs) && time.Now().Before(deadline) {
   285  				time.Sleep(5 * time.Millisecond)
   286  			}
   287  
   288  			// Stop the client: it waits until the current batch is sent
   289  			c.Stop()
   290  			close(receivedReqsChan)
   291  
   292  			// Get all push requests received on the server side
   293  			receivedReqs := make([]receivedReq, 0)
   294  			for req := range receivedReqsChan {
   295  				receivedReqs = append(receivedReqs, req)
   296  			}
   297  
   298  			// Due to implementation details (maps iteration ordering is random) we just check
   299  			// that the expected requests are equal to the received requests, without checking
   300  			// the exact order which is not guaranteed in case of multi-tenant
   301  			require.ElementsMatch(t, testData.expectedReqs, receivedReqs)
   302  
   303  			expectedMetrics := strings.Replace(testData.expectedMetrics, "__HOST__", serverURL.Host, -1)
   304  			err = testutil.GatherAndCompare(reg, strings.NewReader(expectedMetrics), "promtail_sent_entries_total", "promtail_dropped_entries_total")
   305  			assert.NoError(t, err)
   306  		})
   307  	}
   308  }
   309  
   310  func TestClient_StopNow(t *testing.T) {
   311  	cases := []struct {
   312  		name                 string
   313  		clientBatchSize      int
   314  		clientBatchWait      time.Duration
   315  		clientMaxRetries     int
   316  		clientTenantID       string
   317  		serverResponseStatus int
   318  		inputEntries         []api.Entry
   319  		inputDelay           time.Duration
   320  		expectedReqs         []receivedReq
   321  		expectedMetrics      string
   322  	}{
   323  		{
   324  			name:                 "send requests shouldn't be cancelled after StopNow()",
   325  			clientBatchSize:      10,
   326  			clientBatchWait:      100 * time.Millisecond,
   327  			clientMaxRetries:     3,
   328  			serverResponseStatus: 200,
   329  			inputEntries:         []api.Entry{logEntries[0], logEntries[1], logEntries[2]},
   330  			expectedReqs: []receivedReq{
   331  				{
   332  					tenantID: "",
   333  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry, logEntries[1].Entry}}}},
   334  				},
   335  				{
   336  					tenantID: "",
   337  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[2].Entry}}}},
   338  				},
   339  			},
   340  			expectedMetrics: `
   341  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
   342  				# TYPE promtail_sent_entries_total counter
   343  				promtail_sent_entries_total{host="__HOST__"} 3.0
   344  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
   345  				# TYPE promtail_dropped_entries_total counter
   346  				promtail_dropped_entries_total{host="__HOST__"} 0
   347  			`,
   348  		},
   349  		{
   350  			name:                 "shouldn't retry after StopNow()",
   351  			clientBatchSize:      10,
   352  			clientBatchWait:      10 * time.Millisecond,
   353  			clientMaxRetries:     3,
   354  			serverResponseStatus: 429,
   355  			inputEntries:         []api.Entry{logEntries[0]},
   356  			expectedReqs: []receivedReq{
   357  				{
   358  					tenantID: "",
   359  					pushReq:  logproto.PushRequest{Streams: []logproto.Stream{{Labels: "{}", Entries: []logproto.Entry{logEntries[0].Entry}}}},
   360  				},
   361  			},
   362  			expectedMetrics: `
   363  				# HELP promtail_dropped_entries_total Number of log entries dropped because failed to be sent to the ingester after all retries.
   364  				# TYPE promtail_dropped_entries_total counter
   365  				promtail_dropped_entries_total{host="__HOST__"} 1.0
   366  				# HELP promtail_sent_entries_total Number of log entries sent to the ingester.
   367  				# TYPE promtail_sent_entries_total counter
   368  				promtail_sent_entries_total{host="__HOST__"} 0
   369  			`,
   370  		},
   371  	}
   372  
   373  	for _, c := range cases {
   374  		t.Run(c.name, func(t *testing.T) {
   375  			reg := prometheus.NewRegistry()
   376  
   377  			// Create a buffer channel where we do enqueue received requests
   378  			receivedReqsChan := make(chan receivedReq, 10)
   379  
   380  			// Start a local HTTP server
   381  			server := httptest.NewServer(createServerHandler(receivedReqsChan, c.serverResponseStatus))
   382  			require.NotNil(t, server)
   383  			defer server.Close()
   384  
   385  			// Get the URL at which the local test server is listening to
   386  			serverURL := flagext.URLValue{}
   387  			err := serverURL.Set(server.URL)
   388  			require.NoError(t, err)
   389  
   390  			// Instance the client
   391  			cfg := Config{
   392  				URL:            serverURL,
   393  				BatchWait:      c.clientBatchWait,
   394  				BatchSize:      c.clientBatchSize,
   395  				Client:         config.HTTPClientConfig{},
   396  				BackoffConfig:  backoff.Config{MinBackoff: 5 * time.Second, MaxBackoff: 10 * time.Second, MaxRetries: c.clientMaxRetries},
   397  				ExternalLabels: lokiflag.LabelSet{},
   398  				Timeout:        1 * time.Second,
   399  				TenantID:       c.clientTenantID,
   400  			}
   401  			m := NewMetrics(reg, nil)
   402  			cl, err := New(m, cfg, nil, log.NewNopLogger())
   403  			require.NoError(t, err)
   404  
   405  			// Send all the input log entries
   406  			for i, logEntry := range c.inputEntries {
   407  				cl.Chan() <- logEntry
   408  
   409  				if c.inputDelay > 0 && i < len(c.inputEntries)-1 {
   410  					time.Sleep(c.inputDelay)
   411  				}
   412  			}
   413  
   414  			// Wait until the expected push requests are received (with a timeout)
   415  			deadline := time.Now().Add(1 * time.Second)
   416  			for len(receivedReqsChan) < len(c.expectedReqs) && time.Now().Before(deadline) {
   417  				time.Sleep(5 * time.Millisecond)
   418  			}
   419  
   420  			// StopNow should have cancelled client's ctx
   421  			cc := cl.(*client)
   422  			require.NoError(t, cc.ctx.Err())
   423  
   424  			// Stop the client: it waits until the current batch is sent
   425  			cl.StopNow()
   426  			close(receivedReqsChan)
   427  
   428  			require.Error(t, cc.ctx.Err()) // non-nil error if its cancelled.
   429  
   430  			// Get all push requests received on the server side
   431  			receivedReqs := make([]receivedReq, 0)
   432  			for req := range receivedReqsChan {
   433  				receivedReqs = append(receivedReqs, req)
   434  			}
   435  
   436  			// Due to implementation details (maps iteration ordering is random) we just check
   437  			// that the expected requests are equal to the received requests, without checking
   438  			// the exact order which is not guaranteed in case of multi-tenant
   439  			require.ElementsMatch(t, c.expectedReqs, receivedReqs)
   440  
   441  			expectedMetrics := strings.Replace(c.expectedMetrics, "__HOST__", serverURL.Host, -1)
   442  			err = testutil.GatherAndCompare(reg, strings.NewReader(expectedMetrics), "promtail_sent_entries_total", "promtail_dropped_entries_total")
   443  			assert.NoError(t, err)
   444  		})
   445  	}
   446  }
   447  
   448  func createServerHandler(receivedReqsChan chan receivedReq, status int) http.HandlerFunc {
   449  	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
   450  		// Parse the request
   451  		var pushReq logproto.PushRequest
   452  		if err := util.ParseProtoReader(req.Context(), req.Body, int(req.ContentLength), math.MaxInt32, &pushReq, util.RawSnappy); err != nil {
   453  			rw.WriteHeader(500)
   454  			return
   455  		}
   456  
   457  		receivedReqsChan <- receivedReq{
   458  			tenantID: req.Header.Get("X-Scope-OrgID"),
   459  			pushReq:  pushReq,
   460  		}
   461  
   462  		rw.WriteHeader(status)
   463  	})
   464  }
   465  
   466  type RoundTripperFunc func(*http.Request) (*http.Response, error)
   467  
   468  func (r RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
   469  	return r(req)
   470  }
   471  
   472  func Test_Tripperware(t *testing.T) {
   473  	url, err := url.Parse("http://foo.com")
   474  	require.NoError(t, err)
   475  	var called bool
   476  	c, err := NewWithTripperware(metrics, Config{
   477  		URL: flagext.URLValue{URL: url},
   478  	}, nil, log.NewNopLogger(), func(rt http.RoundTripper) http.RoundTripper {
   479  		return RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
   480  			require.Equal(t, r.URL.String(), "http://foo.com")
   481  			called = true
   482  			return &http.Response{
   483  				StatusCode: http.StatusOK,
   484  				Body:       io.NopCloser(strings.NewReader("ok")),
   485  			}, nil
   486  		})
   487  	})
   488  	require.NoError(t, err)
   489  
   490  	c.Chan() <- api.Entry{
   491  		Labels: model.LabelSet{"foo": "bar"},
   492  		Entry:  logproto.Entry{Timestamp: time.Now(), Line: "foo"},
   493  	}
   494  	c.Stop()
   495  	require.True(t, called)
   496  }