github.com/thanos-io/thanos@v0.32.5/pkg/exemplars/proxy_test.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package exemplars
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"reflect"
    12  	"sync"
    13  	"testing"
    14  
    15  	"github.com/go-kit/log"
    16  	"github.com/pkg/errors"
    17  	"github.com/prometheus/prometheus/model/labels"
    18  	"github.com/prometheus/prometheus/promql/parser"
    19  	"go.uber.org/atomic"
    20  	"google.golang.org/grpc"
    21  	"google.golang.org/grpc/codes"
    22  	"google.golang.org/grpc/status"
    23  
    24  	"github.com/efficientgo/core/testutil"
    25  	"github.com/thanos-io/thanos/pkg/exemplars/exemplarspb"
    26  	"github.com/thanos-io/thanos/pkg/store/labelpb"
    27  	"github.com/thanos-io/thanos/pkg/store/storepb"
    28  )
    29  
    30  type testExemplarClient struct {
    31  	grpc.ClientStream
    32  	exemplarErr, recvErr error
    33  	response             *exemplarspb.ExemplarsResponse
    34  	sentResponse         atomic.Bool
    35  }
    36  
    37  func (t *testExemplarClient) String() string {
    38  	return "test"
    39  }
    40  
    41  func (t *testExemplarClient) Recv() (*exemplarspb.ExemplarsResponse, error) {
    42  	// A simulation of underlying grpc Recv behavior as per https://github.com/grpc/grpc-go/blob/7f2581f910fc21497091c4109b56d310276fc943/stream.go#L117-L125.
    43  	if t.recvErr != nil {
    44  		return nil, t.recvErr
    45  	}
    46  
    47  	if t.sentResponse.Load() {
    48  		return nil, io.EOF
    49  	}
    50  	t.sentResponse.Store(true)
    51  
    52  	return t.response, nil
    53  }
    54  
    55  func (t *testExemplarClient) Exemplars(ctx context.Context, in *exemplarspb.ExemplarsRequest, opts ...grpc.CallOption) (exemplarspb.Exemplars_ExemplarsClient, error) {
    56  	expr, err := parser.ParseExpr(in.Query)
    57  	if err != nil {
    58  		return nil, status.Error(codes.Internal, err.Error())
    59  	}
    60  
    61  	if err := t.assertUniqueMatchers(expr); err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	return t, t.exemplarErr
    66  }
    67  
    68  func (t *testExemplarClient) assertUniqueMatchers(expr parser.Expr) error {
    69  	matchersList := parser.ExtractSelectors(expr)
    70  	for _, matchers := range matchersList {
    71  		matcherSet := make(map[string]struct{})
    72  		for _, matcher := range matchers {
    73  			if _, ok := matcherSet[matcher.String()]; ok {
    74  				return status.Error(codes.Internal, fmt.Sprintf("duplicate matcher set found %s", matcher))
    75  			}
    76  			matcherSet[matcher.String()] = struct{}{}
    77  		}
    78  	}
    79  
    80  	return nil
    81  }
    82  
    83  var _ exemplarspb.ExemplarsClient = &testExemplarClient{}
    84  
    85  type testExemplarServer struct {
    86  	grpc.ServerStream
    87  	sendErr   error
    88  	responses []*exemplarspb.ExemplarsResponse
    89  	mu        sync.Mutex
    90  }
    91  
    92  func (t *testExemplarServer) String() string {
    93  	return "test"
    94  }
    95  
    96  func (t *testExemplarServer) Send(response *exemplarspb.ExemplarsResponse) error {
    97  	if t.sendErr != nil {
    98  		return t.sendErr
    99  	}
   100  	t.mu.Lock()
   101  	defer t.mu.Unlock()
   102  	t.responses = append(t.responses, response)
   103  	return nil
   104  }
   105  
   106  func (t *testExemplarServer) Context() context.Context {
   107  	return context.Background()
   108  }
   109  
   110  func TestProxy(t *testing.T) {
   111  	logger := log.NewLogfmtLogger(os.Stderr)
   112  
   113  	for _, tc := range []struct {
   114  		name           string
   115  		request        *exemplarspb.ExemplarsRequest
   116  		clients        []*exemplarspb.ExemplarStore
   117  		server         *testExemplarServer
   118  		selectorLabels labels.Labels
   119  		wantResponses  []*exemplarspb.ExemplarsResponse
   120  		wantError      error
   121  	}{
   122  		{
   123  			name: "proxy success",
   124  			request: &exemplarspb.ExemplarsRequest{
   125  				Query:                   `http_request_duration_bucket`,
   126  				PartialResponseStrategy: storepb.PartialResponseStrategy_WARN,
   127  			},
   128  			clients: []*exemplarspb.ExemplarStore{
   129  				{
   130  					ExemplarsClient: &testExemplarClient{
   131  						response: exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   132  							SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"__name__": "http_request_duration_bucket"}))},
   133  							Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   134  						}),
   135  					},
   136  					LabelSets: []labels.Labels{
   137  						labels.FromMap(map[string]string{"cluster": "A"}),
   138  						labels.FromMap(map[string]string{"cluster": "B"}),
   139  					},
   140  				},
   141  			},
   142  			server: &testExemplarServer{},
   143  			wantResponses: []*exemplarspb.ExemplarsResponse{
   144  				exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   145  					SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"__name__": "http_request_duration_bucket"}))},
   146  					Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   147  				}),
   148  			},
   149  		},
   150  		{
   151  			name: "proxy success with multiple selectors",
   152  			request: &exemplarspb.ExemplarsRequest{
   153  				Query:                   `http_request_duration_bucket{region="us-east1"} / on (region) group_left() http_request_duration_bucket`,
   154  				PartialResponseStrategy: storepb.PartialResponseStrategy_WARN,
   155  			},
   156  			clients: []*exemplarspb.ExemplarStore{
   157  				{
   158  					ExemplarsClient: &testExemplarClient{
   159  						response: exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   160  							SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"__name__": "http_request_duration_bucket"}))},
   161  							Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   162  						}),
   163  					},
   164  					LabelSets: []labels.Labels{
   165  						labels.FromMap(map[string]string{"cluster": "A"}),
   166  						labels.FromMap(map[string]string{"cluster": "B"}),
   167  					},
   168  				},
   169  			},
   170  			server: &testExemplarServer{},
   171  			wantResponses: []*exemplarspb.ExemplarsResponse{
   172  				exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   173  					SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"__name__": "http_request_duration_bucket"}))},
   174  					Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   175  				}),
   176  			},
   177  		},
   178  		{
   179  			name: "warning proxy success",
   180  			request: &exemplarspb.ExemplarsRequest{
   181  				Query:                   `http_request_duration_bucket`,
   182  				PartialResponseStrategy: storepb.PartialResponseStrategy_WARN,
   183  			},
   184  			clients: []*exemplarspb.ExemplarStore{
   185  				{
   186  					ExemplarsClient: &testExemplarClient{
   187  						response: exemplarspb.NewWarningExemplarsResponse(errors.New("warning from client")),
   188  					},
   189  					LabelSets: []labels.Labels{labels.FromMap(map[string]string{"cluster": "A"})},
   190  				},
   191  			},
   192  			server: &testExemplarServer{},
   193  			wantResponses: []*exemplarspb.ExemplarsResponse{
   194  				exemplarspb.NewWarningExemplarsResponse(errors.New("warning from client")),
   195  			},
   196  		},
   197  		{
   198  			// The input query external label doesn't match with the current querier, return null.
   199  			name: "external label doesn't match selector labels",
   200  			request: &exemplarspb.ExemplarsRequest{
   201  				Query:                   `http_request_duration_bucket{query="foo"}`,
   202  				PartialResponseStrategy: storepb.PartialResponseStrategy_WARN,
   203  			},
   204  			clients: []*exemplarspb.ExemplarStore{
   205  				{
   206  					ExemplarsClient: &testExemplarClient{
   207  						response: exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   208  							SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"__name__": "http_request_duration_bucket"}))},
   209  							Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   210  						}),
   211  					},
   212  					LabelSets: []labels.Labels{labels.FromMap(map[string]string{"cluster": "A"})},
   213  				},
   214  			},
   215  			selectorLabels: labels.FromMap(map[string]string{"query": "bar"}),
   216  			server:         &testExemplarServer{},
   217  			wantResponses:  nil,
   218  		},
   219  		{
   220  			// The input query external label matches with the current querier.
   221  			name: "external label matches selector labels",
   222  			request: &exemplarspb.ExemplarsRequest{
   223  				Query:                   `http_request_duration_bucket{query="foo"}`,
   224  				PartialResponseStrategy: storepb.PartialResponseStrategy_WARN,
   225  			},
   226  			clients: []*exemplarspb.ExemplarStore{
   227  				{
   228  					ExemplarsClient: &testExemplarClient{
   229  						response: exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   230  							SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"__name__": "http_request_duration_bucket"}))},
   231  							Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   232  						}),
   233  					},
   234  					LabelSets: []labels.Labels{labels.FromMap(map[string]string{"cluster": "A"})},
   235  				},
   236  			},
   237  			selectorLabels: labels.FromMap(map[string]string{"query": "foo"}),
   238  			server:         &testExemplarServer{},
   239  			wantResponses: []*exemplarspb.ExemplarsResponse{
   240  				exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   241  					SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"__name__": "http_request_duration_bucket"}))},
   242  					Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   243  				}),
   244  			},
   245  		},
   246  		{
   247  			name: "external label selects stores",
   248  			request: &exemplarspb.ExemplarsRequest{
   249  				Query:                   `http_request_duration_bucket{cluster="A"}`,
   250  				PartialResponseStrategy: storepb.PartialResponseStrategy_WARN,
   251  			},
   252  			clients: []*exemplarspb.ExemplarStore{
   253  				{
   254  					ExemplarsClient: &testExemplarClient{
   255  						response: exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   256  							SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"foo": "bar"}))},
   257  							Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   258  						}),
   259  					},
   260  					LabelSets: []labels.Labels{labels.FromMap(map[string]string{"cluster": "A"})},
   261  				},
   262  				{
   263  					ExemplarsClient: &testExemplarClient{
   264  						response: exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   265  							SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"foo": "baz"}))},
   266  							Exemplars:    []*exemplarspb.Exemplar{{Value: 2}},
   267  						}),
   268  					},
   269  					LabelSets: []labels.Labels{labels.FromMap(map[string]string{"cluster": "B"})},
   270  				},
   271  			},
   272  			server: &testExemplarServer{},
   273  			wantResponses: []*exemplarspb.ExemplarsResponse{
   274  				exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   275  					SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"foo": "bar"}))},
   276  					Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   277  				}),
   278  			},
   279  		},
   280  		{
   281  			name: "external label matches different stores",
   282  			request: &exemplarspb.ExemplarsRequest{
   283  				Query:                   `http_request_duration_bucket{cluster="A"} + http_request_duration_bucket{cluster="B"}`,
   284  				PartialResponseStrategy: storepb.PartialResponseStrategy_WARN,
   285  			},
   286  			clients: []*exemplarspb.ExemplarStore{
   287  				{
   288  					ExemplarsClient: &testExemplarClient{
   289  						response: exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   290  							SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"foo": "bar"}))},
   291  							Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   292  						}),
   293  					},
   294  					LabelSets: []labels.Labels{labels.FromMap(map[string]string{"cluster": "A"})},
   295  				},
   296  				{
   297  					ExemplarsClient: &testExemplarClient{
   298  						response: exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   299  							SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"foo": "baz"}))},
   300  							Exemplars:    []*exemplarspb.Exemplar{{Value: 2}},
   301  						}),
   302  					},
   303  					LabelSets: []labels.Labels{labels.FromMap(map[string]string{"cluster": "B"})},
   304  				},
   305  			},
   306  			server: &testExemplarServer{},
   307  			wantResponses: []*exemplarspb.ExemplarsResponse{
   308  				exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   309  					SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"foo": "bar"}))},
   310  					Exemplars:    []*exemplarspb.Exemplar{{Value: 1}},
   311  				}),
   312  				exemplarspb.NewExemplarsResponse(&exemplarspb.ExemplarData{
   313  					SeriesLabels: labelpb.ZLabelSet{Labels: labelpb.ZLabelsFromPromLabels(labels.FromMap(map[string]string{"foo": "baz"}))},
   314  					Exemplars:    []*exemplarspb.Exemplar{{Value: 2}},
   315  				}),
   316  			},
   317  		},
   318  	} {
   319  		t.Run(tc.name, func(t *testing.T) {
   320  			p := NewProxy(logger, func() []*exemplarspb.ExemplarStore {
   321  				return tc.clients
   322  			}, tc.selectorLabels)
   323  
   324  			err := p.Exemplars(tc.request, tc.server)
   325  			gotErr := "<nil>"
   326  			if err != nil {
   327  				gotErr = err.Error()
   328  			}
   329  			wantErr := "<nil>"
   330  			if tc.wantError != nil {
   331  				wantErr = tc.wantError.Error()
   332  			}
   333  
   334  			if gotErr != wantErr {
   335  				t.Errorf("want error %q, got %q", wantErr, gotErr)
   336  			}
   337  
   338  			testutil.Equals(t, len(tc.wantResponses), len(tc.server.responses))
   339  
   340  			// Actual responses are unordered so we search
   341  			// for matched response for simplicity.
   342  		Outer:
   343  			for _, exp := range tc.wantResponses {
   344  				for _, res := range tc.server.responses {
   345  					if reflect.DeepEqual(exp, res) {
   346  						continue Outer
   347  					}
   348  				}
   349  				t.Errorf("miss expected response %v", exp)
   350  			}
   351  		})
   352  	}
   353  }
   354  
   355  // TestProxyDataRace find the concurrent data race bug ( go test -race -run TestProxyDataRace -v ).
   356  func TestProxyDataRace(t *testing.T) {
   357  	logger := log.NewLogfmtLogger(os.Stderr)
   358  	p := NewProxy(logger, func() []*exemplarspb.ExemplarStore {
   359  		es := &exemplarspb.ExemplarStore{
   360  			ExemplarsClient: &testExemplarClient{
   361  				recvErr: errors.New("err"),
   362  			},
   363  			LabelSets: []labels.Labels{labels.FromMap(map[string]string{"cluster": "A"})},
   364  		}
   365  		size := 100
   366  		endpoints := make([]*exemplarspb.ExemplarStore, 0, size)
   367  		for i := 0; i < size; i++ {
   368  			endpoints = append(endpoints, es)
   369  		}
   370  		return endpoints
   371  	}, labels.FromMap(map[string]string{"query": "foo"}))
   372  	req := &exemplarspb.ExemplarsRequest{
   373  		Query:                   `http_request_duration_bucket{query="foo"}`,
   374  		PartialResponseStrategy: storepb.PartialResponseStrategy_WARN,
   375  	}
   376  	s := &exemplarsServer{
   377  		ctx: context.Background(),
   378  	}
   379  	_ = p.Exemplars(req, s)
   380  }