diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go index 1763029016..abb5a50aac 100644 --- a/app/vmselect/prometheus/prometheus.go +++ b/app/vmselect/prometheus/prometheus.go @@ -6,12 +6,15 @@ import ( "math" "net/http" "runtime" + "sort" "strconv" "strings" + "sync" "time" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage" "github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql" + "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" "github.com/VictoriaMetrics/VictoriaMetrics/lib/storage" "github.com/VictoriaMetrics/metrics" "github.com/valyala/quicktemplate" @@ -230,9 +233,39 @@ var deleteDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/ func LabelValuesHandler(labelName string, w http.ResponseWriter, r *http.Request) error { startTime := time.Now() deadline := getDeadline(r) - labelValues, err := netstorage.GetLabelValues(labelName, deadline) - if err != nil { - return fmt.Errorf(`cannot obtain label values for %q: %s`, labelName, err) + + if err := r.ParseForm(); err != nil { + return fmt.Errorf("cannot parse form values: %s", err) + } + var labelValues []string + if len(r.Form["match[]"]) == 0 && len(r.Form["start"]) == 0 && len(r.Form["end"]) == 0 { + var err error + labelValues, err = netstorage.GetLabelValues(labelName, deadline) + if err != nil { + return fmt.Errorf(`cannot obtain label values for %q: %s`, labelName, err) + } + } else { + // Extended functionality that allows filtering by label filters and time range + // i.e. /api/v1/label/foo/values?match[]=foobar{baz="abc"}&start=...&end=... + // is equivalent to `label_values(foobar{baz="abc"}, foo)` call on the selected + // time range in Grafana templating. + matches := r.Form["match[]"] + if len(matches) == 0 { + matches = []string{fmt.Sprintf("{%s!=''}", labelName)} + } + ct := currentTime() + end, err := getTime(r, "end", ct) + if err != nil { + return err + } + start, err := getTime(r, "start", end-defaultStep) + if err != nil { + return err + } + labelValues, err = labelValuesWithMatches(labelName, matches, start, end, deadline) + if err != nil { + return fmt.Errorf("cannot obtain label values for %q, match[]=%q, start=%d, end=%d: %s", labelName, matches, start, end, err) + } } w.Header().Set("Content-Type", "application/json") @@ -241,6 +274,50 @@ func LabelValuesHandler(labelName string, w http.ResponseWriter, r *http.Request return nil } +func labelValuesWithMatches(labelName string, matches []string, start, end int64, deadline netstorage.Deadline) ([]string, error) { + if len(matches) == 0 { + logger.Panicf("BUG: matches must be non-empty") + } + tagFilterss, err := getTagFilterssFromMatches(matches) + if err != nil { + return nil, err + } + if start >= end { + start = end - defaultStep + } + sq := &storage.SearchQuery{ + MinTimestamp: start, + MaxTimestamp: end, + TagFilterss: tagFilterss, + } + rss, err := netstorage.ProcessSearchQuery(sq, false, deadline) + if err != nil { + return nil, fmt.Errorf("cannot fetch data for %q: %s", sq, err) + } + + m := make(map[string]struct{}) + var mLock sync.Mutex + err = rss.RunParallel(func(rs *netstorage.Result, workerID uint) { + labelValue := rs.MetricName.GetTagValue(labelName) + if len(labelValue) == 0 { + return + } + mLock.Lock() + m[string(labelValue)] = struct{}{} + mLock.Unlock() + }) + if err != nil { + return nil, fmt.Errorf("error when data fetching: %s", err) + } + + labelValues := make([]string, 0, len(m)) + for labelValue := range m { + labelValues = append(labelValues, labelValue) + } + sort.Strings(labelValues) + return labelValues, nil +} + var labelValuesDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/label/{}/values"}`) // LabelsCountHandler processes /api/v1/labels/count request.