github.com/m3db/m3@v1.5.0/src/integration/resources/coordinator_client.go (about) 1 // Copyright (c) 2021 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 resources 22 23 import ( 24 "bytes" 25 "context" 26 "encoding/json" 27 "errors" 28 "fmt" 29 "io/ioutil" 30 "net" 31 "net/http" 32 "path" 33 "strings" 34 "time" 35 36 "github.com/gogo/protobuf/jsonpb" 37 "github.com/gogo/protobuf/proto" 38 "github.com/golang/snappy" 39 "github.com/prometheus/common/model" 40 "go.uber.org/zap" 41 "go.uber.org/zap/zapcore" 42 43 "github.com/m3db/m3/src/cluster/generated/proto/placementpb" 44 "github.com/m3db/m3/src/cluster/placementhandler" 45 "github.com/m3db/m3/src/query/api/v1/handler/graphite" 46 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/native" 47 "github.com/m3db/m3/src/query/api/v1/handler/topic" 48 "github.com/m3db/m3/src/query/api/v1/options" 49 "github.com/m3db/m3/src/query/api/v1/route" 50 "github.com/m3db/m3/src/query/generated/proto/admin" 51 "github.com/m3db/m3/src/query/generated/proto/prompb" 52 "github.com/m3db/m3/src/x/headers" 53 xhttp "github.com/m3db/m3/src/x/net/http" 54 ) 55 56 var errUnknownServiceType = errors.New("unknown service type") 57 58 // RetryFunc is a function that retries the provided 59 // operation until successful. 60 type RetryFunc func(op func() error) error 61 62 // ZapMethod appends the method as a log field. 63 func ZapMethod(s string) zapcore.Field { return zap.String("method", s) } 64 65 // CoordinatorClient is a client use to invoke API calls 66 // on a coordinator 67 type CoordinatorClient struct { 68 client *http.Client 69 httpPort int 70 logger *zap.Logger 71 retryFunc RetryFunc 72 } 73 74 // CoordinatorClientOptions are the options for the CoordinatorClient. 75 type CoordinatorClientOptions struct { 76 Client *http.Client 77 HTTPPort int 78 Logger *zap.Logger 79 RetryFunc RetryFunc 80 } 81 82 // NewCoordinatorClient creates a new CoordinatorClient. 83 func NewCoordinatorClient(opts CoordinatorClientOptions) CoordinatorClient { 84 return CoordinatorClient{ 85 client: opts.Client, 86 httpPort: opts.HTTPPort, 87 logger: opts.Logger, 88 retryFunc: opts.RetryFunc, 89 } 90 } 91 92 func (c *CoordinatorClient) makeURL(resource string) string { 93 return fmt.Sprintf("http://0.0.0.0:%d/%s", c.httpPort, strings.TrimPrefix(resource, "/")) 94 } 95 96 // GetNamespace gets namespaces. 97 func (c *CoordinatorClient) GetNamespace() (admin.NamespaceGetResponse, error) { 98 url := c.makeURL("api/v1/services/m3db/namespace") 99 logger := c.logger.With( 100 ZapMethod("getNamespace"), zap.String("url", url)) 101 102 //nolint:noctx 103 resp, err := c.client.Get(url) 104 if err != nil { 105 logger.Error("failed get", zap.Error(err)) 106 return admin.NamespaceGetResponse{}, err 107 } 108 109 var response admin.NamespaceGetResponse 110 if err := toResponse(resp, &response, logger); err != nil { 111 return admin.NamespaceGetResponse{}, err 112 } 113 114 return response, nil 115 } 116 117 // GetPlacement gets placements. 118 func (c *CoordinatorClient) GetPlacement(opts PlacementRequestOptions) (admin.PlacementGetResponse, error) { 119 var handlerurl string 120 switch opts.Service { 121 case ServiceTypeM3DB: 122 handlerurl = placementhandler.M3DBGetURL 123 case ServiceTypeM3Aggregator: 124 handlerurl = placementhandler.M3AggGetURL 125 case ServiceTypeM3Coordinator: 126 handlerurl = placementhandler.M3CoordinatorGetURL 127 default: 128 return admin.PlacementGetResponse{}, errUnknownServiceType 129 } 130 url := c.makeURL(handlerurl) 131 logger := c.logger.With( 132 ZapMethod("getPlacement"), zap.String("url", url)) 133 134 resp, err := c.makeRequest(logger, url, placementhandler.GetHTTPMethod, nil, placementOptsToMap(opts)) 135 if err != nil { 136 logger.Error("failed get", zap.Error(err)) 137 return admin.PlacementGetResponse{}, err 138 } 139 140 var response admin.PlacementGetResponse 141 if err := toResponse(resp, &response, logger); err != nil { 142 return admin.PlacementGetResponse{}, err 143 } 144 145 return response, nil 146 } 147 148 // InitPlacement initializes placements. 149 func (c *CoordinatorClient) InitPlacement( 150 opts PlacementRequestOptions, 151 initRequest admin.PlacementInitRequest, 152 ) (admin.PlacementGetResponse, error) { 153 var handlerurl string 154 switch opts.Service { 155 case ServiceTypeM3DB: 156 handlerurl = placementhandler.M3DBInitURL 157 case ServiceTypeM3Aggregator: 158 handlerurl = placementhandler.M3AggInitURL 159 case ServiceTypeM3Coordinator: 160 handlerurl = placementhandler.M3CoordinatorInitURL 161 default: 162 return admin.PlacementGetResponse{}, errUnknownServiceType 163 } 164 url := c.makeURL(handlerurl) 165 logger := c.logger.With( 166 ZapMethod("initPlacement"), zap.String("url", url)) 167 168 resp, err := c.makeRequest(logger, url, placementhandler.InitHTTPMethod, &initRequest, placementOptsToMap(opts)) 169 if err != nil { 170 logger.Error("failed init", zap.Error(err)) 171 return admin.PlacementGetResponse{}, err 172 } 173 174 var response admin.PlacementGetResponse 175 if err := toResponse(resp, &response, logger); err != nil { 176 return admin.PlacementGetResponse{}, err 177 } 178 179 return response, nil 180 } 181 182 // DeleteAllPlacements deletes all placements for the specified service. 183 func (c *CoordinatorClient) DeleteAllPlacements(opts PlacementRequestOptions) error { 184 var handlerurl string 185 switch opts.Service { 186 case ServiceTypeM3DB: 187 handlerurl = placementhandler.M3DBDeleteAllURL 188 case ServiceTypeM3Aggregator: 189 handlerurl = placementhandler.M3AggDeleteAllURL 190 case ServiceTypeM3Coordinator: 191 handlerurl = placementhandler.M3CoordinatorDeleteAllURL 192 default: 193 return errUnknownServiceType 194 } 195 url := c.makeURL(handlerurl) 196 logger := c.logger.With( 197 ZapMethod("deleteAllPlacements"), zap.String("url", url)) 198 199 resp, err := c.makeRequest( 200 logger, url, placementhandler.DeleteAllHTTPMethod, nil, placementOptsToMap(opts), 201 ) 202 if err != nil { 203 logger.Error("failed to delete all placements", zap.Error(err)) 204 return err 205 } 206 defer resp.Body.Close() 207 208 if resp.StatusCode/100 != 2 { 209 logger.Error("status code not 2xx", 210 zap.Int("status code", resp.StatusCode), 211 zap.String("status", resp.Status)) 212 return fmt.Errorf("status code %d", resp.StatusCode) 213 } 214 215 logger.Info("placements deleted") 216 217 return nil 218 } 219 220 // WaitForNamespace blocks until the given namespace is enabled. 221 // NB: if the name string is empty, this will instead 222 // check for a successful response. 223 func (c *CoordinatorClient) WaitForNamespace(name string) error { 224 logger := c.logger.With(ZapMethod("waitForNamespace")) 225 return c.retryFunc(func() error { 226 ns, err := c.GetNamespace() 227 if err != nil { 228 return err 229 } 230 231 // If no name passed in, instad just check for success. 232 if len(name) == 0 { 233 return nil 234 } 235 236 nss := ns.GetRegistry().GetNamespaces() 237 _, found := nss[name] 238 if !found { 239 err := fmt.Errorf("no namespace with name %s", name) 240 logger.Error("could not get namespace", zap.Error(err)) 241 return err 242 } 243 244 logger.Info("namespace ready", zap.String("namespace", name)) 245 return nil 246 }) 247 } 248 249 // WaitForInstances blocks until the given instance is available. 250 func (c *CoordinatorClient) WaitForInstances( 251 ids []string, 252 ) error { 253 logger := c.logger.With(ZapMethod("waitForPlacement")) 254 return c.retryFunc(func() error { 255 placement, err := c.GetPlacement(PlacementRequestOptions{Service: ServiceTypeM3DB}) 256 if err != nil { 257 logger.Error("retrying get placement", zap.Error(err)) 258 return err 259 } 260 261 logger.Info("got placement", zap.Any("placement", placement)) 262 instances := placement.GetPlacement().GetInstances() 263 for _, id := range ids { 264 placement, found := instances[id] 265 if !found { 266 err = fmt.Errorf("no instance with id %s", id) 267 logger.Error("could not get instance", zap.Error(err)) 268 return err 269 } 270 271 if pID := placement.GetId(); pID != id { 272 err = fmt.Errorf("id mismatch: instance(%s) != placement(%s)", id, pID) 273 logger.Error("could not get instance", zap.Error(err)) 274 return err 275 } 276 } 277 278 logger.Info("instances ready") 279 return nil 280 }) 281 } 282 283 // WaitForShardsReady waits until all shards gets ready. 284 func (c *CoordinatorClient) WaitForShardsReady() error { 285 logger := c.logger.With(ZapMethod("waitForShards")) 286 return c.retryFunc(func() error { 287 placement, err := c.GetPlacement(PlacementRequestOptions{Service: ServiceTypeM3DB}) 288 if err != nil { 289 logger.Error("retrying get placement", zap.Error(err)) 290 return err 291 } 292 293 for _, instance := range placement.Placement.Instances { 294 for _, shard := range instance.Shards { 295 if shard.State == placementpb.ShardState_INITIALIZING { 296 err = fmt.Errorf("at least shard %d of dbnode %s still initializing", shard.Id, instance.Id) 297 logger.Error("shards still are initializing", zap.Error(err)) 298 return err 299 } 300 } 301 } 302 return nil 303 }) 304 } 305 306 // WaitForClusterReady waits until the cluster is ready to receive reads and writes. 307 func (c *CoordinatorClient) WaitForClusterReady() error { 308 var ( 309 url = c.makeURL("ready") 310 logger = c.logger.With(ZapMethod("waitForClusterReady"), zap.String("url", url)) 311 ) 312 return c.retryFunc(func() error { 313 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 314 if err != nil { 315 logger.Error("failed to create request", zap.Error(err)) 316 return err 317 } 318 319 resp, err := c.client.Do(req) 320 if err != nil { 321 logger.Error("failed checking cluster readiness", zap.Error(err)) 322 return err 323 } 324 defer resp.Body.Close() 325 326 if resp.StatusCode != 200 { 327 err = errors.New("non-200 status code received") 328 329 body, rerr := ioutil.ReadAll(resp.Body) 330 if rerr != nil { 331 logger.Warn("failed parse response body", zap.Error(rerr)) 332 body = []byte("") 333 } 334 335 logger.Error("failed to check cluster readiness", zap.Error(err), 336 zap.String("responseBody", string(body)), 337 ) 338 return err 339 } 340 341 logger.Info("cluster ready to receive reads and writes") 342 343 return nil 344 }) 345 } 346 347 // CreateDatabase creates a database. 348 func (c *CoordinatorClient) CreateDatabase( 349 addRequest admin.DatabaseCreateRequest, 350 ) (admin.DatabaseCreateResponse, error) { 351 url := c.makeURL("api/v1/database/create") 352 logger := c.logger.With( 353 ZapMethod("createDatabase"), zap.String("url", url), 354 zap.String("request", addRequest.String())) 355 356 resp, err := c.makeRequest(logger, url, http.MethodPost, &addRequest, nil) 357 if err != nil { 358 logger.Error("failed post", zap.Error(err)) 359 return admin.DatabaseCreateResponse{}, err 360 } 361 362 var response admin.DatabaseCreateResponse 363 if err := toResponse(resp, &response, logger); err != nil { 364 logger.Error("failed response", zap.Error(err)) 365 return admin.DatabaseCreateResponse{}, err 366 } 367 368 if err = c.setNamespaceReady(addRequest.NamespaceName); err != nil { 369 logger.Error("failed to set namespace to ready state", 370 zap.Error(err), 371 zap.String("namespace", addRequest.NamespaceName), 372 ) 373 return response, err 374 } 375 376 logger.Info("created database") 377 return response, nil 378 } 379 380 // AddNamespace adds a namespace. 381 func (c *CoordinatorClient) AddNamespace( 382 addRequest admin.NamespaceAddRequest, 383 ) (admin.NamespaceGetResponse, error) { 384 url := c.makeURL("api/v1/services/m3db/namespace") 385 logger := c.logger.With( 386 ZapMethod("addNamespace"), zap.String("url", url), 387 zap.String("request", addRequest.String())) 388 389 resp, err := c.makeRequest(logger, url, http.MethodPost, &addRequest, nil) 390 if err != nil { 391 logger.Error("failed post", zap.Error(err)) 392 return admin.NamespaceGetResponse{}, err 393 } 394 395 var response admin.NamespaceGetResponse 396 if err := toResponse(resp, &response, logger); err != nil { 397 return admin.NamespaceGetResponse{}, err 398 } 399 400 if err = c.setNamespaceReady(addRequest.Name); err != nil { 401 logger.Error("failed to set namespace to ready state", zap.Error(err), zap.String("namespace", addRequest.Name)) 402 return response, err 403 } 404 405 return response, nil 406 } 407 408 // UpdateNamespace updates the namespace. 409 func (c *CoordinatorClient) UpdateNamespace( 410 req admin.NamespaceUpdateRequest, 411 ) (admin.NamespaceGetResponse, error) { 412 url := c.makeURL("api/v1/services/m3db/namespace") 413 logger := c.logger.With( 414 ZapMethod("updateNamespace"), zap.String("url", url), 415 zap.String("request", req.String())) 416 417 resp, err := c.makeRequest(logger, url, http.MethodPut, &req, nil) 418 if err != nil { 419 logger.Error("failed to update namespace", zap.Error(err)) 420 return admin.NamespaceGetResponse{}, err 421 } 422 423 var response admin.NamespaceGetResponse 424 if err := toResponse(resp, &response, logger); err != nil { 425 return admin.NamespaceGetResponse{}, err 426 } 427 428 return response, nil 429 } 430 431 func (c *CoordinatorClient) setNamespaceReady(name string) error { 432 url := c.makeURL("api/v1/services/m3db/namespace/ready") 433 logger := c.logger.With( 434 ZapMethod("setNamespaceReady"), zap.String("url", url), 435 zap.String("namespace", name)) 436 437 _, err := c.makeRequest(logger, url, http.MethodPost, // nolint: bodyclose 438 &admin.NamespaceReadyRequest{ 439 Name: name, 440 Force: true, 441 }, nil) 442 return err 443 } 444 445 // DeleteNamespace removes the namespace. 446 func (c *CoordinatorClient) DeleteNamespace(namespaceID string) error { 447 url := c.makeURL("api/v1/services/m3db/namespace/" + namespaceID) 448 logger := c.logger.With(ZapMethod("deleteNamespace"), zap.String("url", url)) 449 450 if _, err := c.makeRequest(logger, url, http.MethodDelete, nil, nil); err != nil { // nolint: bodyclose 451 logger.Error("failed to delete namespace", zap.Error(err)) 452 return err 453 } 454 return nil 455 } 456 457 //nolint:dupl 458 // InitM3msgTopic initializes an m3msg topic 459 func (c *CoordinatorClient) InitM3msgTopic( 460 topicOpts M3msgTopicOptions, 461 initRequest admin.TopicInitRequest, 462 ) (admin.TopicGetResponse, error) { 463 url := c.makeURL(topic.InitURL) 464 logger := c.logger.With( 465 ZapMethod("initM3msgTopic"), 466 zap.String("url", url), 467 zap.String("request", initRequest.String()), 468 zap.String("topic", fmt.Sprintf("%v", topicOpts))) 469 470 resp, err := c.makeRequest(logger, url, topic.InitHTTPMethod, &initRequest, m3msgTopicOptionsToMap(topicOpts)) 471 if err != nil { 472 logger.Error("failed post", zap.Error(err)) 473 return admin.TopicGetResponse{}, err 474 } 475 476 var response admin.TopicGetResponse 477 if err := toResponse(resp, &response, logger); err != nil { 478 logger.Error("failed response", zap.Error(err)) 479 return admin.TopicGetResponse{}, err 480 } 481 482 logger.Info("topic initialized") 483 return response, nil 484 } 485 486 // GetM3msgTopic fetches an m3msg topic 487 func (c *CoordinatorClient) GetM3msgTopic( 488 topicOpts M3msgTopicOptions, 489 ) (admin.TopicGetResponse, error) { 490 url := c.makeURL(topic.GetURL) 491 logger := c.logger.With( 492 ZapMethod("getM3msgTopic"), zap.String("url", url), 493 zap.String("topic", fmt.Sprintf("%v", topicOpts))) 494 495 resp, err := c.makeRequest(logger, url, topic.GetHTTPMethod, nil, m3msgTopicOptionsToMap(topicOpts)) 496 if err != nil { 497 logger.Error("failed get", zap.Error(err)) 498 return admin.TopicGetResponse{}, err 499 } 500 501 var response admin.TopicGetResponse 502 if err := toResponse(resp, &response, logger); err != nil { 503 logger.Error("failed response", zap.Error(err)) 504 return admin.TopicGetResponse{}, err 505 } 506 507 logger.Info("topic get") 508 return response, nil 509 } 510 511 //nolint:dupl 512 // AddM3msgTopicConsumer adds a consumer service to an m3msg topic 513 func (c *CoordinatorClient) AddM3msgTopicConsumer( 514 topicOpts M3msgTopicOptions, 515 addRequest admin.TopicAddRequest, 516 ) (admin.TopicGetResponse, error) { 517 url := c.makeURL(topic.AddURL) 518 logger := c.logger.With( 519 ZapMethod("addM3msgTopicConsumer"), 520 zap.String("url", url), 521 zap.String("request", addRequest.String()), 522 zap.String("topic", fmt.Sprintf("%v", topicOpts))) 523 524 resp, err := c.makeRequest(logger, url, topic.AddHTTPMethod, &addRequest, m3msgTopicOptionsToMap(topicOpts)) 525 if err != nil { 526 logger.Error("failed post", zap.Error(err)) 527 return admin.TopicGetResponse{}, err 528 } 529 530 var response admin.TopicGetResponse 531 if err := toResponse(resp, &response, logger); err != nil { 532 logger.Error("failed response", zap.Error(err)) 533 return admin.TopicGetResponse{}, err 534 } 535 536 logger.Info("topic consumer added") 537 return response, nil 538 } 539 540 func placementOptsToMap(opts PlacementRequestOptions) map[string]string { 541 return map[string]string{ 542 headers.HeaderClusterEnvironmentName: opts.Env, 543 headers.HeaderClusterZoneName: opts.Zone, 544 } 545 } 546 547 func m3msgTopicOptionsToMap(opts M3msgTopicOptions) map[string]string { 548 return map[string]string{ 549 headers.HeaderClusterEnvironmentName: opts.Env, 550 headers.HeaderClusterZoneName: opts.Zone, 551 topic.HeaderTopicName: opts.TopicName, 552 } 553 } 554 555 // WriteCarbon writes a carbon metric datapoint at a given time. 556 func (c *CoordinatorClient) WriteCarbon( 557 url string, metric string, v float64, t time.Time, 558 ) error { 559 logger := c.logger.With( 560 ZapMethod("writeCarbon"), zap.String("url", url), 561 zap.String("at time", time.Now().String()), 562 zap.String("at ts", t.String())) 563 564 con, err := net.Dial("tcp", url) 565 if err != nil { 566 logger.Error("could not dial", zap.Error(err)) 567 return err 568 } 569 570 write := fmt.Sprintf("%s %f %d", metric, v, t.Unix()) 571 logger.Info("writing", zap.String("metric", write)) 572 n, err := con.Write([]byte(write)) 573 if err != nil { 574 logger.Error("could not write", zap.Error(err)) 575 } 576 577 if n != len(write) { 578 err := fmt.Errorf("wrote %d, wanted %d", n, len(write)) 579 logger.Error("write failure", zap.Error(err)) 580 return err 581 } 582 583 logger.Info("write success", zap.Int("bytes written", n)) 584 return con.Close() 585 } 586 587 // WriteProm writes a prometheus metric. Takes tags/labels as a map for convenience. 588 func (c *CoordinatorClient) WriteProm( 589 name string, 590 tags map[string]string, 591 samples []prompb.Sample, 592 headers Headers, 593 ) error { 594 labels := make([]prompb.Label, 0, len(tags)+1) 595 596 labels = append(labels, prompb.Label{ 597 Name: []byte(model.MetricNameLabel), 598 Value: []byte(name), 599 }) 600 601 for tag, value := range tags { 602 labels = append(labels, prompb.Label{ 603 Name: []byte(tag), 604 Value: []byte(value), 605 }) 606 } 607 608 writeRequest := prompb.WriteRequest{ 609 Timeseries: []prompb.TimeSeries{ 610 { 611 Labels: labels, 612 Samples: samples, 613 }, 614 }, 615 } 616 617 return c.WritePromWithRequest(writeRequest, headers) 618 } 619 620 // WritePromWithRequest executes a prometheus write request. Allows you to 621 // provide the request directly which is useful for batch metric requests. 622 func (c *CoordinatorClient) WritePromWithRequest(writeRequest prompb.WriteRequest, headers Headers) error { 623 url := c.makeURL("api/v1/prom/remote/write") 624 625 logger := c.logger.With( 626 ZapMethod("writeProm"), zap.String("url", url), 627 zap.String("request", writeRequest.String())) 628 629 body, err := proto.Marshal(&writeRequest) 630 if err != nil { 631 logger.Error("failed marshaling request message", zap.Error(err)) 632 return err 633 } 634 data := bytes.NewBuffer(snappy.Encode(nil, body)) 635 636 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, data) 637 if err != nil { 638 logger.Error("failed constructing request", zap.Error(err)) 639 return err 640 } 641 for key, vals := range headers { 642 for _, val := range vals { 643 req.Header.Add(key, val) 644 } 645 } 646 req.Header.Add(xhttp.HeaderContentType, xhttp.ContentTypeProtobuf) 647 648 resp, err := c.client.Do(req) 649 if err != nil { 650 logger.Error("failed making a request", zap.Error(err)) 651 return err 652 } 653 defer resp.Body.Close() 654 if resp.StatusCode < 200 || resp.StatusCode > 299 { 655 logger.Error("status code not 2xx", 656 zap.Int("status code", resp.StatusCode), 657 zap.String("status", resp.Status)) 658 return fmt.Errorf("status code %d", resp.StatusCode) 659 } 660 661 return nil 662 } 663 664 func (c *CoordinatorClient) makeRequest( 665 logger *zap.Logger, 666 url string, 667 method string, 668 body proto.Message, 669 header map[string]string, 670 ) (*http.Response, error) { 671 data := bytes.NewBuffer(nil) 672 if body != nil { 673 if err := (&jsonpb.Marshaler{}).Marshal(data, body); err != nil { 674 logger.Error("failed to marshal", zap.Error(err)) 675 676 return nil, fmt.Errorf("failed to marshal: %w", err) 677 } 678 } 679 680 req, err := http.NewRequestWithContext(context.Background(), method, url, data) 681 if err != nil { 682 logger.Error("failed to construct request", zap.Error(err)) 683 684 return nil, fmt.Errorf("failed to construct request: %w", err) 685 } 686 687 req.Header.Add(xhttp.HeaderContentType, xhttp.ContentTypeJSON) 688 for k, v := range header { 689 req.Header.Add(k, v) 690 } 691 692 return c.client.Do(req) 693 } 694 695 // ApplyKVUpdate applies a KV update. 696 func (c *CoordinatorClient) ApplyKVUpdate(update string) error { 697 url := c.makeURL("api/v1/kvstore") 698 699 logger := c.logger.With( 700 ZapMethod("ApplyKVUpdate"), zap.String("url", url), 701 zap.String("update", update)) 702 703 data := bytes.NewBuffer([]byte(update)) 704 req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, data) 705 if err != nil { 706 logger.Error("failed to construct request", zap.Error(err)) 707 return fmt.Errorf("failed to construct request: %w", err) 708 } 709 710 req.Header.Add(xhttp.HeaderContentType, xhttp.ContentTypeJSON) 711 712 resp, err := c.client.Do(req) 713 if err != nil { 714 logger.Error("failed to apply request", zap.Error(err)) 715 return fmt.Errorf("failed to apply request: %w", err) 716 } 717 718 bs, err := ioutil.ReadAll(resp.Body) 719 if err != nil { 720 logger.Error("failed to read body", zap.Error(err)) 721 return fmt.Errorf("failed to read body: %w", err) 722 } 723 724 logger.Info("applied KV update", zap.ByteString("response", bs)) 725 _ = resp.Body.Close() 726 return nil 727 } 728 729 func (c *CoordinatorClient) query( 730 verifier ResponseVerifier, query string, headers map[string][]string, 731 ) error { 732 url := c.makeURL(query) 733 logger := c.logger.With( 734 ZapMethod("query"), zap.String("url", url), zap.Any("headers", headers)) 735 logger.Info("running") 736 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 737 if err != nil { 738 return err 739 } 740 741 if headers != nil { 742 req.Header = headers 743 } 744 745 resp, err := c.client.Do(req) 746 if err != nil { 747 logger.Error("failed get", zap.Error(err)) 748 return err 749 } 750 751 defer resp.Body.Close() 752 b, err := ioutil.ReadAll(resp.Body) 753 754 return verifier(resp.StatusCode, resp.Header, string(b), err) 755 } 756 757 // InstantQuery runs an instant query with provided headers 758 func (c *CoordinatorClient) InstantQuery(req QueryRequest, headers Headers) (model.Vector, error) { 759 return c.instantQuery(req, route.QueryURL, headers) 760 } 761 762 // InstantQueryWithEngine runs an instant query with provided headers and the specified 763 // query engine. 764 func (c *CoordinatorClient) InstantQueryWithEngine( 765 req QueryRequest, 766 engine options.QueryEngine, 767 headers Headers, 768 ) (model.Vector, error) { 769 if engine == options.M3QueryEngine { 770 return c.instantQuery(req, native.M3QueryReadInstantURL, headers) 771 } else if engine == options.PrometheusEngine { 772 return c.instantQuery(req, native.PrometheusReadInstantURL, headers) 773 } 774 return nil, fmt.Errorf("unknown query engine: %s", engine) 775 } 776 777 func (c *CoordinatorClient) instantQuery( 778 req QueryRequest, 779 queryRoute string, 780 headers Headers, 781 ) (model.Vector, error) { 782 queryStr := fmt.Sprintf("%s?query=%s", queryRoute, req.Query) 783 if req.Time != nil { 784 queryStr = fmt.Sprintf("%s&time=%d", queryStr, req.Time.Unix()) 785 } 786 787 resp, err := c.runQuery(queryStr, headers) 788 if err != nil { 789 return nil, err 790 } 791 792 var parsedResp jsonInstantQueryResponse 793 if err := json.Unmarshal([]byte(resp), &parsedResp); err != nil { 794 return nil, err 795 } 796 797 return parsedResp.Data.Result, nil 798 } 799 800 type jsonInstantQueryResponse struct { 801 Status string 802 Data vectorResult 803 } 804 805 type vectorResult struct { 806 ResultType model.ValueType 807 Result model.Vector 808 } 809 810 // RangeQuery runs a range query with provided headers 811 func (c *CoordinatorClient) RangeQuery( 812 req RangeQueryRequest, 813 headers Headers, 814 ) (model.Matrix, error) { 815 return c.rangeQuery(req, route.QueryRangeURL, headers) 816 } 817 818 // RangeQueryWithEngine runs a range query with provided headers and the specified 819 // query engine. 820 func (c *CoordinatorClient) RangeQueryWithEngine( 821 req RangeQueryRequest, 822 engine options.QueryEngine, 823 headers Headers, 824 ) (model.Matrix, error) { 825 if engine == options.M3QueryEngine { 826 return c.rangeQuery(req, native.M3QueryReadURL, headers) 827 } else if engine == options.PrometheusEngine { 828 return c.rangeQuery(req, native.PrometheusReadURL, headers) 829 } 830 return nil, fmt.Errorf("unknown query engine: %s", engine) 831 } 832 833 func (c *CoordinatorClient) rangeQuery( 834 req RangeQueryRequest, 835 queryRoute string, 836 headers Headers, 837 ) (model.Matrix, error) { 838 if req.Step == 0 { 839 req.Step = 15 * time.Second // default step is 15 seconds. 840 } 841 queryStr := fmt.Sprintf( 842 "%s?query=%s&start=%d&end=%d&step=%f", 843 queryRoute, req.Query, 844 req.Start.Unix(), 845 req.End.Unix(), 846 req.Step.Seconds(), 847 ) 848 849 resp, err := c.runQuery(queryStr, headers) 850 if err != nil { 851 return nil, err 852 } 853 854 var parsedResp jsonRangeQueryResponse 855 if err := json.Unmarshal([]byte(resp), &parsedResp); err != nil { 856 return nil, err 857 } 858 859 return parsedResp.Data.Result, nil 860 } 861 862 // LabelNames return matching label names based on the request. 863 func (c *CoordinatorClient) LabelNames( 864 req LabelNamesRequest, 865 headers Headers, 866 ) (model.LabelNames, error) { 867 urlPathAndQuery := fmt.Sprintf("%s?%s", route.LabelNamesURL, req.String()) 868 resp, err := c.runQuery(urlPathAndQuery, headers) 869 if err != nil { 870 return nil, err 871 } 872 873 var parsedResp labelResponse 874 if err := json.Unmarshal([]byte(resp), &parsedResp); err != nil { 875 return nil, err 876 } 877 878 labelNames := make(model.LabelNames, 0, len(parsedResp.Data)) 879 for _, label := range parsedResp.Data { 880 labelNames = append(labelNames, model.LabelName(label)) 881 } 882 883 return labelNames, nil 884 } 885 886 // LabelValues return matching label values based on the request. 887 func (c *CoordinatorClient) LabelValues( 888 req LabelValuesRequest, 889 headers Headers, 890 ) (model.LabelValues, error) { 891 urlPathAndQuery := fmt.Sprintf("%s?%s", 892 path.Join(route.Prefix, "label", req.LabelName, "values"), 893 req.String()) 894 resp, err := c.runQuery(urlPathAndQuery, headers) 895 if err != nil { 896 return nil, err 897 } 898 899 var parsedResp labelResponse 900 if err := json.Unmarshal([]byte(resp), &parsedResp); err != nil { 901 return nil, err 902 } 903 904 labelValues := make(model.LabelValues, 0, len(parsedResp.Data)) 905 for _, label := range parsedResp.Data { 906 labelValues = append(labelValues, model.LabelValue(label)) 907 } 908 909 return labelValues, nil 910 } 911 912 // Series returns matching series based on the request. 913 func (c *CoordinatorClient) Series( 914 req SeriesRequest, 915 headers Headers, 916 ) ([]model.Metric, error) { 917 urlPathAndQuery := fmt.Sprintf("%s?%s", route.SeriesMatchURL, req.String()) 918 resp, err := c.runQuery(urlPathAndQuery, headers) 919 if err != nil { 920 return nil, err 921 } 922 923 var parsedResp seriesResponse 924 if err := json.Unmarshal([]byte(resp), &parsedResp); err != nil { 925 return nil, err 926 } 927 928 series := make([]model.Metric, 0, len(parsedResp.Data)) 929 for _, labels := range parsedResp.Data { 930 labelSet := make(model.LabelSet) 931 for name, val := range labels { 932 labelSet[model.LabelName(name)] = model.LabelValue(val) 933 } 934 series = append(series, model.Metric(labelSet)) 935 } 936 937 return series, nil 938 } 939 940 type jsonRangeQueryResponse struct { 941 Status string 942 Data matrixResult 943 } 944 945 type matrixResult struct { 946 ResultType model.ValueType 947 Result model.Matrix 948 } 949 950 type labelResponse struct { 951 Status string 952 Data []string 953 } 954 955 type seriesResponse struct { 956 Status string 957 Data []map[string]string 958 } 959 960 func (c *CoordinatorClient) runQuery( 961 query string, headers map[string][]string, 962 ) (string, error) { 963 url := c.makeURL(query) 964 logger := c.logger.With( 965 ZapMethod("query"), zap.String("url", url), zap.Any("headers", headers)) 966 logger.Info("running") 967 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 968 if err != nil { 969 return "", err 970 } 971 972 if headers != nil { 973 req.Header = headers 974 } 975 976 resp, err := c.client.Do(req) 977 if err != nil { 978 logger.Error("failed get", zap.Error(err)) 979 return "", err 980 } 981 982 defer resp.Body.Close() 983 b, err := ioutil.ReadAll(resp.Body) 984 985 if status := resp.StatusCode; status != http.StatusOK { 986 return "", fmt.Errorf("query response status not OK, received %v. error=%v", 987 status, string(b)) 988 } 989 990 if contentType, ok := resp.Header["Content-Type"]; !ok { 991 return "", fmt.Errorf("missing Content-Type header") 992 } else if len(contentType) != 1 || contentType[0] != "application/json" { //nolint:goconst 993 return "", fmt.Errorf("expected json content type, got %v", contentType) 994 } 995 996 return string(b), err 997 } 998 999 // RunQuery runs the given query with a given verification function. 1000 func (c *CoordinatorClient) RunQuery( 1001 verifier ResponseVerifier, query string, headers map[string][]string, 1002 ) error { 1003 logger := c.logger.With(ZapMethod("runQuery"), 1004 zap.String("query", query)) 1005 err := c.retryFunc(func() error { 1006 err := c.query(verifier, query, headers) 1007 if err != nil { 1008 logger.Info("retrying", zap.Error(err)) 1009 } 1010 1011 return err 1012 }) 1013 if err != nil { 1014 logger.Error("failed run", zap.Error(err)) 1015 } 1016 1017 return err 1018 } 1019 1020 func toResponse( 1021 resp *http.Response, 1022 response proto.Message, 1023 logger *zap.Logger, 1024 ) error { 1025 b, err := ioutil.ReadAll(resp.Body) 1026 if err != nil { 1027 logger.Error("could not read body", zap.Error(err)) 1028 return err 1029 } 1030 1031 defer resp.Body.Close() 1032 if resp.StatusCode/100 != 2 { 1033 logger.Error("status code not 2xx", 1034 zap.Int("status code", resp.StatusCode), 1035 zap.String("status", resp.Status)) 1036 return fmt.Errorf("status code %d", resp.StatusCode) 1037 } 1038 1039 err = jsonpb.Unmarshal(bytes.NewReader(b), response) 1040 if err != nil { 1041 logger.Error("unable to unmarshal response", 1042 zap.Error(err), 1043 zap.Any("response", response)) 1044 return err 1045 } 1046 1047 return nil 1048 } 1049 1050 // GraphiteQuery retrieves graphite raw data. 1051 func (c *CoordinatorClient) GraphiteQuery( 1052 graphiteReq GraphiteQueryRequest, 1053 ) ([]Datapoint, error) { 1054 if graphiteReq.From.IsZero() { 1055 graphiteReq.From = time.Now().Add(-24 * time.Hour) 1056 } 1057 if graphiteReq.Until.IsZero() { 1058 graphiteReq.Until = time.Now() 1059 } 1060 1061 queryStr := fmt.Sprintf( 1062 "%s?target=%s&from=%d&until=%d", 1063 graphite.ReadURL, graphiteReq.Target, 1064 graphiteReq.From.Unix(), 1065 graphiteReq.Until.Unix(), 1066 ) 1067 1068 url := c.makeURL(queryStr) 1069 logger := c.logger.With( 1070 ZapMethod("graphiteQuery"), zap.String("url", url)) 1071 logger.Info("running") 1072 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 1073 if err != nil { 1074 return nil, err 1075 } 1076 1077 resp, err := c.client.Do(req) 1078 if err != nil { 1079 logger.Error("failed get", zap.Error(err)) 1080 return nil, err 1081 } 1082 1083 defer resp.Body.Close() 1084 b, err := ioutil.ReadAll(resp.Body) 1085 if err != nil { 1086 return nil, err 1087 } 1088 1089 if status := resp.StatusCode; status != http.StatusOK { 1090 return nil, fmt.Errorf("query response status not OK, received %v %s", status, resp.Status) 1091 } 1092 1093 var parsedResp jsonGraphiteQueryResponse 1094 if err := json.Unmarshal(b, &parsedResp); err != nil { 1095 return nil, err 1096 } 1097 1098 if len(parsedResp) == 0 { 1099 return nil, nil 1100 } 1101 1102 results := make([]Datapoint, 0, len(parsedResp[0].Datapoints)) 1103 for _, dp := range parsedResp[0].Datapoints { 1104 if len(dp) != 2 { 1105 return nil, fmt.Errorf("failed to parse response: %s", string(b)) 1106 } 1107 1108 results = append(results, Datapoint{ 1109 Value: dp[0], 1110 Timestamp: int64(*dp[1]), 1111 }) 1112 } 1113 1114 return results, nil 1115 } 1116 1117 type jsonGraphiteQueryResponse []series 1118 1119 type series struct { 1120 Target string 1121 Datapoints []tuple 1122 StepSizeMs int 1123 } 1124 1125 type tuple []*float64