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  }