| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | package client | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2022-06-15 22:46:21 +08:00
										 |  |  | 	"bytes" | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2022-12-16 18:15:19 +08:00
										 |  |  | 	"io" | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"net/url" | 
					
						
							|  |  |  | 	"path" | 
					
						
							|  |  |  | 	"strconv" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-15 22:46:21 +08:00
										 |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/backend" | 
					
						
							| 
									
										
										
										
											2023-01-30 16:38:51 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 00:22:33 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/promlib/models" | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type doer interface { | 
					
						
							|  |  |  | 	Do(req *http.Request) (*http.Response, error) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | // Client is a custom Prometheus client. Reason for this is that Prom Go client serializes response into its own
 | 
					
						
							|  |  |  | // objects, we have to go through them and then serialize again into DataFrame which isn't very efficient. Using custom
 | 
					
						
							|  |  |  | // client we can parse response directly into DataFrame.
 | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | type Client struct { | 
					
						
							| 
									
										
										
										
											2024-10-23 22:16:00 +08:00
										 |  |  | 	doer         doer | 
					
						
							|  |  |  | 	method       string | 
					
						
							|  |  |  | 	baseUrl      string | 
					
						
							|  |  |  | 	queryTimeout string | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-23 22:16:00 +08:00
										 |  |  | func NewClient(d doer, method, baseUrl, queryTimeout string) *Client { | 
					
						
							|  |  |  | 	return &Client{doer: d, method: method, baseUrl: baseUrl, queryTimeout: queryTimeout} | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | func (c *Client) QueryRange(ctx context.Context, q *models.Query) (*http.Response, error) { | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	tr := q.TimeRange() | 
					
						
							| 
									
										
										
										
											2022-12-16 18:15:19 +08:00
										 |  |  | 	qv := map[string]string{ | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 		"query": q.Expr, | 
					
						
							|  |  |  | 		"start": formatTime(tr.Start), | 
					
						
							|  |  |  | 		"end":   formatTime(tr.End), | 
					
						
							|  |  |  | 		"step":  strconv.FormatFloat(tr.Step.Seconds(), 'f', -1, 64), | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-10-23 22:16:00 +08:00
										 |  |  | 	if c.queryTimeout != "" { | 
					
						
							|  |  |  | 		qv["timeout"] = c.queryTimeout | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-12-16 18:15:19 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | 	req, err := c.createQueryRequest(ctx, "api/v1/query_range", qv) | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	return c.doer.Do(req) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | func (c *Client) QueryInstant(ctx context.Context, q *models.Query) (*http.Response, error) { | 
					
						
							| 
									
										
										
										
											2022-12-29 23:32:08 +08:00
										 |  |  | 	// We do not need a time range here.
 | 
					
						
							|  |  |  | 	// Instant query evaluates at a single point in time.
 | 
					
						
							|  |  |  | 	// Using q.TimeRange is aligning the query range to step.
 | 
					
						
							|  |  |  | 	// Which causes a misleading time point.
 | 
					
						
							|  |  |  | 	// Instead of aligning we use time point directly.
 | 
					
						
							|  |  |  | 	// https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries
 | 
					
						
							|  |  |  | 	qv := map[string]string{"query": q.Expr, "time": formatTime(q.End)} | 
					
						
							| 
									
										
										
										
											2024-10-23 22:16:00 +08:00
										 |  |  | 	if c.queryTimeout != "" { | 
					
						
							|  |  |  | 		qv["timeout"] = c.queryTimeout | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | 	req, err := c.createQueryRequest(ctx, "api/v1/query", qv) | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	return c.doer.Do(req) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | func (c *Client) QueryExemplars(ctx context.Context, q *models.Query) (*http.Response, error) { | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	tr := q.TimeRange() | 
					
						
							| 
									
										
										
										
											2022-12-16 18:15:19 +08:00
										 |  |  | 	qv := map[string]string{ | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 		"query": q.Expr, | 
					
						
							|  |  |  | 		"start": formatTime(tr.Start), | 
					
						
							|  |  |  | 		"end":   formatTime(tr.End), | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | 	req, err := c.createQueryRequest(ctx, "api/v1/query_exemplars", qv) | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	return c.doer.Do(req) | 
					
						
							| 
									
										
										
										
											2022-06-15 22:46:21 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (c *Client) QueryResource(ctx context.Context, req *backend.CallResourceRequest) (*http.Response, error) { | 
					
						
							|  |  |  | 	// The way URL is represented in CallResourceRequest and what we need for the fetch function is different
 | 
					
						
							|  |  |  | 	// so here we have to do a bit of parsing, so we can then compose it with the base url in correct way.
 | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	reqUrlParsed, err := url.Parse(req.URL) | 
					
						
							| 
									
										
										
										
											2022-06-15 22:46:21 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	u, err := c.createUrl(req.Path, nil) | 
					
						
							| 
									
										
										
										
											2022-06-04 02:56:13 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	u.RawQuery = reqUrlParsed.RawQuery | 
					
						
							| 
									
										
										
										
											2022-06-04 02:56:13 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	// We use method from the request, as for resources front end may do a fallback to GET if POST does not work
 | 
					
						
							|  |  |  | 	// nad we want to respect that.
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | 	httpRequest, err := createRequest(ctx, req.Method, u, bytes.NewReader(req.Body)) | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-06-04 02:56:13 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	return c.doer.Do(httpRequest) | 
					
						
							| 
									
										
										
										
											2022-06-04 02:56:13 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | func (c *Client) createQueryRequest(ctx context.Context, endpoint string, qv map[string]string) (*http.Request, error) { | 
					
						
							| 
									
										
										
										
											2022-12-16 18:15:19 +08:00
										 |  |  | 	if strings.ToUpper(c.method) == http.MethodPost { | 
					
						
							|  |  |  | 		u, err := c.createUrl(endpoint, nil) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		v := make(url.Values) | 
					
						
							|  |  |  | 		for key, val := range qv { | 
					
						
							|  |  |  | 			v.Set(key, val) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | 		return createRequest(ctx, c.method, u, strings.NewReader(v.Encode())) | 
					
						
							| 
									
										
										
										
											2022-12-16 18:15:19 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	u, err := c.createUrl(endpoint, qv) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | 	return createRequest(ctx, c.method, u, http.NoBody) | 
					
						
							| 
									
										
										
										
											2022-12-16 18:15:19 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | func (c *Client) createUrl(endpoint string, qs map[string]string) (*url.URL, error) { | 
					
						
							|  |  |  | 	finalUrl, err := url.ParseRequestURI(c.baseUrl) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	finalUrl.Path = path.Join(finalUrl.Path, endpoint) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-16 18:15:19 +08:00
										 |  |  | 	// don't re-encode the Query if not needed
 | 
					
						
							|  |  |  | 	if len(qs) != 0 { | 
					
						
							|  |  |  | 		urlQuery := finalUrl.Query() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for key, val := range qs { | 
					
						
							|  |  |  | 			urlQuery.Set(key, val) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		finalUrl.RawQuery = urlQuery.Encode() | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return finalUrl, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | func createRequest(ctx context.Context, method string, u *url.URL, bodyReader io.Reader) (*http.Request, error) { | 
					
						
							| 
									
										
										
										
											2022-06-15 22:46:21 +08:00
										 |  |  | 	request, err := http.NewRequestWithContext(ctx, method, u.String(), bodyReader) | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-12-21 20:25:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-04 02:56:13 +08:00
										 |  |  | 	if strings.ToUpper(method) == http.MethodPost { | 
					
						
							| 
									
										
										
										
											2022-07-05 04:55:07 +08:00
										 |  |  | 		// This may not be true but right now we don't have more information here and seems like we send just this type
 | 
					
						
							|  |  |  | 		// of encoding right now if it is a POST
 | 
					
						
							| 
									
										
										
										
											2022-06-15 22:46:21 +08:00
										 |  |  | 		request.Header.Set("Content-Type", "application/x-www-form-urlencoded") | 
					
						
							| 
									
										
										
										
											2022-07-05 04:55:07 +08:00
										 |  |  | 		// This allows transport to retry request. See https://github.com/prometheus/client_golang/pull/1022
 | 
					
						
							|  |  |  | 		// It's set to nil so it is not actually sent over the wire, just used in Go http lib to retry requests.
 | 
					
						
							|  |  |  | 		request.Header["Idempotency-Key"] = nil | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-07-04 17:18:45 +08:00
										 |  |  | 	return request, nil | 
					
						
							| 
									
										
										
										
											2022-05-14 02:28:54 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func formatTime(t time.Time) string { | 
					
						
							|  |  |  | 	return strconv.FormatFloat(float64(t.Unix())+float64(t.Nanosecond())/1e9, 'f', -1, 64) | 
					
						
							|  |  |  | } |