grafana/pkg/tsdb/loki/scopes.go
Carl Bergquist 16330e4113
Loki: Adds a suggestions resource call that inject scopes for finding label/labelvalues (#96025)
Signed-off-by: bergquist <carl.bergquist@gmail.com>
Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>
2024-11-13 14:36:12 +01:00

108 lines
3.3 KiB
Go

package loki
import (
"context"
"encoding/json"
"fmt"
"net/url"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/promlib/models"
"github.com/grafana/grafana/pkg/tsdb/loki/kinds/dataquery"
"github.com/grafana/loki/v3/pkg/logql/syntax"
"github.com/prometheus/prometheus/promql/parser"
)
// 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"`
Query string `json:"query"`
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"`
}
// GetSuggestions returns label names or label values for the given queries and scopes.
func GetSuggestions(ctx context.Context, lokiAPI *LokiAPI, req *backend.CallResourceRequest) (RawLokiResponse, error) {
sugReq := SuggestionRequest{}
err := json.Unmarshal(req.Body, &sugReq)
if err != nil {
return RawLokiResponse{}, fmt.Errorf("error unmarshalling suggestion request: %v", err)
}
values := url.Values{}
if sugReq.Query != "" {
// the query is used to find label/labelvalues the duration and interval does not matter.
// If the user want to filter values based on time it should used the `start` and `end` fields
interpolatedQuery := interpolateVariables(sugReq.Query, time.Minute, time.Minute, dataquery.LokiQueryTypeRange, time.Minute)
if len(sugReq.Scopes) > 0 {
rewrittenQuery, err := ApplyScopes(interpolatedQuery, sugReq.Scopes)
if err == nil {
values.Add("query", rewrittenQuery)
} else {
values.Add("query", interpolatedQuery)
}
}
} else if len(sugReq.Scopes) > 0 {
matchers, err := models.FiltersToMatchers(sugReq.Scopes, sugReq.AdhocFilters)
if err != nil {
return RawLokiResponse{}, fmt.Errorf("error converting filters to matchers: %v", err)
}
vs := parser.VectorSelector{LabelMatchers: matchers}
values.Add("query", vs.String())
}
if sugReq.Start != "" {
values.Add("start", sugReq.Start)
}
if sugReq.End != "" {
values.Add("end", sugReq.End)
}
var path string
if sugReq.LabelName != "" {
path = "/loki/api/v1/label/" + url.QueryEscape(sugReq.LabelName) + "/values?" + values.Encode()
} else {
path = "/loki/api/v1/labels?" + values.Encode()
}
return lokiAPI.RawQuery(ctx, path)
}
// ApplyScopes applies the given scope filters to the given raw expression.
func ApplyScopes(rawExpr string, scopeFilters []models.ScopeFilter) (string, error) {
if len(scopeFilters) == 0 {
return rawExpr, nil
}
scopeMatchers, err := models.FiltersToMatchers(scopeFilters, nil)
if err != nil {
return "", fmt.Errorf("failed to convert filters to matchers: %w", err)
}
// Need WithoutValidation to allow empty `{}` expressions
syntaxTree, err := syntax.ParseExprWithoutValidation(rawExpr)
if err != nil {
return "", fmt.Errorf("failed to parse raw expression: %w", err)
}
syntaxTree.Walk(func(e syntax.Expr) {
switch e := e.(type) {
case *syntax.MatchersExpr:
// TODO: Key Collisions?
e.Mts = append(e.Mts, scopeMatchers...)
default:
}
})
return syntaxTree.String(), nil
}