diff --git a/README.md b/README.md index 6e94e61b3..3c9e493fd 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,7 @@ or [an alternative dashboard for VictoriaMetrics cluster](https://grafana.com/gr - `metrics/expand` - expands Graphite metrics. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand). - `metrics/index.json` - returns all the metric names. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json). - `tags` - returns tag names. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags). + - `tags/` - returns tag values for the given ``. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags). * URL for time series deletion: `http://:8481/delete//prometheus/api/v1/admin/tsdb/delete_series?match[]=`. 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 diff --git a/app/vmselect/graphite/tag_values_response.qtpl b/app/vmselect/graphite/tag_values_response.qtpl new file mode 100644 index 000000000..497e37689 --- /dev/null +++ b/app/vmselect/graphite/tag_values_response.qtpl @@ -0,0 +1,21 @@ +{% stripspace %} + +Tags generates response for /tags/ handler +See https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags +{% func TagValuesResponse(isPartial bool, tagName string, tagValues []string) %} +{ + "tag":{%q= tagName %}, + {% if isPartial %}"is_partial":true,{% endif %} + "values":[ + {% for i, value := range tagValues %} + { + "count":1, + "value":{%q= value %} + } + {% if i+1 < len(tagValues) %},{% endif %} + {% endfor %} + ] +} +{% endfunc %} + +{% endstripspace %} diff --git a/app/vmselect/graphite/tag_values_response.qtpl.go b/app/vmselect/graphite/tag_values_response.qtpl.go new file mode 100644 index 000000000..747152f8a --- /dev/null +++ b/app/vmselect/graphite/tag_values_response.qtpl.go @@ -0,0 +1,83 @@ +// Code generated by qtc from "tag_values_response.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +// Tags generates response for /tags/ handlerSee https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags + +//line app/vmselect/graphite/tag_values_response.qtpl:5 +package graphite + +//line app/vmselect/graphite/tag_values_response.qtpl:5 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line app/vmselect/graphite/tag_values_response.qtpl:5 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line app/vmselect/graphite/tag_values_response.qtpl:5 +func StreamTagValuesResponse(qw422016 *qt422016.Writer, isPartial bool, tagName string, tagValues []string) { +//line app/vmselect/graphite/tag_values_response.qtpl:5 + qw422016.N().S(`{"tag":`) +//line app/vmselect/graphite/tag_values_response.qtpl:7 + qw422016.N().Q(tagName) +//line app/vmselect/graphite/tag_values_response.qtpl:7 + qw422016.N().S(`,`) +//line app/vmselect/graphite/tag_values_response.qtpl:8 + if isPartial { +//line app/vmselect/graphite/tag_values_response.qtpl:8 + qw422016.N().S(`"is_partial":true,`) +//line app/vmselect/graphite/tag_values_response.qtpl:8 + } +//line app/vmselect/graphite/tag_values_response.qtpl:8 + qw422016.N().S(`"values":[`) +//line app/vmselect/graphite/tag_values_response.qtpl:10 + for i, value := range tagValues { +//line app/vmselect/graphite/tag_values_response.qtpl:10 + qw422016.N().S(`{"count":1,"value":`) +//line app/vmselect/graphite/tag_values_response.qtpl:13 + qw422016.N().Q(value) +//line app/vmselect/graphite/tag_values_response.qtpl:13 + qw422016.N().S(`}`) +//line app/vmselect/graphite/tag_values_response.qtpl:15 + if i+1 < len(tagValues) { +//line app/vmselect/graphite/tag_values_response.qtpl:15 + qw422016.N().S(`,`) +//line app/vmselect/graphite/tag_values_response.qtpl:15 + } +//line app/vmselect/graphite/tag_values_response.qtpl:16 + } +//line app/vmselect/graphite/tag_values_response.qtpl:16 + qw422016.N().S(`]}`) +//line app/vmselect/graphite/tag_values_response.qtpl:19 +} + +//line app/vmselect/graphite/tag_values_response.qtpl:19 +func WriteTagValuesResponse(qq422016 qtio422016.Writer, isPartial bool, tagName string, tagValues []string) { +//line app/vmselect/graphite/tag_values_response.qtpl:19 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmselect/graphite/tag_values_response.qtpl:19 + StreamTagValuesResponse(qw422016, isPartial, tagName, tagValues) +//line app/vmselect/graphite/tag_values_response.qtpl:19 + qt422016.ReleaseWriter(qw422016) +//line app/vmselect/graphite/tag_values_response.qtpl:19 +} + +//line app/vmselect/graphite/tag_values_response.qtpl:19 +func TagValuesResponse(isPartial bool, tagName string, tagValues []string) string { +//line app/vmselect/graphite/tag_values_response.qtpl:19 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmselect/graphite/tag_values_response.qtpl:19 + WriteTagValuesResponse(qb422016, isPartial, tagName, tagValues) +//line app/vmselect/graphite/tag_values_response.qtpl:19 + qs422016 := string(qb422016.B) +//line app/vmselect/graphite/tag_values_response.qtpl:19 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmselect/graphite/tag_values_response.qtpl:19 + return qs422016 +//line app/vmselect/graphite/tag_values_response.qtpl:19 +} diff --git a/app/vmselect/graphite/tags_api.go b/app/vmselect/graphite/tags_api.go index 2615ba2bf..3ad968141 100644 --- a/app/vmselect/graphite/tags_api.go +++ b/app/vmselect/graphite/tags_api.go @@ -14,7 +14,49 @@ import ( "github.com/VictoriaMetrics/metrics" ) -// TagsHandler implements handler for /tags endpoint. +// TagValuesHandler implements /tags/ endpoint from Graphite Tags API. +// +// See https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags +func TagValuesHandler(startTime time.Time, at *auth.Token, tagName string, 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 := 0 + if limitStr := r.FormValue("limit"); len(limitStr) > 0 { + var err error + limit, err = strconv.Atoi(limitStr) + if err != nil { + return fmt.Errorf("cannot parse limit=%q: %w", limit, err) + } + } + denyPartialResponse := searchutils.GetDenyPartialResponse(r) + tagValues, isPartial, err := netstorage.GetGraphiteTagValues(at, denyPartialResponse, tagName, limit, deadline) + if err != nil { + return err + } + filter := r.FormValue("filter") + if len(filter) > 0 { + tagValues, err = applyRegexpFilter(filter, tagValues) + if err != nil { + return err + } + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + bw := bufferedwriter.Get(w) + defer bufferedwriter.Put(bw) + WriteTagValuesResponse(bw, isPartial, tagName, tagValues) + if err := bw.Flush(); err != nil { + return err + } + tagValuesDuration.UpdateDuration(startTime) + return nil +} + +var tagValuesDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/tags/"}`) + +// TagsHandler implements /tags endpoint from Graphite Tags API. // // See https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags func TagsHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter, r *http.Request) error { @@ -37,20 +79,10 @@ func TagsHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter, r * } filter := r.FormValue("filter") if len(filter) > 0 { - // Anchor filter regexp to the beginning of the string as Graphite does. - // See https://github.com/graphite-project/graphite-web/blob/3ad279df5cb90b211953e39161df416e54a84948/webapp/graphite/tags/localdatabase.py#L157 - filter = "^(?:" + filter + ")" - re, err := regexp.Compile(filter) + labels, err = applyRegexpFilter(filter, labels) if err != nil { - return fmt.Errorf("cannot parse regexp filter=%q: %w", filter, err) + return err } - dst := labels[:0] - for _, label := range labels { - if re.MatchString(label) { - dst = append(dst, label) - } - } - labels = dst } w.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -65,3 +97,20 @@ func TagsHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter, r * } var tagsDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/tags"}`) + +func applyRegexpFilter(filter string, ss []string) ([]string, error) { + // Anchor filter regexp to the beginning of the string as Graphite does. + // See https://github.com/graphite-project/graphite-web/blob/3ad279df5cb90b211953e39161df416e54a84948/webapp/graphite/tags/localdatabase.py#L157 + filter = "^(?:" + filter + ")" + re, err := regexp.Compile(filter) + if err != nil { + return nil, fmt.Errorf("cannot parse regexp filter=%q: %w", filter, err) + } + dst := ss[:0] + for _, s := range ss { + if re.MatchString(s) { + dst = append(dst, s) + } + } + return dst, nil +} diff --git a/app/vmselect/main.go b/app/vmselect/main.go index 31063e682..46cc83371 100644 --- a/app/vmselect/main.go +++ b/app/vmselect/main.go @@ -209,6 +209,16 @@ func selectHandler(startTime time.Time, w http.ResponseWriter, r *http.Request, return true } } + if strings.HasPrefix(p.Suffix, "graphite/tags/") { + tagName := p.Suffix[len("graphite/tags/"):] + graphiteTagValuesRequests.Inc() + if err := graphite.TagValuesHandler(startTime, at, tagName, w, r); err != nil { + graphiteTagValuesErrors.Inc() + httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err) + return true + } + return true + } switch p.Suffix { case "prometheus/api/v1/query": @@ -450,6 +460,9 @@ var ( graphiteTagsRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/graphite/tags"}`) graphiteTagsErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/select/{}/graphite/tags"}`) + graphiteTagValuesRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/graphite/tags/"}`) + graphiteTagValuesErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/select/{}/graphite/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"}`) diff --git a/app/vmselect/netstorage/netstorage.go b/app/vmselect/netstorage/netstorage.go index 2a42f7c6f..3bbbfa71b 100644 --- a/app/vmselect/netstorage/netstorage.go +++ b/app/vmselect/netstorage/netstorage.go @@ -713,6 +713,24 @@ func GetLabelValuesOnTimeRange(at *auth.Token, denyPartialResponse bool, labelNa return labelValues, isPartial, nil } +// GetGraphiteTagValues returns tag values for the given tagName until the given deadline. +func GetGraphiteTagValues(at *auth.Token, denyPartialResponse bool, tagName string, limit int, deadline searchutils.Deadline) ([]string, bool, error) { + if deadline.Exceeded() { + return nil, false, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String()) + } + if tagName == "name" { + tagName = "" + } + tagValues, isPartial, err := GetLabelValues(at, denyPartialResponse, tagName, deadline) + if err != nil { + return nil, false, err + } + if limit < len(tagValues) { + tagValues = tagValues[:limit] + } + return tagValues, isPartial, nil +} + // GetLabelValues returns label values for the given labelName // until the given deadline. func GetLabelValues(at *auth.Token, denyPartialResponse bool, labelName string, deadline searchutils.Deadline) ([]string, bool, error) { diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md index 6e94e61b3..3c9e493fd 100644 --- a/docs/Cluster-VictoriaMetrics.md +++ b/docs/Cluster-VictoriaMetrics.md @@ -206,6 +206,7 @@ or [an alternative dashboard for VictoriaMetrics cluster](https://grafana.com/gr - `metrics/expand` - expands Graphite metrics. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand). - `metrics/index.json` - returns all the metric names. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json). - `tags` - returns tag names. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags). + - `tags/` - returns tag values for the given ``. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags). * URL for time series deletion: `http://:8481/delete//prometheus/api/v1/admin/tsdb/delete_series?match[]=`. 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 diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md index 2f0385969..859d44967 100644 --- a/docs/Single-server-VictoriaMetrics.md +++ b/docs/Single-server-VictoriaMetrics.md @@ -549,6 +549,7 @@ VictoriaMetrics supports the following handlers from [Graphite Tags API](https:/ * [/tags/tagSeries](https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb) * [/tags/tagMultiSeries](https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb) * [/tags](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags) +* [/tags/tag_name](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags) ### How to build from sources