github.com/thanos-io/thanos@v0.32.5/pkg/promclient/promclient.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 // Package promclient offers helper client function for various API endpoints. 5 6 package promclient 7 8 import ( 9 "context" 10 "encoding/json" 11 "fmt" 12 "io" 13 "net/http" 14 "net/url" 15 "os" 16 "path" 17 "path/filepath" 18 "sort" 19 "strconv" 20 "strings" 21 "time" 22 23 "github.com/go-kit/log" 24 "github.com/go-kit/log/level" 25 "github.com/gogo/status" 26 "github.com/pkg/errors" 27 "github.com/prometheus/common/model" 28 "github.com/prometheus/prometheus/config" 29 "github.com/prometheus/prometheus/model/labels" 30 "github.com/prometheus/prometheus/model/timestamp" 31 "github.com/prometheus/prometheus/promql" 32 "github.com/prometheus/prometheus/promql/parser" 33 "google.golang.org/grpc/codes" 34 "gopkg.in/yaml.v2" 35 36 "github.com/thanos-io/thanos/pkg/exemplars/exemplarspb" 37 "github.com/thanos-io/thanos/pkg/httpconfig" 38 "github.com/thanos-io/thanos/pkg/metadata/metadatapb" 39 "github.com/thanos-io/thanos/pkg/rules/rulespb" 40 "github.com/thanos-io/thanos/pkg/runutil" 41 "github.com/thanos-io/thanos/pkg/store/storepb" 42 "github.com/thanos-io/thanos/pkg/targets/targetspb" 43 "github.com/thanos-io/thanos/pkg/tracing" 44 ) 45 46 var ( 47 ErrFlagEndpointNotFound = errors.New("no flag endpoint found") 48 49 statusToCode = map[int]codes.Code{ 50 http.StatusBadRequest: codes.InvalidArgument, 51 http.StatusNotFound: codes.NotFound, 52 http.StatusUnprocessableEntity: codes.Internal, 53 http.StatusServiceUnavailable: codes.Unavailable, 54 http.StatusInternalServerError: codes.Internal, 55 } 56 ) 57 58 const ( 59 SUCCESS = "success" 60 ) 61 62 // HTTPClient sends an HTTP request and returns the response. 63 type HTTPClient interface { 64 Do(*http.Request) (*http.Response, error) 65 } 66 67 // Client represents a Prometheus API client. 68 type Client struct { 69 HTTPClient 70 userAgent string 71 logger log.Logger 72 } 73 74 // NewClient returns a new Prometheus API client. 75 func NewClient(c HTTPClient, logger log.Logger, userAgent string) *Client { 76 if logger == nil { 77 logger = log.NewNopLogger() 78 } 79 return &Client{ 80 HTTPClient: c, 81 logger: logger, 82 userAgent: userAgent, 83 } 84 } 85 86 // NewDefaultClient returns Client with tracing tripperware. 87 func NewDefaultClient() *Client { 88 client, _ := httpconfig.NewHTTPClient(httpconfig.ClientConfig{}, "") 89 return NewWithTracingClient( 90 log.NewNopLogger(), 91 client, 92 "", 93 ) 94 } 95 96 // NewWithTracingClient returns client with tracing tripperware. 97 func NewWithTracingClient(logger log.Logger, httpClient *http.Client, userAgent string) *Client { 98 httpClient.Transport = tracing.HTTPTripperware(log.NewNopLogger(), httpClient.Transport) 99 return NewClient( 100 httpClient, 101 logger, 102 userAgent, 103 ) 104 } 105 106 // req2xx sends a request to the given url.URL. If method is http.MethodPost then 107 // the raw query is encoded in the body and the appropriate Content-Type is set. 108 func (c *Client) req2xx(ctx context.Context, u *url.URL, method string) (_ []byte, _ int, err error) { 109 var b io.Reader 110 if method == http.MethodPost { 111 rq := u.RawQuery 112 b = strings.NewReader(rq) 113 u.RawQuery = "" 114 } 115 116 req, err := http.NewRequest(method, u.String(), b) 117 if err != nil { 118 return nil, 0, errors.Wrapf(err, "create %s request", method) 119 } 120 if c.userAgent != "" { 121 req.Header.Set("User-Agent", c.userAgent) 122 } 123 if method == http.MethodPost { 124 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 125 } 126 127 resp, err := c.Do(req.WithContext(ctx)) 128 if err != nil { 129 return nil, 0, errors.Wrapf(err, "perform %s request against %s", method, u.String()) 130 } 131 defer runutil.ExhaustCloseWithErrCapture(&err, resp.Body, "%s: close body", req.URL.String()) 132 133 body, err := io.ReadAll(resp.Body) 134 if err != nil { 135 return nil, resp.StatusCode, errors.Wrap(err, "read body") 136 } 137 if resp.StatusCode/100 != 2 { 138 return nil, resp.StatusCode, errors.Errorf("expected 2xx response, got %d. Body: %v", resp.StatusCode, string(body)) 139 } 140 return body, resp.StatusCode, nil 141 } 142 143 // IsWALDirAccessible returns no error if WAL dir can be found. This helps to tell 144 // if we have access to Prometheus TSDB directory. 145 func IsWALDirAccessible(dir string) error { 146 const errMsg = "WAL dir is not accessible. Is this dir a TSDB directory? If yes it is shared with TSDB?" 147 148 f, err := os.Stat(filepath.Join(dir, "wal")) 149 if err != nil { 150 return errors.Wrap(err, errMsg) 151 } 152 153 if !f.IsDir() { 154 return errors.New(errMsg) 155 } 156 157 return nil 158 } 159 160 // ExternalLabels returns sorted external labels from /api/v1/status/config Prometheus endpoint. 161 // Note that configuration can be hot reloadable on Prometheus, so this config might change in runtime. 162 func (c *Client) ExternalLabels(ctx context.Context, base *url.URL) (labels.Labels, error) { 163 u := *base 164 u.Path = path.Join(u.Path, "/api/v1/status/config") 165 166 span, ctx := tracing.StartSpan(ctx, "/prom_config HTTP[client]") 167 defer span.Finish() 168 169 body, _, err := c.req2xx(ctx, &u, http.MethodGet) 170 if err != nil { 171 return nil, err 172 } 173 var d struct { 174 Data struct { 175 YAML string `json:"yaml"` 176 } `json:"data"` 177 } 178 if err := json.Unmarshal(body, &d); err != nil { 179 return nil, errors.Wrapf(err, "unmarshal response: %v", string(body)) 180 } 181 var cfg struct { 182 GlobalConfig config.GlobalConfig `yaml:"global"` 183 } 184 if err := yaml.Unmarshal([]byte(d.Data.YAML), &cfg); err != nil { 185 return nil, errors.Wrapf(err, "parse Prometheus config: %v", d.Data.YAML) 186 } 187 188 lset := cfg.GlobalConfig.ExternalLabels 189 sort.Sort(lset) 190 return lset, nil 191 } 192 193 type Flags struct { 194 TSDBPath string `json:"storage.tsdb.path"` 195 TSDBRetention model.Duration `json:"storage.tsdb.retention"` 196 TSDBMinTime model.Duration `json:"storage.tsdb.min-block-duration"` 197 TSDBMaxTime model.Duration `json:"storage.tsdb.max-block-duration"` 198 WebEnableAdminAPI bool `json:"web.enable-admin-api"` 199 WebEnableLifecycle bool `json:"web.enable-lifecycle"` 200 } 201 202 // UnmarshalJSON implements the json.Unmarshaler interface. 203 func (f *Flags) UnmarshalJSON(b []byte) error { 204 // TODO(bwplotka): Avoid this custom unmarshal by: 205 // - prometheus/common: adding unmarshalJSON to modelDuration 206 // - prometheus/prometheus: flags should return proper JSON (not bool in string). 207 parsableFlags := struct { 208 TSDBPath string `json:"storage.tsdb.path"` 209 TSDBRetention modelDuration `json:"storage.tsdb.retention"` 210 TSDBMinTime modelDuration `json:"storage.tsdb.min-block-duration"` 211 TSDBMaxTime modelDuration `json:"storage.tsdb.max-block-duration"` 212 WebEnableAdminAPI modelBool `json:"web.enable-admin-api"` 213 WebEnableLifecycle modelBool `json:"web.enable-lifecycle"` 214 }{} 215 216 if err := json.Unmarshal(b, &parsableFlags); err != nil { 217 return err 218 } 219 220 *f = Flags{ 221 TSDBPath: parsableFlags.TSDBPath, 222 TSDBRetention: model.Duration(parsableFlags.TSDBRetention), 223 TSDBMinTime: model.Duration(parsableFlags.TSDBMinTime), 224 TSDBMaxTime: model.Duration(parsableFlags.TSDBMaxTime), 225 WebEnableAdminAPI: bool(parsableFlags.WebEnableAdminAPI), 226 WebEnableLifecycle: bool(parsableFlags.WebEnableLifecycle), 227 } 228 return nil 229 } 230 231 type modelDuration model.Duration 232 233 // UnmarshalJSON implements the json.Unmarshaler interface. 234 func (d *modelDuration) UnmarshalJSON(b []byte) error { 235 var s string 236 if err := json.Unmarshal(b, &s); err != nil { 237 return err 238 } 239 240 dur, err := model.ParseDuration(s) 241 if err != nil { 242 return err 243 } 244 *d = modelDuration(dur) 245 return nil 246 } 247 248 type modelBool bool 249 250 // UnmarshalJSON implements the json.Unmarshaler interface. 251 func (m *modelBool) UnmarshalJSON(b []byte) error { 252 var s string 253 if err := json.Unmarshal(b, &s); err != nil { 254 return err 255 } 256 257 boolean, err := strconv.ParseBool(s) 258 if err != nil { 259 return err 260 } 261 *m = modelBool(boolean) 262 return nil 263 } 264 265 // ConfiguredFlags returns configured flags from /api/v1/status/flags Prometheus endpoint. 266 // Added to Prometheus from v2.2. 267 func (c *Client) ConfiguredFlags(ctx context.Context, base *url.URL) (Flags, error) { 268 u := *base 269 u.Path = path.Join(u.Path, "/api/v1/status/flags") 270 271 req, err := http.NewRequest(http.MethodGet, u.String(), nil) 272 if err != nil { 273 return Flags{}, errors.Wrap(err, "create request") 274 } 275 276 span, ctx := tracing.StartSpan(ctx, "/prom_flags HTTP[client]") 277 defer span.Finish() 278 279 resp, err := c.Do(req.WithContext(ctx)) 280 if err != nil { 281 return Flags{}, errors.Wrapf(err, "request config against %s", u.String()) 282 } 283 defer runutil.ExhaustCloseWithLogOnErr(c.logger, resp.Body, "query body") 284 285 b, err := io.ReadAll(resp.Body) 286 if err != nil { 287 return Flags{}, errors.New("failed to read body") 288 } 289 290 switch resp.StatusCode { 291 case 404: 292 return Flags{}, ErrFlagEndpointNotFound 293 case 200: 294 var d struct { 295 Data Flags `json:"data"` 296 } 297 298 if err := json.Unmarshal(b, &d); err != nil { 299 return Flags{}, errors.Wrapf(err, "unmarshal response: %v", string(b)) 300 } 301 302 return d.Data, nil 303 default: 304 return Flags{}, errors.Errorf("got non-200 response code: %v, response: %v", resp.StatusCode, string(b)) 305 } 306 307 } 308 309 // Snapshot will request Prometheus to perform snapshot in directory returned by this function. 310 // Returned directory is relative to Prometheus data-dir. 311 // NOTE: `--web.enable-admin-api` flag has to be set on Prometheus. 312 // Added to Prometheus from v2.1. 313 // TODO(bwplotka): Add metrics. 314 func (c *Client) Snapshot(ctx context.Context, base *url.URL, skipHead bool) (string, error) { 315 u := *base 316 u.Path = path.Join(u.Path, "/api/v1/admin/tsdb/snapshot") 317 318 req, err := http.NewRequest( 319 http.MethodPost, 320 u.String(), 321 strings.NewReader(url.Values{"skip_head": []string{strconv.FormatBool(skipHead)}}.Encode()), 322 ) 323 if err != nil { 324 return "", errors.Wrap(err, "create request") 325 } 326 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 327 328 span, ctx := tracing.StartSpan(ctx, "/prom_snapshot HTTP[client]") 329 defer span.Finish() 330 331 resp, err := c.Do(req.WithContext(ctx)) 332 if err != nil { 333 return "", errors.Wrapf(err, "request snapshot against %s", u.String()) 334 } 335 defer runutil.ExhaustCloseWithLogOnErr(c.logger, resp.Body, "query body") 336 337 b, err := io.ReadAll(resp.Body) 338 if err != nil { 339 return "", errors.New("failed to read body") 340 } 341 342 if resp.StatusCode != 200 { 343 return "", errors.Errorf("is 'web.enable-admin-api' flag enabled? got non-200 response code: %v, response: %v", resp.StatusCode, string(b)) 344 } 345 346 var d struct { 347 Data struct { 348 Name string `json:"name"` 349 } `json:"data"` 350 } 351 if err := json.Unmarshal(b, &d); err != nil { 352 return "", errors.Wrapf(err, "unmarshal response: %v", string(b)) 353 } 354 355 return path.Join("snapshots", d.Data.Name), nil 356 } 357 358 type QueryOptions struct { 359 DoNotAddThanosParams bool 360 Deduplicate bool 361 PartialResponseStrategy storepb.PartialResponseStrategy 362 Method string 363 MaxSourceResolution string 364 Engine string 365 Explain bool 366 } 367 368 func (p *QueryOptions) AddTo(values url.Values) error { 369 values.Add("dedup", fmt.Sprintf("%v", p.Deduplicate)) 370 if len(p.MaxSourceResolution) > 0 { 371 values.Add("max_source_resolution", p.MaxSourceResolution) 372 } 373 374 values.Add("explain", fmt.Sprintf("%v", p.Explain)) 375 values.Add("engine", p.Engine) 376 377 var partialResponseValue string 378 switch p.PartialResponseStrategy { 379 case storepb.PartialResponseStrategy_WARN: 380 partialResponseValue = strconv.FormatBool(true) 381 case storepb.PartialResponseStrategy_ABORT: 382 partialResponseValue = strconv.FormatBool(false) 383 default: 384 return errors.Errorf("unknown partial response strategy %v", p.PartialResponseStrategy) 385 } 386 387 // TODO(bwplotka): Apply change from bool to strategy in Query API as well. 388 values.Add("partial_response", partialResponseValue) 389 390 return nil 391 } 392 393 type Explanation struct { 394 Name string `json:"name"` 395 Children []*Explanation `json:"children,omitempty"` 396 } 397 398 // QueryInstant performs an instant query using a default HTTP client and returns results in model.Vector type. 399 func (c *Client) QueryInstant(ctx context.Context, base *url.URL, query string, t time.Time, opts QueryOptions) (model.Vector, []string, *Explanation, error) { 400 params, err := url.ParseQuery(base.RawQuery) 401 if err != nil { 402 return nil, nil, nil, errors.Wrapf(err, "parse raw query %s", base.RawQuery) 403 } 404 params.Add("query", query) 405 params.Add("time", t.Format(time.RFC3339Nano)) 406 if !opts.DoNotAddThanosParams { 407 if err := opts.AddTo(params); err != nil { 408 return nil, nil, nil, errors.Wrap(err, "add thanos opts query params") 409 } 410 } 411 412 u := *base 413 u.Path = path.Join(u.Path, "/api/v1/query") 414 u.RawQuery = params.Encode() 415 416 level.Debug(c.logger).Log("msg", "querying instant", "url", u.String()) 417 418 span, ctx := tracing.StartSpan(ctx, "/prom_query_instant HTTP[client]") 419 defer span.Finish() 420 421 method := opts.Method 422 if method == "" { 423 method = http.MethodGet 424 } 425 426 body, _, err := c.req2xx(ctx, &u, method) 427 if err != nil { 428 return nil, nil, nil, errors.Wrap(err, "read query instant response") 429 } 430 431 // Decode only ResultType and load Result only as RawJson since we don't know 432 // structure of the Result yet. 433 var m struct { 434 Data struct { 435 ResultType string `json:"resultType"` 436 Result json.RawMessage `json:"result"` 437 Explanation *Explanation `json:"explanation,omitempty"` 438 } `json:"data"` 439 440 Error string `json:"error,omitempty"` 441 ErrorType string `json:"errorType,omitempty"` 442 // Extra fields supported by Thanos Querier. 443 Warnings []string `json:"warnings"` 444 } 445 446 if err = json.Unmarshal(body, &m); err != nil { 447 return nil, nil, nil, errors.Wrap(err, "unmarshal query instant response") 448 } 449 450 var vectorResult model.Vector 451 452 // Decode the Result depending on the ResultType 453 // Currently only `vector` and `scalar` types are supported. 454 switch m.Data.ResultType { 455 case string(parser.ValueTypeVector): 456 if err = json.Unmarshal(m.Data.Result, &vectorResult); err != nil { 457 return nil, nil, nil, errors.Wrap(err, "decode result into ValueTypeVector") 458 } 459 case string(parser.ValueTypeScalar): 460 vectorResult, err = convertScalarJSONToVector(m.Data.Result) 461 if err != nil { 462 return nil, nil, nil, errors.Wrap(err, "decode result into ValueTypeScalar") 463 } 464 default: 465 if m.Warnings != nil { 466 return nil, nil, nil, errors.Errorf("error: %s, type: %s, warning: %s", m.Error, m.ErrorType, strings.Join(m.Warnings, ", ")) 467 } 468 if m.Error != "" { 469 return nil, nil, nil, errors.Errorf("error: %s, type: %s", m.Error, m.ErrorType) 470 } 471 return nil, nil, nil, errors.Errorf("received status code: 200, unknown response type: '%q'", m.Data.ResultType) 472 } 473 474 return vectorResult, m.Warnings, m.Data.Explanation, nil 475 } 476 477 // PromqlQueryInstant performs instant query and returns results in promql.Vector type that is compatible with promql package. 478 func (c *Client) PromqlQueryInstant(ctx context.Context, base *url.URL, query string, t time.Time, opts QueryOptions) (promql.Vector, []string, error) { 479 vectorResult, warnings, _, err := c.QueryInstant(ctx, base, query, t, opts) 480 if err != nil { 481 return nil, nil, err 482 } 483 484 vec := make(promql.Vector, 0, len(vectorResult)) 485 486 for _, e := range vectorResult { 487 lset := make(labels.Labels, 0, len(e.Metric)) 488 489 for k, v := range e.Metric { 490 lset = append(lset, labels.Label{ 491 Name: string(k), 492 Value: string(v), 493 }) 494 } 495 sort.Sort(lset) 496 497 vec = append(vec, promql.Sample{ 498 Metric: lset, 499 T: int64(e.Timestamp), 500 F: float64(e.Value), 501 }) 502 } 503 504 return vec, warnings, nil 505 } 506 507 // QueryRange performs a range query using a default HTTP client and returns results in model.Matrix type. 508 func (c *Client) QueryRange(ctx context.Context, base *url.URL, query string, startTime, endTime, step int64, opts QueryOptions) (model.Matrix, []string, *Explanation, error) { 509 params, err := url.ParseQuery(base.RawQuery) 510 if err != nil { 511 return nil, nil, nil, errors.Wrapf(err, "parse raw query %s", base.RawQuery) 512 } 513 params.Add("query", query) 514 params.Add("start", formatTime(timestamp.Time(startTime))) 515 params.Add("end", formatTime(timestamp.Time(endTime))) 516 params.Add("step", strconv.FormatInt(step, 10)) 517 if !opts.DoNotAddThanosParams { 518 if err := opts.AddTo(params); err != nil { 519 return nil, nil, nil, errors.Wrap(err, "add thanos opts query params") 520 } 521 } 522 523 u := *base 524 u.Path = path.Join(u.Path, "/api/v1/query_range") 525 u.RawQuery = params.Encode() 526 527 level.Debug(c.logger).Log("msg", "range query", "url", u.String()) 528 529 span, ctx := tracing.StartSpan(ctx, "/prom_query_range HTTP[client]") 530 defer span.Finish() 531 532 body, _, err := c.req2xx(ctx, &u, http.MethodGet) 533 if err != nil { 534 return nil, nil, nil, errors.Wrap(err, "read query range response") 535 } 536 537 // Decode only ResultType and load Result only as RawJson since we don't know 538 // structure of the Result yet. 539 var m struct { 540 Data struct { 541 ResultType string `json:"resultType"` 542 Result json.RawMessage `json:"result"` 543 Explanation *Explanation `json:"explanation,omitempty"` 544 } `json:"data"` 545 546 Error string `json:"error,omitempty"` 547 ErrorType string `json:"errorType,omitempty"` 548 // Extra fields supported by Thanos Querier. 549 Warnings []string `json:"warnings"` 550 } 551 552 if err = json.Unmarshal(body, &m); err != nil { 553 return nil, nil, nil, errors.Wrap(err, "unmarshal query range response") 554 } 555 556 var matrixResult model.Matrix 557 558 // Decode the Result depending on the ResultType 559 switch m.Data.ResultType { 560 case string(parser.ValueTypeMatrix): 561 if err = json.Unmarshal(m.Data.Result, &matrixResult); err != nil { 562 return nil, nil, nil, errors.Wrap(err, "decode result into ValueTypeMatrix") 563 } 564 default: 565 if m.Warnings != nil { 566 return nil, nil, nil, errors.Errorf("error: %s, type: %s, warning: %s", m.Error, m.ErrorType, strings.Join(m.Warnings, ", ")) 567 } 568 if m.Error != "" { 569 return nil, nil, nil, errors.Errorf("error: %s, type: %s", m.Error, m.ErrorType) 570 } 571 572 return nil, nil, nil, errors.Errorf("received status code: 200, unknown response type: '%q'", m.Data.ResultType) 573 } 574 return matrixResult, m.Warnings, m.Data.Explanation, nil 575 } 576 577 // Scalar response consists of array with mixed types so it needs to be 578 // unmarshaled separately. 579 func convertScalarJSONToVector(scalarJSONResult json.RawMessage) (model.Vector, error) { 580 var ( 581 // Do not specify exact length of the expected slice since JSON unmarshaling 582 // would make the length fit the size and we won't be able to check the length afterwards. 583 resultPointSlice []json.RawMessage 584 resultTime model.Time 585 resultValue model.SampleValue 586 ) 587 if err := json.Unmarshal(scalarJSONResult, &resultPointSlice); err != nil { 588 return nil, err 589 } 590 if len(resultPointSlice) != 2 { 591 return nil, errors.Errorf("invalid scalar result format %v, expected timestamp -> value tuple", resultPointSlice) 592 } 593 if err := json.Unmarshal(resultPointSlice[0], &resultTime); err != nil { 594 return nil, errors.Wrapf(err, "unmarshaling scalar time from %v", resultPointSlice) 595 } 596 if err := json.Unmarshal(resultPointSlice[1], &resultValue); err != nil { 597 return nil, errors.Wrapf(err, "unmarshaling scalar value from %v", resultPointSlice) 598 } 599 return model.Vector{&model.Sample{ 600 Metric: model.Metric{}, 601 Value: resultValue, 602 Timestamp: resultTime}}, nil 603 } 604 605 // AlertmanagerAlerts returns alerts from Alertmanager. 606 func (c *Client) AlertmanagerAlerts(ctx context.Context, base *url.URL) ([]*model.Alert, error) { 607 u := *base 608 u.Path = path.Join(u.Path, "/api/v1/alerts") 609 610 level.Debug(c.logger).Log("msg", "querying instant", "url", u.String()) 611 612 span, ctx := tracing.StartSpan(ctx, "/alertmanager_alerts HTTP[client]") 613 defer span.Finish() 614 615 body, _, err := c.req2xx(ctx, &u, http.MethodGet) 616 if err != nil { 617 return nil, err 618 } 619 620 // Decode only ResultType and load Result only as RawJson since we don't know 621 // structure of the Result yet. 622 var v struct { 623 Data []*model.Alert `json:"data"` 624 } 625 if err = json.Unmarshal(body, &v); err != nil { 626 return nil, errors.Wrap(err, "unmarshal alertmanager alert API response") 627 } 628 sort.Slice(v.Data, func(i, j int) bool { 629 return v.Data[i].Labels.Before(v.Data[j].Labels) 630 }) 631 return v.Data, nil 632 } 633 634 // BuildVersion returns Prometheus version from /api/v1/status/buildinfo Prometheus endpoint. 635 // For Prometheus versions < 2.14.0 it returns "0" as Prometheus version. 636 func (c *Client) BuildVersion(ctx context.Context, base *url.URL) (string, error) { 637 u := *base 638 u.Path = path.Join(u.Path, "/api/v1/status/buildinfo") 639 640 level.Debug(c.logger).Log("msg", "build version", "url", u.String()) 641 642 span, ctx := tracing.StartSpan(ctx, "/prom_buildversion HTTP[client]") 643 defer span.Finish() 644 645 // We get status code 404 or 405 for prometheus versions lower than 2.14.0 646 body, code, err := c.req2xx(ctx, &u, http.MethodGet) 647 if err != nil { 648 if code == http.StatusNotFound { 649 return "0", nil 650 } 651 if code == http.StatusMethodNotAllowed { 652 return "0", nil 653 } 654 return "", err 655 } 656 657 var b struct { 658 Data struct { 659 Version string `json:"version"` 660 } `json:"data"` 661 } 662 663 if err = json.Unmarshal(body, &b); err != nil { 664 return "", errors.Wrap(err, "unmarshal build info API response") 665 } 666 667 return b.Data.Version, nil 668 } 669 670 func formatTime(t time.Time) string { 671 return strconv.FormatFloat(float64(t.Unix())+float64(t.Nanosecond())/1e9, 'f', -1, 64) 672 } 673 674 func (c *Client) get2xxResultWithGRPCErrors(ctx context.Context, spanName string, u *url.URL, data interface{}) error { 675 span, ctx := tracing.StartSpan(ctx, spanName) 676 defer span.Finish() 677 678 body, code, err := c.req2xx(ctx, u, http.MethodGet) 679 if err != nil { 680 if code, exists := statusToCode[code]; exists && code != 0 { 681 return status.Error(code, err.Error()) 682 } 683 return status.Error(codes.Internal, err.Error()) 684 } 685 686 if code == http.StatusNoContent { 687 return nil 688 } 689 690 var m struct { 691 Data interface{} `json:"data"` 692 Status string `json:"status"` 693 Error string `json:"error"` 694 } 695 696 if err = json.Unmarshal(body, &m); err != nil { 697 return status.Error(codes.Internal, err.Error()) 698 } 699 700 if m.Status != SUCCESS { 701 code, exists := statusToCode[code] 702 if !exists { 703 return status.Error(codes.Internal, m.Error) 704 } 705 return status.Error(code, m.Error) 706 } 707 708 if err = json.Unmarshal(body, &data); err != nil { 709 return status.Error(codes.Internal, err.Error()) 710 } 711 712 return nil 713 } 714 715 // SeriesInGRPC returns the labels from Prometheus series API. It uses gRPC errors. 716 // NOTE: This method is tested in pkg/store/prometheus_test.go against Prometheus. 717 func (c *Client) SeriesInGRPC(ctx context.Context, base *url.URL, matchers []*labels.Matcher, startTime, endTime int64) ([]map[string]string, error) { 718 u := *base 719 u.Path = path.Join(u.Path, "/api/v1/series") 720 q := u.Query() 721 722 q.Add("match[]", storepb.PromMatchersToString(matchers...)) 723 q.Add("start", formatTime(timestamp.Time(startTime))) 724 q.Add("end", formatTime(timestamp.Time(endTime))) 725 u.RawQuery = q.Encode() 726 727 var m struct { 728 Data []map[string]string `json:"data"` 729 } 730 731 return m.Data, c.get2xxResultWithGRPCErrors(ctx, "/prom_series HTTP[client]", &u, &m) 732 } 733 734 // LabelNamesInGRPC returns all known label names constrained by the given matchers. It uses gRPC errors. 735 // NOTE: This method is tested in pkg/store/prometheus_test.go against Prometheus. 736 func (c *Client) LabelNamesInGRPC(ctx context.Context, base *url.URL, matchers []*labels.Matcher, startTime, endTime int64) ([]string, error) { 737 u := *base 738 u.Path = path.Join(u.Path, "/api/v1/labels") 739 q := u.Query() 740 741 if len(matchers) > 0 { 742 q.Add("match[]", storepb.PromMatchersToString(matchers...)) 743 } 744 q.Add("start", formatTime(timestamp.Time(startTime))) 745 q.Add("end", formatTime(timestamp.Time(endTime))) 746 u.RawQuery = q.Encode() 747 748 var m struct { 749 Data []string `json:"data"` 750 } 751 return m.Data, c.get2xxResultWithGRPCErrors(ctx, "/prom_label_names HTTP[client]", &u, &m) 752 } 753 754 // LabelValuesInGRPC returns all known label values for a given label name. It uses gRPC errors. 755 // NOTE: This method is tested in pkg/store/prometheus_test.go against Prometheus. 756 func (c *Client) LabelValuesInGRPC(ctx context.Context, base *url.URL, label string, matchers []*labels.Matcher, startTime, endTime int64) ([]string, error) { 757 u := *base 758 u.Path = path.Join(u.Path, "/api/v1/label/", label, "/values") 759 q := u.Query() 760 761 if len(matchers) > 0 { 762 q.Add("match[]", storepb.PromMatchersToString(matchers...)) 763 } 764 q.Add("start", formatTime(timestamp.Time(startTime))) 765 q.Add("end", formatTime(timestamp.Time(endTime))) 766 u.RawQuery = q.Encode() 767 768 var m struct { 769 Data []string `json:"data"` 770 } 771 return m.Data, c.get2xxResultWithGRPCErrors(ctx, "/prom_label_values HTTP[client]", &u, &m) 772 } 773 774 // RulesInGRPC returns the rules from Prometheus rules API. It uses gRPC errors. 775 // NOTE: This method is tested in pkg/store/prometheus_test.go against Prometheus. 776 func (c *Client) RulesInGRPC(ctx context.Context, base *url.URL, typeRules string) ([]*rulespb.RuleGroup, error) { 777 u := *base 778 u.Path = path.Join(u.Path, "/api/v1/rules") 779 780 if typeRules != "" { 781 q := u.Query() 782 q.Add("type", typeRules) 783 u.RawQuery = q.Encode() 784 } 785 786 var m struct { 787 Data *rulespb.RuleGroups `json:"data"` 788 } 789 790 if err := c.get2xxResultWithGRPCErrors(ctx, "/prom_rules HTTP[client]", &u, &m); err != nil { 791 return nil, err 792 } 793 794 // Prometheus does not support PartialResponseStrategy, and probably would never do. Make it Abort by default. 795 for _, g := range m.Data.Groups { 796 g.PartialResponseStrategy = storepb.PartialResponseStrategy_ABORT 797 } 798 return m.Data.Groups, nil 799 } 800 801 // AlertsInGRPC returns the rules from Prometheus alerts API. It uses gRPC errors. 802 // NOTE: This method is tested in pkg/store/prometheus_test.go against Prometheus. 803 func (c *Client) AlertsInGRPC(ctx context.Context, base *url.URL) ([]*rulespb.AlertInstance, error) { 804 u := *base 805 u.Path = path.Join(u.Path, "/api/v1/alerts") 806 807 var m struct { 808 Data struct { 809 Alerts []*rulespb.AlertInstance `json:"alerts"` 810 } `json:"data"` 811 } 812 813 if err := c.get2xxResultWithGRPCErrors(ctx, "/prom_alerts HTTP[client]", &u, &m); err != nil { 814 return nil, err 815 } 816 817 // Prometheus does not support PartialResponseStrategy, and probably would never do. Make it Abort by default. 818 for _, g := range m.Data.Alerts { 819 g.PartialResponseStrategy = storepb.PartialResponseStrategy_ABORT 820 } 821 return m.Data.Alerts, nil 822 } 823 824 // MetricMetadataInGRPC returns the metadata from Prometheus metric metadata API. It uses gRPC errors. 825 func (c *Client) MetricMetadataInGRPC(ctx context.Context, base *url.URL, metric string, limit int) (map[string][]metadatapb.Meta, error) { 826 u := *base 827 u.Path = path.Join(u.Path, "/api/v1/metadata") 828 q := u.Query() 829 830 if metric != "" { 831 q.Add("metric", metric) 832 } 833 // We only set limit when it is >= 0. 834 if limit >= 0 { 835 q.Add("limit", strconv.Itoa(limit)) 836 } 837 838 u.RawQuery = q.Encode() 839 840 var v struct { 841 Data map[string][]metadatapb.Meta `json:"data"` 842 } 843 return v.Data, c.get2xxResultWithGRPCErrors(ctx, "/prom_metric_metadata HTTP[client]", &u, &v) 844 } 845 846 // ExemplarsInGRPC returns the exemplars from Prometheus exemplars API. It uses gRPC errors. 847 // NOTE: This method is tested in pkg/store/prometheus_test.go against Prometheus. 848 func (c *Client) ExemplarsInGRPC(ctx context.Context, base *url.URL, query string, startTime, endTime int64) ([]*exemplarspb.ExemplarData, error) { 849 u := *base 850 u.Path = path.Join(u.Path, "/api/v1/query_exemplars") 851 q := u.Query() 852 853 q.Add("query", query) 854 q.Add("start", formatTime(timestamp.Time(startTime))) 855 q.Add("end", formatTime(timestamp.Time(endTime))) 856 u.RawQuery = q.Encode() 857 858 var m struct { 859 Data []*exemplarspb.ExemplarData `json:"data"` 860 } 861 862 if err := c.get2xxResultWithGRPCErrors(ctx, "/prom_exemplars HTTP[client]", &u, &m); err != nil { 863 return nil, err 864 } 865 866 return m.Data, nil 867 } 868 869 func (c *Client) TargetsInGRPC(ctx context.Context, base *url.URL, stateTargets string) (*targetspb.TargetDiscovery, error) { 870 u := *base 871 u.Path = path.Join(u.Path, "/api/v1/targets") 872 873 if stateTargets != "" { 874 q := u.Query() 875 q.Add("state", stateTargets) 876 u.RawQuery = q.Encode() 877 } 878 879 var v struct { 880 Data *targetspb.TargetDiscovery `json:"data"` 881 } 882 return v.Data, c.get2xxResultWithGRPCErrors(ctx, "/prom_targets HTTP[client]", &u, &v) 883 }