mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-02-09 15:27:11 +00:00
app/vmselect/graphite: add /tags/autoComplete/tags
handler from Graphite Tags API
See https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support
This commit is contained in:
parent
2f4421b86c
commit
f2f16d8e79
9 changed files with 239 additions and 24 deletions
|
@ -208,6 +208,7 @@ or [an alternative dashboard for VictoriaMetrics cluster](https://grafana.com/gr
|
|||
- `tags` - returns tag names. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
|
||||
- `tags/<tag_name>` - returns tag values for the given `<tag_name>`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
|
||||
- `tags/findSeries` - returns series matching the given `expr`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
|
||||
- `tags/autoComplete/tags` - returns tags matching the given `tagPrefix` and/or `expr`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support).
|
||||
|
||||
* URL for time series deletion: `http://<vmselect>:8481/delete/<accountID>/prometheus/api/v1/admin/tsdb/delete_series?match[]=<timeseries_selector_for_delete>`.
|
||||
Note that the `delete_series` handler should be used only in exceptional cases such as deletion of accidentally ingested incorrect time series. It shouldn't
|
||||
|
|
|
@ -86,10 +86,7 @@ func MetricsFindHandler(startTime time.Time, at *auth.Token, w http.ResponseWrit
|
|||
}
|
||||
paths = deduplicatePaths(paths, delimiter)
|
||||
sortPaths(paths, delimiter)
|
||||
contentType := "application/json; charset=utf-8"
|
||||
if jsonp != "" {
|
||||
contentType = "text/javascript; charset=utf-8"
|
||||
}
|
||||
contentType := getContentType(jsonp)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
bw := bufferedwriter.Get(w)
|
||||
defer bufferedwriter.Put(bw)
|
||||
|
@ -173,10 +170,7 @@ func MetricsExpandHandler(startTime time.Time, at *auth.Token, w http.ResponseWr
|
|||
}
|
||||
m[query] = paths
|
||||
}
|
||||
contentType := "application/json; charset=utf-8"
|
||||
if jsonp != "" {
|
||||
contentType = "text/javascript; charset=utf-8"
|
||||
}
|
||||
contentType := getContentType(jsonp)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if groupByExpr {
|
||||
for _, paths := range m {
|
||||
|
@ -223,10 +217,7 @@ func MetricsIndexHandler(startTime time.Time, at *auth.Token, w http.ResponseWri
|
|||
if err != nil {
|
||||
return fmt.Errorf(`cannot obtain metric names: %w`, err)
|
||||
}
|
||||
contentType := "application/json; charset=utf-8"
|
||||
if jsonp != "" {
|
||||
contentType = "text/javascript; charset=utf-8"
|
||||
}
|
||||
contentType := getContentType(jsonp)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
bw := bufferedwriter.Get(w)
|
||||
defer bufferedwriter.Put(bw)
|
||||
|
@ -429,3 +420,10 @@ var regexpCache = make(map[regexpCacheKey]*regexpCacheEntry)
|
|||
var regexpCacheLock sync.Mutex
|
||||
|
||||
const maxRegexpCacheSize = 10000
|
||||
|
||||
func getContentType(jsonp string) string {
|
||||
if jsonp == "" {
|
||||
return "application/json; charset=utf-8"
|
||||
}
|
||||
return "text/javascript; charset=utf-8"
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package graphite
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -16,6 +17,93 @@ import (
|
|||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
// TagsAutoCompleteTagsHandler implements /tags/autoComplete/tags endpoint from Graphite Tags API.
|
||||
//
|
||||
// See https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support
|
||||
func TagsAutoCompleteTagsHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter, r *http.Request) error {
|
||||
deadline := searchutils.GetDeadlineForQuery(r, startTime)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return fmt.Errorf("cannot parse form values: %w", err)
|
||||
}
|
||||
limit, err := getInt(r, "limit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if limit <= 0 {
|
||||
// Use limit=100 by default. See https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support
|
||||
limit = 100
|
||||
}
|
||||
tagPrefix := r.FormValue("tagPrefix")
|
||||
exprs := r.Form["expr"]
|
||||
denyPartialResponse := searchutils.GetDenyPartialResponse(r)
|
||||
var labels []string
|
||||
isPartial := false
|
||||
if len(exprs) == 0 {
|
||||
// Fast path: there are no `expr` filters.
|
||||
|
||||
// Escape special chars in tagPrefix as Graphite does.
|
||||
// See https://github.com/graphite-project/graphite-web/blob/3ad279df5cb90b211953e39161df416e54a84948/webapp/graphite/tags/base.py#L181
|
||||
filter := regexp.QuoteMeta(tagPrefix)
|
||||
labels, isPartial, err = netstorage.GetGraphiteTags(at, denyPartialResponse, filter, limit, deadline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Slow path: use netstorage.SearchMetricNames for applying `expr` filters.
|
||||
tfs, err := exprsToTagFilters(exprs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ct := time.Now().UnixNano() / 1e6
|
||||
sq := &storage.SearchQuery{
|
||||
MinTimestamp: 0,
|
||||
MaxTimestamp: ct,
|
||||
TagFilterss: [][]storage.TagFilter{tfs},
|
||||
}
|
||||
mns, isPartialResponse, err := netstorage.SearchMetricNames(at, denyPartialResponse, sq, deadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot fetch metric names for %q: %w", sq, err)
|
||||
}
|
||||
isPartial = isPartialResponse
|
||||
m := make(map[string]struct{})
|
||||
for _, mn := range mns {
|
||||
m["name"] = struct{}{}
|
||||
for _, tag := range mn.Tags {
|
||||
m[string(tag.Key)] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(tagPrefix) > 0 {
|
||||
for label := range m {
|
||||
if !strings.HasPrefix(label, tagPrefix) {
|
||||
delete(m, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
labels = make([]string, 0, len(m))
|
||||
for label := range m {
|
||||
labels = append(labels, label)
|
||||
}
|
||||
sort.Strings(labels)
|
||||
if limit > 0 && limit < len(labels) {
|
||||
labels = labels[:limit]
|
||||
}
|
||||
}
|
||||
|
||||
jsonp := r.FormValue("jsonp")
|
||||
contentType := getContentType(jsonp)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
bw := bufferedwriter.Get(w)
|
||||
defer bufferedwriter.Put(bw)
|
||||
WriteTagsAutoCompleteTagsResponse(bw, isPartial, labels, jsonp)
|
||||
if err := bw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
tagsAutoCompleteTagsDuration.UpdateDuration(startTime)
|
||||
return nil
|
||||
}
|
||||
|
||||
var tagsAutoCompleteTagsDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/tags/autoComplete/tags"}`)
|
||||
|
||||
// TagsFindSeriesHandler implements /tags/findSeries endpoint from Graphite Tags API.
|
||||
//
|
||||
// See https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags
|
||||
|
@ -32,18 +120,10 @@ func TagsFindSeriesHandler(startTime time.Time, at *auth.Token, w http.ResponseW
|
|||
if len(exprs) == 0 {
|
||||
return fmt.Errorf("expecting at least one `expr` query arg")
|
||||
}
|
||||
|
||||
// Convert exprs to []storage.TagFilter
|
||||
tfs := make([]storage.TagFilter, 0, len(exprs))
|
||||
for _, expr := range exprs {
|
||||
tf, err := parseFilterExpr(expr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse `expr` query arg: %w", err)
|
||||
}
|
||||
tfs = append(tfs, *tf)
|
||||
tfs, err := exprsToTagFilters(exprs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the request to storage
|
||||
ct := time.Now().UnixNano() / 1e6
|
||||
sq := &storage.SearchQuery{
|
||||
MinTimestamp: 0,
|
||||
|
@ -171,6 +251,18 @@ func getInt(r *http.Request, argName string) (int, error) {
|
|||
return n, nil
|
||||
}
|
||||
|
||||
func exprsToTagFilters(exprs []string) ([]storage.TagFilter, error) {
|
||||
tfs := make([]storage.TagFilter, 0, len(exprs))
|
||||
for _, expr := range exprs {
|
||||
tf, err := parseFilterExpr(expr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse `expr` query arg: %w", err)
|
||||
}
|
||||
tfs = append(tfs, *tf)
|
||||
}
|
||||
return tfs, nil
|
||||
}
|
||||
|
||||
func parseFilterExpr(s string) (*storage.TagFilter, error) {
|
||||
n := strings.Index(s, "=")
|
||||
if n < 0 {
|
||||
|
|
16
app/vmselect/graphite/tags_autocomplete_tags_response.qtpl
Normal file
16
app/vmselect/graphite/tags_autocomplete_tags_response.qtpl
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% stripspace %}
|
||||
|
||||
TagsAutoCompleteTagsResponse generates response for /tags/autoComplete/tags handler in Graphite Tags API.
|
||||
See https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support
|
||||
{% func TagsAutoCompleteTagsResponse(isPartial bool, labels []string, jsonp string) %}
|
||||
{% if jsonp != "" %}{%s= jsonp %}({% endif %}
|
||||
[
|
||||
{% for i, label := range labels %}
|
||||
{%q= label %}
|
||||
{% if i+1 < len(labels) %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
{% if jsonp != "" %}){% endif %}
|
||||
{% endfunc %}
|
||||
|
||||
{% endstripspace %}
|
|
@ -0,0 +1,81 @@
|
|||
// Code generated by qtc from "tags_autocomplete_tags_response.qtpl". DO NOT EDIT.
|
||||
// See https://github.com/valyala/quicktemplate for details.
|
||||
|
||||
// TagsAutoCompleteTagsResponse generates response for /tags/autoComplete/tags handler in Graphite Tags API.See https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support
|
||||
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:5
|
||||
package graphite
|
||||
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:5
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:5
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:5
|
||||
func StreamTagsAutoCompleteTagsResponse(qw422016 *qt422016.Writer, isPartial bool, labels []string, jsonp string) {
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:6
|
||||
if jsonp != "" {
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:6
|
||||
qw422016.N().S(jsonp)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:6
|
||||
qw422016.N().S(`(`)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:6
|
||||
}
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:6
|
||||
qw422016.N().S(`[`)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:8
|
||||
for i, label := range labels {
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:9
|
||||
qw422016.N().Q(label)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:10
|
||||
if i+1 < len(labels) {
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:10
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:10
|
||||
}
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:11
|
||||
}
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:11
|
||||
qw422016.N().S(`]`)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:13
|
||||
if jsonp != "" {
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:13
|
||||
qw422016.N().S(`)`)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:13
|
||||
}
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
}
|
||||
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
func WriteTagsAutoCompleteTagsResponse(qq422016 qtio422016.Writer, isPartial bool, labels []string, jsonp string) {
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
StreamTagsAutoCompleteTagsResponse(qw422016, isPartial, labels, jsonp)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
}
|
||||
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
func TagsAutoCompleteTagsResponse(isPartial bool, labels []string, jsonp string) string {
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
WriteTagsAutoCompleteTagsResponse(qb422016, isPartial, labels, jsonp)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
return qs422016
|
||||
//line app/vmselect/graphite/tags_autocomplete_tags_response.qtpl:14
|
||||
}
|
|
@ -209,7 +209,7 @@ func selectHandler(startTime time.Time, w http.ResponseWriter, r *http.Request,
|
|||
return true
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(p.Suffix, "graphite/tags/") && p.Suffix != "graphite/tags/findSeries" {
|
||||
if strings.HasPrefix(p.Suffix, "graphite/tags/") && !isGraphiteTagsPath(p.Suffix[len("graphite"):]) {
|
||||
tagName := p.Suffix[len("graphite/tags/"):]
|
||||
graphiteTagValuesRequests.Inc()
|
||||
if err := graphite.TagValuesHandler(startTime, at, tagName, w, r); err != nil {
|
||||
|
@ -362,6 +362,15 @@ func selectHandler(startTime time.Time, w http.ResponseWriter, r *http.Request,
|
|||
return true
|
||||
}
|
||||
return true
|
||||
case "/tags/autoComplete/tags":
|
||||
graphiteTagsAutoCompleteTagsRequests.Inc()
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := graphite.TagsAutoCompleteTagsHandler(startTime, at, w, r); err != nil {
|
||||
graphiteTagsAutoCompleteTagsErrors.Inc()
|
||||
httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
case "prometheus/api/v1/rules":
|
||||
// Return dumb placeholder
|
||||
rulesRequests.Inc()
|
||||
|
@ -401,6 +410,18 @@ func deleteHandler(startTime time.Time, w http.ResponseWriter, r *http.Request,
|
|||
}
|
||||
}
|
||||
|
||||
func isGraphiteTagsPath(path string) bool {
|
||||
switch path {
|
||||
// See https://graphite.readthedocs.io/en/stable/tags.html for a list of Graphite Tags API paths.
|
||||
// Do not include `/tags/<tag_name>` here, since this will fool the caller.
|
||||
case "/tags/tagSeries", "/tags/tagMultiSeries", "/tags/findSeries",
|
||||
"/tags/autoComplete/tags", "/tags/autoComplete/values", "/tags/delSeries":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func sendPrometheusError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
logger.Warnf("error in %q: %s", r.RequestURI, err)
|
||||
|
||||
|
@ -474,6 +495,9 @@ var (
|
|||
graphiteTagsFindSeriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/graphite/tags/findSeries"}`)
|
||||
graphiteTagsFindSeriesErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/select/{}/graphite/tags/findSeries"}`)
|
||||
|
||||
graphiteTagsAutoCompleteTagsRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/graphite/tags/autoComplete/tags"}`)
|
||||
graphiteTagsAutoCompleteTagsErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/select/{}/graphite/tags/autoComplete/tags"}`)
|
||||
|
||||
rulesRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/prometheus/api/v1/rules"}`)
|
||||
alertsRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/prometheus/api/v1/alerts"}`)
|
||||
metadataRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/prometheus/api/v1/metadata"}`)
|
||||
|
|
|
@ -577,6 +577,7 @@ func GetGraphiteTags(at *auth.Token, denyPartialResponse bool, filter string, li
|
|||
for i := range labels {
|
||||
if labels[i] == "__name__" {
|
||||
labels[i] = "name"
|
||||
sort.Strings(labels)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,6 +208,7 @@ or [an alternative dashboard for VictoriaMetrics cluster](https://grafana.com/gr
|
|||
- `tags` - returns tag names. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
|
||||
- `tags/<tag_name>` - returns tag values for the given `<tag_name>`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
|
||||
- `tags/findSeries` - returns series matching the given `expr`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
|
||||
- `tags/autoComplete/tags` - returns tags matching the given `tagPrefix` and/or `expr`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support).
|
||||
|
||||
* URL for time series deletion: `http://<vmselect>:8481/delete/<accountID>/prometheus/api/v1/admin/tsdb/delete_series?match[]=<timeseries_selector_for_delete>`.
|
||||
Note that the `delete_series` handler should be used only in exceptional cases such as deletion of accidentally ingested incorrect time series. It shouldn't
|
||||
|
|
|
@ -551,6 +551,7 @@ VictoriaMetrics supports the following handlers from [Graphite Tags API](https:/
|
|||
* [/tags](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags)
|
||||
* [/tags/tag_name](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags)
|
||||
* [/tags/findSeries](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags)
|
||||
* [/tags/autoComplete/tags](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support)
|
||||
|
||||
|
||||
### How to build from sources
|
||||
|
|
Loading…
Reference in a new issue