app/vmselect: add ability to pass match[], start and end to /api/v1/labels

This makes the `/api/v1/labels` handler consistent with already existing functionality for `/api/v1/label/.../values`.

See https://github.com/prometheus/prometheus/issues/6178 for more details.
This commit is contained in:
Aliaksandr Valialkin 2019-12-15 00:07:09 +02:00
parent 02f6566ce1
commit 96ff8d9adb
3 changed files with 98 additions and 9 deletions

View File

@ -405,6 +405,18 @@ VictoriaMetrics supports the following handlers from [Prometheus querying API](h
These handlers can be queried from Prometheus-compatible clients such as Grafana or curl. These handlers can be queried from Prometheus-compatible clients such as Grafana or curl.
VictoriaMetrics accepts additional args for `/api/v1/labels` and `/api/v1/label/.../values` handlers.
See [this feature request](https://github.com/prometheus/prometheus/issues/6178) for details:
* Any number [time series selectors](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors) via `match[]` query arg.
* Optional `start` and `end` query args for limiting the time range for the selected labels or label values.
Additionally VictoriaMetrics provides the following handlers:
* `/api/v1/series/count` - it returns the total number of time series in the database. Note that this handler scans all the inverted index,
so it can be slow if the database contains tens of millions of time series.
* `/api/v1/labels/count` - it returns a list of `label: values_count` entries. It can be used for determining labels with the maximum number of values.
### How to build from sources ### How to build from sources

View File

@ -432,13 +432,10 @@ func GetLabelEntries(deadline Deadline) ([]storage.TagEntry, error) {
// Sort labelEntries by the number of label values in each entry. // Sort labelEntries by the number of label values in each entry.
sort.Slice(labelEntries, func(i, j int) bool { sort.Slice(labelEntries, func(i, j int) bool {
a, b := labelEntries[i].Values, labelEntries[j].Values a, b := labelEntries[i].Values, labelEntries[j].Values
if len(a) < len(b) { if len(a) != len(b) {
return true return len(a) > len(b)
} }
if len(a) > len(b) { return labelEntries[i].Key > labelEntries[j].Key
return false
}
return labelEntries[i].Key < labelEntries[j].Key
}) })
return labelEntries, nil return labelEntries, nil

View File

@ -285,6 +285,13 @@ func labelValuesWithMatches(labelName string, matches []string, start, end int64
if err != nil { if err != nil {
return nil, err return nil, err
} }
for i, tfs := range tagFilterss {
// Add `labelName!=''` tag filter in order to filter out series without the labelName.
tagFilterss[i] = append(tfs, storage.TagFilter{
Key: []byte(labelName),
IsNegative: true,
})
}
if start >= end { if start >= end {
end = start + defaultStep end = start + defaultStep
} }
@ -346,9 +353,37 @@ var labelsCountDuration = metrics.NewSummary(`vm_request_duration_seconds{path="
func LabelsHandler(w http.ResponseWriter, r *http.Request) error { func LabelsHandler(w http.ResponseWriter, r *http.Request) error {
startTime := time.Now() startTime := time.Now()
deadline := getDeadline(r) deadline := getDeadline(r)
labels, err := netstorage.GetLabels(deadline)
if err != nil { if err := r.ParseForm(); err != nil {
return fmt.Errorf("cannot obtain labels: %s", err) return fmt.Errorf("cannot parse form values: %s", err)
}
var labels []string
if len(r.Form["match[]"]) == 0 && len(r.Form["start"]) == 0 && len(r.Form["end"]) == 0 {
var err error
labels, err = netstorage.GetLabels(deadline)
if err != nil {
return fmt.Errorf("cannot obtain labels: %s", err)
}
} else {
// Extended functionality that allows filtering by label filters and time range
// i.e. /api/v1/labels?match[]=foobar{baz="abc"}&start=...&end=...
matches := r.Form["match[]"]
if len(matches) == 0 {
matches = []string{"{__name__!=''}"}
}
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
}
labels, err = labelsWithMatches(matches, start, end, deadline)
if err != nil {
return fmt.Errorf("cannot obtain labels for match[]=%q, start=%d, end=%d: %s", matches, start, end, err)
}
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -357,6 +392,51 @@ func LabelsHandler(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func labelsWithMatches(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 {
end = start + 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) {
mLock.Lock()
tags := rs.MetricName.Tags
for i := range tags {
t := &tags[i]
m[string(t.Key)] = struct{}{}
}
m["__name__"] = struct{}{}
mLock.Unlock()
})
if err != nil {
return nil, fmt.Errorf("error when data fetching: %s", err)
}
labels := make([]string, 0, len(m))
for label := range m {
labels = append(labels, label)
}
sort.Strings(labels)
return labels, nil
}
var labelsDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/labels"}`) var labelsDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/labels"}`)
// SeriesCountHandler processes /api/v1/series/count request. // SeriesCountHandler processes /api/v1/series/count request.