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 }