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 }