mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 04:12:14 +08:00
208 lines
5.8 KiB
Go
208 lines
5.8 KiB
Go
package resource
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
|
"github.com/grafana/grafana-plugin-sdk-go/data/utils/maputil"
|
|
"github.com/prometheus/prometheus/promql/parser"
|
|
|
|
"github.com/grafana/grafana/pkg/promlib/client"
|
|
"github.com/grafana/grafana/pkg/promlib/models"
|
|
"github.com/grafana/grafana/pkg/promlib/utils"
|
|
)
|
|
|
|
type Resource struct {
|
|
promClient *client.Client
|
|
log log.Logger
|
|
}
|
|
|
|
func New(
|
|
httpClient *http.Client,
|
|
settings backend.DataSourceInstanceSettings,
|
|
plog log.Logger,
|
|
) (*Resource, error) {
|
|
jsonData, err := utils.GetJsonData(settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
httpMethod, _ := maputil.GetStringOptional(jsonData, "httpMethod")
|
|
|
|
if httpMethod == "" {
|
|
httpMethod = http.MethodPost
|
|
}
|
|
|
|
return &Resource{
|
|
log: plog,
|
|
// we don't use queryTimeout for resource calls
|
|
promClient: client.NewClient(httpClient, httpMethod, settings.URL, ""),
|
|
}, nil
|
|
}
|
|
|
|
func (r *Resource) Execute(ctx context.Context, req *backend.CallResourceRequest) (*backend.CallResourceResponse, error) {
|
|
r.log.FromContext(ctx).Debug("Sending resource query", "URL", req.URL)
|
|
resp, err := r.promClient.QueryResource(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error querying resource: %v", err)
|
|
}
|
|
|
|
// frontend sets the X-Grafana-Cache with the desired response cache control value
|
|
if len(req.GetHTTPHeaders().Get("X-Grafana-Cache")) > 0 {
|
|
resp.Header.Set("X-Grafana-Cache", "y")
|
|
resp.Header.Set("Cache-Control", req.GetHTTPHeaders().Get("X-Grafana-Cache"))
|
|
}
|
|
|
|
defer func() {
|
|
tmpErr := resp.Body.Close()
|
|
if tmpErr != nil && err == nil {
|
|
err = tmpErr
|
|
}
|
|
}()
|
|
|
|
var buf bytes.Buffer
|
|
// Should be more efficient than ReadAll. See https://github.com/prometheus/client_golang/pull/976
|
|
_, err = buf.ReadFrom(resp.Body)
|
|
body := buf.Bytes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
callResponse := &backend.CallResourceResponse{
|
|
Status: resp.StatusCode,
|
|
Headers: resp.Header,
|
|
Body: body,
|
|
}
|
|
|
|
return callResponse, err
|
|
}
|
|
|
|
func getSelectors(expr string) ([]string, error) {
|
|
parsed, err := parser.ParseExpr(expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
selectors := make([]string, 0)
|
|
|
|
parser.Inspect(parsed, func(node parser.Node, nodes []parser.Node) error {
|
|
switch v := node.(type) {
|
|
case *parser.VectorSelector:
|
|
for _, matcher := range v.LabelMatchers {
|
|
if matcher == nil {
|
|
continue
|
|
}
|
|
if matcher.Name == "__name__" {
|
|
selectors = append(selectors, matcher.Value)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return selectors, nil
|
|
}
|
|
|
|
// SuggestionRequest is the request body for the GetSuggestions resource.
|
|
type SuggestionRequest struct {
|
|
// LabelName, if provided, will result in label values being returned for the given label name.
|
|
LabelName string `json:"labelName"`
|
|
|
|
Queries []string `json:"queries"`
|
|
|
|
Scopes []models.ScopeFilter `json:"scopes"`
|
|
AdhocFilters []models.ScopeFilter `json:"adhocFilters"`
|
|
|
|
// Start and End are proxied directly to the prometheus endpoint (which is rfc3339 | unix_timestamp)
|
|
Start string `json:"start"`
|
|
End string `json:"end"`
|
|
|
|
// Limit is the maximum number of suggestions to return and is proxied directly to the prometheus endpoint.
|
|
Limit int64 `json:"limit"`
|
|
}
|
|
|
|
// GetSuggestions takes a Suggestion Request in the body of the resource request.
|
|
// It builds a to call prometheus' labels endpoint (or label values endpoint if labelName is provided)
|
|
// The match parameters for the endpoints are built from metrics extracted from the queries
|
|
// combined with the scopes and adhoc filters provided in the request.
|
|
// Queries must be valid raw promql.
|
|
func (r *Resource) GetSuggestions(ctx context.Context, req *backend.CallResourceRequest) (*backend.CallResourceResponse, error) {
|
|
sugReq := SuggestionRequest{}
|
|
err := json.Unmarshal(req.Body, &sugReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error unmarshalling suggestion request: %v", err)
|
|
}
|
|
|
|
selectorList := []string{}
|
|
for _, query := range sugReq.Queries {
|
|
// Since we are only extracting selectors from the metric name, we can use dummy
|
|
// time durations.
|
|
interpolatedQuery := models.InterpolateVariables(
|
|
query,
|
|
time.Minute,
|
|
time.Minute,
|
|
"1m",
|
|
"15s",
|
|
time.Minute,
|
|
)
|
|
s, err := getSelectors(interpolatedQuery)
|
|
if err != nil {
|
|
r.log.Warn("error parsing selectors", "error", err, "query", interpolatedQuery)
|
|
continue
|
|
}
|
|
selectorList = append(selectorList, s...)
|
|
}
|
|
|
|
slices.Sort(selectorList)
|
|
selectorList = slices.Compact(selectorList)
|
|
|
|
matchers, err := models.FiltersToMatchers(sugReq.Scopes, sugReq.AdhocFilters)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error converting filters to matchers: %v", err)
|
|
}
|
|
|
|
values := url.Values{}
|
|
for _, s := range selectorList {
|
|
vs := parser.VectorSelector{Name: s, LabelMatchers: matchers}
|
|
values.Add("match[]", vs.String())
|
|
}
|
|
|
|
// if no timeserie name is provided, but scopes or adhoc filters are, the scope is still rendered and passed as match param.
|
|
if len(selectorList) == 0 && len(matchers) > 0 {
|
|
vs := parser.VectorSelector{LabelMatchers: matchers}
|
|
values.Add("match[]", vs.String())
|
|
}
|
|
|
|
if sugReq.Start != "" {
|
|
values.Add("start", sugReq.Start)
|
|
}
|
|
if sugReq.End != "" {
|
|
values.Add("end", sugReq.End)
|
|
}
|
|
if sugReq.Limit > 0 {
|
|
values.Add("limit", fmt.Sprintf("%d", sugReq.Limit))
|
|
}
|
|
|
|
newReq := &backend.CallResourceRequest{
|
|
PluginContext: req.PluginContext,
|
|
}
|
|
|
|
if sugReq.LabelName != "" {
|
|
// Get label values for the given name (key)
|
|
newReq.Path = "/api/v1/label/" + sugReq.LabelName + "/values"
|
|
newReq.URL = "/api/v1/label/" + sugReq.LabelName + "/values?" + values.Encode()
|
|
} else {
|
|
// Get Label names (keys)
|
|
newReq.Path = "/api/v1/labels"
|
|
newReq.URL = "/api/v1/labels?" + values.Encode()
|
|
}
|
|
|
|
return r.Execute(ctx, newReq)
|
|
}
|