From 795d3fe722a36f8d359fd0dff9b659e1eec77b8e Mon Sep 17 00:00:00 2001 From: f41gh7 Date: Wed, 16 Apr 2025 20:09:35 +0200 Subject: [PATCH] lib/storage: enhance TSDB status response This commit adds new fields - `requestsCount` and `lastRequestTimestamp` to series count be metric names stats. It allows to display an additional stats at explore cardinality page. Stats will only be added if `storage.trackMetricNameStats` flag is set. This change requires an update to RPC protocol in order to properly marshal data. In addition, this commit adds integration tests to TSDB stats API. Related issue: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6145 --- .../prometheus/tsdb_status_response.qtpl | 30 +++++- .../prometheus/tsdb_status_response.qtpl.go | 92 ++++++++++++++++++- apptest/model.go | 50 ++++++++++ apptest/tests/metric_names_stats_test.go | 56 +++++++++++ apptest/vmselect.go | 31 +++++++ apptest/vmsingle.go | 31 +++++++ docs/victoriametrics/README.md | 2 + docs/victoriametrics/changelog/CHANGELOG.md | 5 + lib/storage/index_db.go | 1 + lib/storage/metricnamestats/tracker.go | 27 ++++++ lib/storage/storage.go | 17 +++- 11 files changed, 337 insertions(+), 5 deletions(-) diff --git a/app/vmselect/prometheus/tsdb_status_response.qtpl b/app/vmselect/prometheus/tsdb_status_response.qtpl index ef327e45d6..2c158035ef 100644 --- a/app/vmselect/prometheus/tsdb_status_response.qtpl +++ b/app/vmselect/prometheus/tsdb_status_response.qtpl @@ -11,7 +11,7 @@ TSDBStatusResponse generates response for /api/v1/status/tsdb . "data":{ "totalSeries": {%dul= status.TotalSeries %}, "totalLabelValuePairs": {%dul= status.TotalLabelValuePairs %}, - "seriesCountByMetricName":{%= tsdbStatusEntries(status.SeriesCountByMetricName) %}, + "seriesCountByMetricName":{%= tsdbStatusMetricNameEntries(status.SeriesCountByMetricName,status.SeriesQueryStatsByMetricName) %}, "seriesCountByLabelName":{%= tsdbStatusEntries(status.SeriesCountByLabelName) %}, "seriesCountByFocusLabelValue":{%= tsdbStatusEntries(status.SeriesCountByFocusLabelValue) %}, "seriesCountByLabelValuePair":{%= tsdbStatusEntries(status.SeriesCountByLabelValuePair) %}, @@ -34,4 +34,32 @@ TSDBStatusResponse generates response for /api/v1/status/tsdb . ] {% endfunc %} +{% func tsdbStatusMetricNameEntries(a []storage.TopHeapEntry, queryStats []storage.MetricNamesStatsRecord) %} +{% code + queryStatsByMetricName := make(map[string]storage.MetricNamesStatsRecord,len(queryStats)) + for _, record := range queryStats{ + queryStatsByMetricName[record.MetricName] = record + } +%} +[ + {% for i, e := range a %} + { + {% code + entry, ok := queryStatsByMetricName[e.Name] + %} + "name":{%q= e.Name %}, + {% if !ok %} + "value":{%d= int(e.Count) %} + {% else %} + "value":{%d= int(e.Count) %}, + "requestsCount":{%d= int(entry.RequestsCount) %}, + "lastRequestTimestamp":{%d= int(entry.LastRequestTs) %} + {% endif %} + } + {% if i+1 < len(a) %},{% endif %} + {% endfor %} +] +{% endfunc %} + + {% endstripspace %} diff --git a/app/vmselect/prometheus/tsdb_status_response.qtpl.go b/app/vmselect/prometheus/tsdb_status_response.qtpl.go index b138b8e024..5af5cbf2b0 100644 --- a/app/vmselect/prometheus/tsdb_status_response.qtpl.go +++ b/app/vmselect/prometheus/tsdb_status_response.qtpl.go @@ -37,9 +37,9 @@ func StreamTSDBStatusResponse(qw422016 *qt422016.Writer, status *storage.TSDBSta qw422016.N().DUL(status.TotalLabelValuePairs) //line app/vmselect/prometheus/tsdb_status_response.qtpl:13 qw422016.N().S(`,"seriesCountByMetricName":`) -//line app/vmselect/prometheus/tsdb_status_response.qtpl:14 - streamtsdbStatusEntries(qw422016, status.SeriesCountByMetricName) -//line app/vmselect/prometheus/tsdb_status_response.qtpl:14 +//line app/vmselect/prometheus/tsdb_status_response.qtpl:15 + streamtsdbStatusMetricNameEntries(qw422016, status.SeriesCountByMetricName, status.SeriesQueryStatsByMetricName) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:15 qw422016.N().S(`,"seriesCountByLabelName":`) //line app/vmselect/prometheus/tsdb_status_response.qtpl:15 streamtsdbStatusEntries(qw422016, status.SeriesCountByLabelName) @@ -147,3 +147,89 @@ func tsdbStatusEntries(a []storage.TopHeapEntry) string { return qs422016 //line app/vmselect/prometheus/tsdb_status_response.qtpl:35 } + +//line app/vmselect/prometheus/tsdb_status_response.qtpl:38 +func streamtsdbStatusMetricNameEntries(qw422016 *qt422016.Writer, a []storage.TopHeapEntry, queryStats []storage.MetricNamesStatsRecord) { +//line app/vmselect/prometheus/tsdb_status_response.qtpl:40 + queryStatsByMetricName := make(map[string]storage.MetricNamesStatsRecord, len(queryStats)) + for _, record := range queryStats { + queryStatsByMetricName[record.MetricName] = record + } + +//line app/vmselect/prometheus/tsdb_status_response.qtpl:44 + qw422016.N().S(`[`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:46 + for i, e := range a { +//line app/vmselect/prometheus/tsdb_status_response.qtpl:46 + qw422016.N().S(`{`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:49 + entry, ok := queryStatsByMetricName[e.Name] + +//line app/vmselect/prometheus/tsdb_status_response.qtpl:50 + qw422016.N().S(`"name":`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:51 + qw422016.N().Q(e.Name) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:51 + qw422016.N().S(`,`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:52 + if !ok { +//line app/vmselect/prometheus/tsdb_status_response.qtpl:52 + qw422016.N().S(`"value":`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:53 + qw422016.N().D(int(e.Count)) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:54 + } else { +//line app/vmselect/prometheus/tsdb_status_response.qtpl:54 + qw422016.N().S(`"value":`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:55 + qw422016.N().D(int(e.Count)) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:55 + qw422016.N().S(`,"requestsCount":`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:56 + qw422016.N().D(int(entry.RequestsCount)) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:56 + qw422016.N().S(`,"lastRequestTimestamp":`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:57 + qw422016.N().D(int(entry.LastRequestTs)) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:58 + } +//line app/vmselect/prometheus/tsdb_status_response.qtpl:58 + qw422016.N().S(`}`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:60 + if i+1 < len(a) { +//line app/vmselect/prometheus/tsdb_status_response.qtpl:60 + qw422016.N().S(`,`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:60 + } +//line app/vmselect/prometheus/tsdb_status_response.qtpl:61 + } +//line app/vmselect/prometheus/tsdb_status_response.qtpl:61 + qw422016.N().S(`]`) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 +} + +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 +func writetsdbStatusMetricNameEntries(qq422016 qtio422016.Writer, a []storage.TopHeapEntry, queryStats []storage.MetricNamesStatsRecord) { +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 + qw422016 := qt422016.AcquireWriter(qq422016) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 + streamtsdbStatusMetricNameEntries(qw422016, a, queryStats) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 + qt422016.ReleaseWriter(qw422016) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 +} + +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 +func tsdbStatusMetricNameEntries(a []storage.TopHeapEntry, queryStats []storage.MetricNamesStatsRecord) string { +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 + qb422016 := qt422016.AcquireByteBuffer() +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 + writetsdbStatusMetricNameEntries(qb422016, a, queryStats) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 + qs422016 := string(qb422016.B) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 + qt422016.ReleaseByteBuffer(qb422016) +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 + return qs422016 +//line app/vmselect/prometheus/tsdb_status_response.qtpl:63 +} diff --git a/apptest/model.go b/apptest/model.go index 4faf9547dd..0a6952a5d2 100644 --- a/apptest/model.go +++ b/apptest/model.go @@ -344,3 +344,53 @@ type SnapshotDeleteResponse struct { type SnapshotDeleteAllResponse struct { Status string } + +// TSDBStatusResponse is an in-memory reprensentation of the json response +// returned by the /prometheus/api/v1/status/tsdb endpoint. +type TSDBStatusResponse struct { + IsPartial bool + Data TSDBStatusResponseData +} + +// Sort performs sorting of stats entries +func (tsr *TSDBStatusResponse) Sort() { + sortTSDBStatusResponseEntries(tsr.Data.SeriesCountByLabelName) + sortTSDBStatusResponseEntries(tsr.Data.SeriesCountByFocusLabelValue) + sortTSDBStatusResponseEntries(tsr.Data.SeriesCountByLabelValuePair) + sortTSDBStatusResponseEntries(tsr.Data.LabelValueCountByLabelName) +} + +// TSDBStatusResponseData is a part of TSDBStatusResponse +type TSDBStatusResponseData struct { + TotalSeries int + TotalLabelValuePairs int + SeriesCountByMetricName []TSDBStatusResponseMetricNameEntry + SeriesCountByLabelName []TSDBStatusResponseEntry + SeriesCountByFocusLabelValue []TSDBStatusResponseEntry + SeriesCountByLabelValuePair []TSDBStatusResponseEntry + LabelValueCountByLabelName []TSDBStatusResponseEntry +} + +// TSDBStatusResponseEntry defines stats entry for TSDBStatusResponseData +type TSDBStatusResponseEntry struct { + Name string + Count int +} + +// TSDBStatusResponseMetricNameEntry defines metric names stats entry for TSDBStatusResponseData +type TSDBStatusResponseMetricNameEntry struct { + Name string + Count int + RequestsCount int + LastRequestTimestamp int +} + +func sortTSDBStatusResponseEntries(entries []TSDBStatusResponseEntry) { + sort.Slice(entries, func(i, j int) bool { + left, right := entries[i], entries[j] + if left.Count == right.Count { + return left.Name < right.Name + } + return left.Count < right.Count + }) +} diff --git a/apptest/tests/metric_names_stats_test.go b/apptest/tests/metric_names_stats_test.go index db49e111d1..67f1038a61 100644 --- a/apptest/tests/metric_names_stats_test.go +++ b/apptest/tests/metric_names_stats_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/VictoriaMetrics/VictoriaMetrics/apptest" at "github.com/VictoriaMetrics/VictoriaMetrics/apptest" @@ -19,6 +20,7 @@ func TestSingleMetricNamesStats(t *testing.T) { const ingestDateTime = `2024-02-05T08:57:36.700Z` const ingestTimestamp = ` 1707123456700` + const date = `2024-02-05` dataSet := []string{ `metric_name_1{label="foo"} 10`, `metric_name_1{label="bar"} 10`, @@ -29,6 +31,7 @@ func TestSingleMetricNamesStats(t *testing.T) { for idx := range dataSet { dataSet[idx] += ingestTimestamp } + tsdbMetricNameEntryCmpOpts := cmpopts.IgnoreFields(apptest.TSDBStatusResponseMetricNameEntry{}, "LastRequestTimestamp") sut.PrometheusAPIV1ImportPrometheus(t, dataSet, at.QueryOpts{}) sut.ForceFlush(t) @@ -60,6 +63,31 @@ func TestSingleMetricNamesStats(t *testing.T) { t.Errorf("unexpected response (-want, +got):\n%s", diff) } + expectedStatsResponse := apptest.TSDBStatusResponse{ + Data: at.TSDBStatusResponseData{ + TotalSeries: 5, + TotalLabelValuePairs: 10, + SeriesCountByMetricName: []apptest.TSDBStatusResponseMetricNameEntry{ + {Name: "metric_name_1", RequestsCount: 3}, + {Name: "metric_name_2", RequestsCount: 1}, + {Name: "metric_name_3", RequestsCount: 1}, + }, + SeriesCountByLabelName: []apptest.TSDBStatusResponseEntry{{Name: "__name__"}, {Name: "label"}}, + SeriesCountByFocusLabelValue: []apptest.TSDBStatusResponseEntry{}, + SeriesCountByLabelValuePair: []apptest.TSDBStatusResponseEntry{ + {Name: "__name__=metric_name_1"}, {Name: "label=baz"}, + {Name: "__name__=metric_name_2"}, {Name: "__name__=metric_name_3"}, + {Name: "label=bar"}, {Name: "label=foo"}, + }, + LabelValueCountByLabelName: []apptest.TSDBStatusResponseEntry{{Name: "__name__"}, {Name: "label"}}, + }, + } + expectedStatsResponse.Sort() + gotStatus := sut.APIV1StatusTSDB(t, "", date, "", apptest.QueryOpts{}) + if diff := cmp.Diff(expectedStatsResponse, gotStatus, tsdbMetricNameEntryCmpOpts); diff != "" { + t.Errorf("unexpected APIV1StatusTSDB response (-want, +got):\n%s", diff) + } + // perform query request for single metric and check counter increase sut.PrometheusAPIV1Query(t, `metric_name_2`, at.QueryOpts{Time: ingestDateTime}) expected = apptest.MetricNamesStatsResponse{ @@ -129,6 +157,7 @@ func TestClusterMetricNamesStats(t *testing.T) { const ingestDateTime = `2024-02-05T08:57:36.700Z` const ingestTimestamp = ` 1707123456700` + const date = `2024-02-05` dataSet := []string{ `metric_name_1{label="foo"} 10`, `metric_name_1{label="bar"} 10`, @@ -140,6 +169,8 @@ func TestClusterMetricNamesStats(t *testing.T) { dataSet[idx] += ingestTimestamp } + tsdbMetricNameEntryCmpOpts := cmpopts.IgnoreFields(apptest.TSDBStatusResponseMetricNameEntry{}, "LastRequestTimestamp") + // ingest per tenant data and verify it with search tenantIDs := []string{"1:1", "1:15", "15:15"} for _, tenantID := range tenantIDs { @@ -176,6 +207,31 @@ func TestClusterMetricNamesStats(t *testing.T) { if diff := cmp.Diff(expected, gotStats); diff != "" { t.Errorf("unexpected response tenant: %s (-want, +got):\n%s", tenantID, diff) } + + expectedStatsResponse := apptest.TSDBStatusResponse{ + Data: at.TSDBStatusResponseData{ + TotalSeries: 5, + TotalLabelValuePairs: 10, + SeriesCountByMetricName: []apptest.TSDBStatusResponseMetricNameEntry{ + {Name: "metric_name_1", RequestsCount: 3}, + {Name: "metric_name_2", RequestsCount: 1}, + {Name: "metric_name_3", RequestsCount: 1}, + }, + SeriesCountByLabelName: []apptest.TSDBStatusResponseEntry{{Name: "__name__"}, {Name: "label"}}, + SeriesCountByFocusLabelValue: []apptest.TSDBStatusResponseEntry{}, + SeriesCountByLabelValuePair: []apptest.TSDBStatusResponseEntry{ + {Name: "__name__=metric_name_1"}, {Name: "label=baz"}, + {Name: "__name__=metric_name_2"}, {Name: "__name__=metric_name_3"}, + {Name: "label=bar"}, {Name: "label=foo"}, + }, + LabelValueCountByLabelName: []apptest.TSDBStatusResponseEntry{{Name: "__name__"}, {Name: "label"}}, + }, + } + expectedStatsResponse.Sort() + gotStatus := vmselect.APIV1StatusTSDB(t, "", date, "", apptest.QueryOpts{Tenant: tenantID}) + if diff := cmp.Diff(expectedStatsResponse, gotStatus, tsdbMetricNameEntryCmpOpts); diff != "" { + t.Errorf("unexpected APIV1StatusTSDB response tenant: %s (-want, +got):\n%s", tenantID, diff) + } } // verify multitenant stats diff --git a/apptest/vmselect.go b/apptest/vmselect.go index 242bcb3f4a..9701db57b1 100644 --- a/apptest/vmselect.go +++ b/apptest/vmselect.go @@ -174,6 +174,37 @@ func (app *Vmselect) MetricNamesStatsReset(t *testing.T, opts QueryOpts) { } } +// APIV1StatusTSDB sends a query to a /prometheus/api/v1/status/tsdb +// // +// See https://docs.victoriametrics.com/#tsdb-stats +func (app *Vmselect) APIV1StatusTSDB(t *testing.T, matchQuery string, date string, topN string, opts QueryOpts) TSDBStatusResponse { + t.Helper() + + seriesURL := fmt.Sprintf("http://%s/select/%s/prometheus/api/v1/status/tsdb", app.httpListenAddr, opts.getTenant()) + values := opts.asURLValues() + addNonEmpty := func(name, value string) { + if len(value) == 0 { + return + } + values.Add(name, value) + } + addNonEmpty("match[]", matchQuery) + addNonEmpty("topN", topN) + addNonEmpty("date", date) + + res, statusCode := app.cli.PostForm(t, seriesURL, values) + if statusCode != http.StatusOK { + t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res) + } + + var status TSDBStatusResponse + if err := json.Unmarshal([]byte(res), &status); err != nil { + t.Fatalf("could not unmarshal tsdb status response data:\n%s\n err: %v", res, err) + } + status.Sort() + return status +} + // String returns the string representation of the vmselect app state. func (app *Vmselect) String() string { return fmt.Sprintf("{app: %s httpListenAddr: %q}", app.app, app.httpListenAddr) diff --git a/apptest/vmsingle.go b/apptest/vmsingle.go index bccc4cbb6a..07392c64fb 100644 --- a/apptest/vmsingle.go +++ b/apptest/vmsingle.go @@ -350,6 +350,37 @@ func (app *Vmsingle) SnapshotDeleteAll(t *testing.T) *SnapshotDeleteAllResponse return &res } +// APIV1StatusTSDB sends a query to a /prometheus/api/v1/status/tsdb +// // +// See https://docs.victoriametrics.com/#tsdb-stats +func (app *Vmsingle) APIV1StatusTSDB(t *testing.T, matchQuery string, date string, topN string, opts QueryOpts) TSDBStatusResponse { + t.Helper() + + seriesURL := fmt.Sprintf("http://%s/prometheus/api/v1/status/tsdb", app.httpListenAddr) + values := opts.asURLValues() + addNonEmpty := func(name, value string) { + if len(value) == 0 { + return + } + values.Add(name, value) + } + addNonEmpty("match[]", matchQuery) + addNonEmpty("topN", topN) + addNonEmpty("date", date) + + res, statusCode := app.cli.PostForm(t, seriesURL, values) + if statusCode != http.StatusOK { + t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", statusCode, http.StatusOK, res) + } + + var status TSDBStatusResponse + if err := json.Unmarshal([]byte(res), &status); err != nil { + t.Fatalf("could not unmarshal tsdb status response data:\n%s\n err: %v", res, err) + } + status.Sort() + return status +} + // HTTPAddr returns the address at which the vmstorage process is listening // for http connections. func (app *Vmsingle) HTTPAddr() string { diff --git a/docs/victoriametrics/README.md b/docs/victoriametrics/README.md index 7a797d98f7..978b3c07ae 100644 --- a/docs/victoriametrics/README.md +++ b/docs/victoriametrics/README.md @@ -2410,6 +2410,8 @@ due to [replication](#replication) or [rerouting](https://docs.victoriametrics.c VictoriaMetrics provides UI on top of `/api/v1/status/tsdb` - see [cardinality explorer docs](#cardinality-explorer). +VictoriaMetrics enhances Prometheus stats with `requestsCount` and `lastRequestTimestamp` for `seriesCountByMetricName`. This stats added if [tracking metric names stats](https://docs.victoriametrics.com/#track-ingested-metrics-usage) is configured. + ## Query tracing VictoriaMetrics supports query tracing, which can be used for determining bottlenecks during query processing. diff --git a/docs/victoriametrics/changelog/CHANGELOG.md b/docs/victoriametrics/changelog/CHANGELOG.md index c0226caacd..33d0782eba 100644 --- a/docs/victoriametrics/changelog/CHANGELOG.md +++ b/docs/victoriametrics/changelog/CHANGELOG.md @@ -18,10 +18,15 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/). ## tip +** Update Note 1: Updated the RPC cluster protocol version for the [TSDB status API](https://docs.victoriametrics.com/#tsdb-stats) from `tsdbStatus_v5` to `tsdbStatus_v6`. + The TSDB Status API will be temporarily unavailable during the version upgrade process. + This update requires both vmselect and vmstorage components to be running version v1.116.0 or higher for full compatibility. + * FEATURE: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): add command-line flag `-search.logSlowQueryStats`. This flag is available only in VictoriaMetrics [enterprise](https://docs.victoriametrics.com/enterprise/). See the following [docs](https://docs.victoriametrics.com/query-stats) for details. * FEATURE: all the VictoriaMetrics components: mask `authKey` value from log messages. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5973) for details. * FEATURE: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/), [vmagent](https://docs.victoriametrics.com/vmagent/): add helpful hints to the unexpected EOF error message in the write concurrency limiter. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8704) for details. * FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent/): use [VM remote write protocol](https://docs.victoriametrics.com/vmagent/#victoriametrics-remote-write-protocol) by default with automatic downgrade in runtime to Prometheus protocol when needed. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8462) for details. +* FEATURE: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and [vmstorage](https://docs.victoriametrics.com/cluster-victoriametrics/): add additional metric name stats to TSDB Status API response. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6145) for details and this [doc](https://docs.victoriametrics.com/#tsdb-stats). * BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent/): properly init [enterprise](https://docs.victoriametrics.com/enterprise/) version for `linux/arm` and non-CGO buids. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6019) for details. * BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent/): remote write client sets correct content encoding header based on actual body content, rather than relying on configuration. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8650). diff --git a/lib/storage/index_db.go b/lib/storage/index_db.go index 1468d0cbc9..ffc15d8cef 100644 --- a/lib/storage/index_db.go +++ b/lib/storage/index_db.go @@ -1461,6 +1461,7 @@ type TSDBStatus struct { SeriesCountByFocusLabelValue []TopHeapEntry SeriesCountByLabelValuePair []TopHeapEntry LabelValueCountByLabelName []TopHeapEntry + SeriesQueryStatsByMetricName []MetricNamesStatsRecord } func (status *TSDBStatus) hasEntries() bool { diff --git a/lib/storage/metricnamestats/tracker.go b/lib/storage/metricnamestats/tracker.go index 119333dcd9..4c5c235729 100644 --- a/lib/storage/metricnamestats/tracker.go +++ b/lib/storage/metricnamestats/tracker.go @@ -391,6 +391,33 @@ func (mt *Tracker) GetStatsForTenant(accountID, projectID uint32, limit, le int, return result } +// GetStatRecordsForNames returns stats records for the given metric names and tenant +func (mt *Tracker) GetStatRecordsForNames(accountID, projectID uint32, metricNames []string) []StatRecord { + if mt == nil { + return nil + } + mt.mu.RLock() + records := make([]StatRecord, 0, len(metricNames)) + for _, mn := range metricNames { + sk := statKey{ + accountID: accountID, + projectID: projectID, + metricName: mn, + } + si, ok := mt.store[sk] + if !ok { + continue + } + records = append(records, StatRecord{ + MetricName: mn, + RequestsCount: si.requestsCount.Load(), + LastRequestTs: si.lastRequestTs.Load(), + }) + } + mt.mu.RUnlock() + return records +} + // GetStats returns stats response for the tracked metrics // // DeduplicateMergeRecords must be called at cluster version on returned result. diff --git a/lib/storage/storage.go b/lib/storage/storage.go index 0b79b8dd3d..a03c913ffe 100644 --- a/lib/storage/storage.go +++ b/lib/storage/storage.go @@ -1705,7 +1705,19 @@ func (s *Storage) GetTSDBStatus(qt *querytracer.Tracer, tfss []*TagFilters, date if s.disablePerDayIndex { date = globalIndexDate } - return idb.GetTSDBStatus(qt, tfss, date, focusLabel, topN, maxMetrics, deadline) + res, err := idb.GetTSDBStatus(qt, tfss, date, focusLabel, topN, maxMetrics, deadline) + if err != nil { + return nil, err + } + if s.metricsTracker != nil && len(res.SeriesCountByMetricName) > 0 { + // for performance reason always check if metricsTracker is configured + names := make([]string, len(res.SeriesCountByMetricName)) + for idx, mns := range res.SeriesCountByMetricName { + names[idx] = mns.Name + } + res.SeriesQueryStatsByMetricName = s.metricsTracker.GetStatRecordsForNames(0, 0, names) + } + return res, nil } // MetricRow is a metric to insert into storage. @@ -2957,6 +2969,9 @@ func (s *Storage) wasMetricIDMissingBefore(metricID uint64) bool { // MetricNamesStatsResponse contains metric names usage stats API response type MetricNamesStatsResponse = metricnamestats.StatsResult +// MetricNamesStatsRecord represents record at MetricNamesStatsResponse +type MetricNamesStatsRecord = metricnamestats.StatRecord + // GetMetricNamesStats returns metric names usage stats with given limit and le predicate func (s *Storage) GetMetricNamesStats(_ *querytracer.Tracer, limit, le int, matchPattern string) MetricNamesStatsResponse { return s.metricsTracker.GetStats(limit, le, matchPattern)