diff --git a/app/vmstorage/main.go b/app/vmstorage/main.go index aa3c265cfe..c54df95678 100644 --- a/app/vmstorage/main.go +++ b/app/vmstorage/main.go @@ -37,6 +37,8 @@ var ( futureRetention = flagutil.NewRetentionDuration("futureRetention", "2d", "Data with timestamps bigger than now+futureRetention is automatically deleted. "+ "The minimum futureRetention is 2 days. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention") vmselectAddr = flag.String("vmselectAddr", ":8401", "TCP address to accept connections from vmselect services") + accountID = flag.Uint64("accountID", 0, "The accountID of the stored data") + projectID = flag.Uint64("projectID", 0, "The projectID of the stored data") snapshotAuthKey = flagutil.NewPassword("snapshotAuthKey", "authKey, which must be passed in query string to /snapshot* pages. It overrides -httpAuth.*") forceMergeAuthKey = flagutil.NewPassword("forceMergeAuthKey", "authKey, which must be passed in query string to /internal/force_merge pages. It overrides -httpAuth.*") forceFlushAuthKey = flagutil.NewPassword("forceFlushAuthKey", "authKey, which must be passed in query string to /internal/force_flush pages. It overrides -httpAuth.*") @@ -185,7 +187,13 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) { fs.RegisterPathFsMetrics(*DataPath) var err error - vmselectSrv, err = servers.NewVMSelectServer(*vmselectAddr, strg) + if *accountID > math.MaxUint32 { + logger.Fatalf("-accountID must to be in the range [0, %d], got %d", math.MaxUint32, *accountID) + } + if *projectID > math.MaxUint32 { + logger.Fatalf("-projectID must to be in the range [0, %d], got %d", math.MaxUint32, *projectID) + } + vmselectSrv, err = servers.NewVMSelectServer(*vmselectAddr, strg, uint32(*accountID), uint32(*projectID)) if err != nil { logger.Fatalf("cannot create a server with -vmselectAddr=%s: %s", *vmselectAddr, err) } diff --git a/app/vmstorage/servers/vmselect.go b/app/vmstorage/servers/vmselect.go index 038bbcc921..3502f4ba4f 100644 --- a/app/vmstorage/servers/vmselect.go +++ b/app/vmstorage/servers/vmselect.go @@ -48,9 +48,11 @@ var ( ) // NewVMSelectServer starts new server at the given addr, which serves vmselect requests from the given s. -func NewVMSelectServer(addr string, s *storage.Storage) (*vmselectapi.Server, error) { +func NewVMSelectServer(addr string, s *storage.Storage, accountID, projectID uint32) (*vmselectapi.Server, error) { api := &vmstorageAPI{ - s: s, + s: s, + accountID: accountID, + projectID: projectID, } limits := vmselectapi.Limits{ MaxLabelNames: *maxTagKeys, @@ -66,17 +68,22 @@ func NewVMSelectServer(addr string, s *storage.Storage) (*vmselectapi.Server, er // vmstorageAPI impelements vmselectapi.API type vmstorageAPI struct { - s *storage.Storage + s *storage.Storage + accountID uint32 + projectID uint32 } func (api *vmstorageAPI) InitSearch(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (vmselectapi.BlockIterator, error) { - // TODO(rtm0): Return empty result if sq.AccountID, sq.ProjectID do not - // match tenantID from flag and sq is not multitenant. + // TODO(rtm0): Support multitenant queries. + if sq.AccountID != api.accountID || sq.ProjectID != api.projectID { + return emptyBI, nil + } tr := sq.GetTimeRange() if err := checkTimeRange(api.s, tr); err != nil { return nil, err } + maxMetrics := getMaxMetrics(sq.MaxMetrics) tfss, err := api.setupTfss(qt, sq, tr, maxMetrics, deadline) if err != nil { @@ -100,8 +107,10 @@ func (api *vmstorageAPI) InitSearch(qt *querytracer.Tracer, sq *storage.SearchQu } func (api *vmstorageAPI) SearchMetricNames(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) ([]string, error) { - // TODO(rtm0): Return empty result if sq.AccountID, sq.ProjectID do not - // match tenantID from flag and sq is not multitenant. + // TODO(@rtm0): Support multitenant queries. + if sq.AccountID != api.accountID || sq.ProjectID != api.projectID { + return nil, nil + } tr := sq.GetTimeRange() maxMetrics := sq.MaxMetrics @@ -138,8 +147,10 @@ func (api *vmstorageAPI) SearchMetricNames(qt *querytracer.Tracer, sq *storage.S } func (api *vmstorageAPI) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQuery, labelName string, maxLabelValues int, deadline uint64) ([]string, error) { - // TODO(rtm0): Return empty result if sq.AccountID, sq.ProjectID do not - // match tenantID from flag and sq is not multitenant. + // TODO(@rtm0): Support multitenant queries. + if sq.AccountID != api.accountID || sq.ProjectID != api.projectID { + return nil, nil + } tr := sq.GetTimeRange() maxMetrics := sq.MaxMetrics @@ -157,8 +168,10 @@ func (api *vmstorageAPI) LabelValues(qt *querytracer.Tracer, sq *storage.SearchQ func (api *vmstorageAPI) TagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte, maxSuffixes int, deadline uint64) ([]string, error) { - // TODO(rtm0): Return empty result if accountID, projectID do not match - // tenantID from flag. + // TODO(@rtm0): Support multitenant queries? + if accountID != api.accountID || projectID != api.projectID { + return nil, nil + } suffixes, err := api.s.SearchTagValueSuffixes(qt, tr, tagKey, tagValuePrefix, delimiter, maxSuffixes, deadline) if err != nil { @@ -172,8 +185,10 @@ func (api *vmstorageAPI) TagValueSuffixes(qt *querytracer.Tracer, accountID, pro } func (api *vmstorageAPI) LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery, maxLabelNames int, deadline uint64) ([]string, error) { - // TODO(rtm0): Return empty result if sq.AccountID, sq.ProjectID do not - // match tenantID from flag and sq is not multitenant. + // TODO(@rtm0): Support multitenant queries. + if sq.AccountID != api.accountID || sq.ProjectID != api.projectID { + return nil, nil + } tr := sq.GetTimeRange() maxMetrics := sq.MaxMetrics @@ -190,20 +205,23 @@ func (api *vmstorageAPI) LabelNames(qt *querytracer.Tracer, sq *storage.SearchQu } func (api *vmstorageAPI) SeriesCount(_ *querytracer.Tracer, accountID, projectID uint32, deadline uint64) (uint64, error) { - // TODO(rtm0): Return 0 if accountID, projectID do not match tenantID from - // flag. + // TODO(@rtm0): Support multitenant queries? + if accountID != api.accountID || projectID != api.projectID { + return 0, nil + } return api.s.GetSeriesCount(deadline) } -func (api *vmstorageAPI) Tenants(qt *querytracer.Tracer, tr storage.TimeRange, deadline uint64) ([]string, error) { - // TODO(rtm0): Return the tenantID from flag. - return []string{"0:0"}, nil - // return api.s.SearchTenants(qt, tr, deadline) +func (api *vmstorageAPI) Tenants(_ *querytracer.Tracer, _ storage.TimeRange, _ uint64) ([]string, error) { + tenantID := fmt.Sprintf("%d:%d", api.accountID, api.projectID) + return []string{tenantID}, nil } func (api *vmstorageAPI) TSDBStatus(qt *querytracer.Tracer, sq *storage.SearchQuery, focusLabel string, topN int, deadline uint64) (*storage.TSDBStatus, error) { - // TODO(rtm0): Return empty result if sq.AccountID, sq.ProjectID do not - // match tenantID from flag and sq is not multitenant. + // TODO(@rtm0): Support multitenant queries. + if sq.AccountID != api.accountID || sq.ProjectID != api.projectID { + return &storage.TSDBStatus{}, nil + } tr := sq.GetTimeRange() maxMetrics := sq.MaxMetrics @@ -221,8 +239,10 @@ func (api *vmstorageAPI) TSDBStatus(qt *querytracer.Tracer, sq *storage.SearchQu } func (api *vmstorageAPI) DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline uint64) (int, error) { - // TODO(rtm0): Return empty result if sq.AccountID, sq.ProjectID do not - // match tenantID from flag and sq is not multitenant. + // TODO(@rtm0): Support multitenant queries. + if sq.AccountID != api.accountID || sq.ProjectID != api.projectID { + return 0, nil + } tr := sq.GetTimeRange() maxMetrics := sq.MaxMetrics @@ -246,7 +266,10 @@ func (api *vmstorageAPI) RegisterMetricNames(qt *querytracer.Tracer, mrs []stora } func (api *vmstorageAPI) GetMetricNamesUsageStats(qt *querytracer.Tracer, tt *storage.TenantToken, limit, le int, matchPattern string, _ uint64) (metricnamestats.StatsResult, error) { - // TODO(rtm0): Return empty result if tt do not match tenantID from flag. + // TODO(@rtm0): Support multitenant queries? + if tt.AccountID != api.accountID || tt.ProjectID != api.projectID { + return metricnamestats.StatsResult{}, nil + } return api.s.GetMetricNamesStats(qt, limit, le, matchPattern), nil } @@ -287,7 +310,10 @@ func (api *vmstorageAPI) setupTfss(qt *querytracer.Tracer, sq *storage.SearchQue } func (api *vmstorageAPI) GetMetadataRecords(qt *querytracer.Tracer, tt *storage.TenantToken, limit int, metricName string, deadline uint64) ([]*metricsmetadata.Row, error) { - // TODO(rtm0): Return empty result if tt do not match tenantID from flag. + // TODO(@rtm0): Support multitenant queries? + if tt.AccountID != api.accountID || tt.ProjectID != api.projectID { + return nil, nil + } return api.s.GetMetadataRows(qt, limit, metricName), nil } @@ -338,6 +364,20 @@ func (bi *blockIterator) Error() error { return bi.sr.Error() } +var emptyBI = &emptyBlockIterator{} + +type emptyBlockIterator struct{} + +func (bi *emptyBlockIterator) MustClose() {} + +func (bi *emptyBlockIterator) NextBlock(dst []byte) ([]byte, bool) { + return dst, false +} + +func (bi *emptyBlockIterator) Error() error { + return nil +} + // checkTimeRange returns true if the given tr is denied for querying. func checkTimeRange(s *storage.Storage, tr storage.TimeRange) error { if !*denyQueriesOutsideRetention { diff --git a/apptest/testdata.go b/apptest/testdata.go index 2dfab64cc8..c6c4c57545 100644 --- a/apptest/testdata.go +++ b/apptest/testdata.go @@ -1,32 +1,51 @@ package apptest -import "fmt" +import ( + "fmt" + "slices" +) type TestData struct { - Samples []string - Step int64 - WantSeries []map[string]string - WantQueryResults []*QueryResult + Samples []string + Step int64 + WantSeries []map[string]string + WantLabels []string + WantLabelValues []string + WantQueryResults []*QueryResult + WantMetadata map[string][]MetadataEntry + WantMetricNamesStats []MetricNamesStatsRecord } func GenerateTestData(prefix string, numMetrics, start, end int64) TestData { - samples := make([]string, numMetrics) - step := (end - start) / numMetrics - wantSeries := make([]map[string]string, numMetrics) - wantQueryResults := make([]*QueryResult, numMetrics) + d := TestData{ + Samples: []string{}, + Step: (end - start) / numMetrics, + WantSeries: make([]map[string]string, numMetrics), + WantLabels: make([]string, numMetrics), + WantLabelValues: make([]string, numMetrics), + WantQueryResults: make([]*QueryResult, numMetrics), + WantMetadata: make(map[string][]MetadataEntry), + WantMetricNamesStats: make([]MetricNamesStatsRecord, numMetrics), + } for i := range numMetrics { metricName := fmt.Sprintf("%s_%04d", prefix, i) + metricHelp := fmt.Sprintf("# HELP %s some help message", metricName) + metricType := fmt.Sprintf("# TYPE %s gauge", metricName) labelName := fmt.Sprintf("label_%04d", i) labelValue := fmt.Sprintf("value_%04d", i) value := i - timestamp := start + i*step - samples[i] = fmt.Sprintf(`%s{%s="value", label="%s"} %d %d`, metricName, labelName, labelValue, value, timestamp) - wantSeries[i] = map[string]string{ + timestamp := start + i*d.Step + sample := fmt.Sprintf(`%s{%s="value", label="%s"} %d %d`, metricName, labelName, labelValue, value, timestamp) + + d.Samples = append(d.Samples, metricHelp, metricType, sample) + d.WantSeries[i] = map[string]string{ "__name__": metricName, labelName: "value", "label": labelValue, } - wantQueryResults[i] = &QueryResult{ + d.WantLabels[i] = labelName + d.WantLabelValues[i] = labelValue + d.WantQueryResults[i] = &QueryResult{ Metric: map[string]string{ "__name__": metricName, labelName: "value", @@ -34,42 +53,121 @@ func GenerateTestData(prefix string, numMetrics, start, end int64) TestData { }, Samples: []*Sample{{Timestamp: timestamp, Value: float64(value)}}, } + d.WantMetadata[metricName] = []MetadataEntry{{Help: "some help message", Type: "gauge"}} + d.WantMetricNamesStats[i].MetricName = metricName } - return TestData{samples, step, wantSeries, wantQueryResults} + d.WantLabels = append(d.WantLabels, "__name__", "label") + slices.Sort(d.WantLabels) + return d } // AssertSeries retrieves metric names from the storage and compares the result // with the expected one. -func AssertSeries(tc *TestCase, app PrometheusQuerier, metricNameRE string, start, end int64, want []map[string]string) { +func AssertSeries(tc *TestCase, app PrometheusQuerier, metricNameRE, tenantID string, start, end int64, want []map[string]string) { tc.T().Helper() query := fmt.Sprintf(`{__name__=~"%s"}`, metricNameRE) tc.Assert(&AssertOptions{ - Msg: "unexpected /api/v1/series response", + Msg: "unexpected /prometheus/api/v1/series response", Got: func() any { return app.PrometheusAPIV1Series(tc.T(), query, QueryOpts{ - Start: fmt.Sprintf("%d", start), - End: fmt.Sprintf("%d", end), + Tenant: tenantID, + Start: fmt.Sprintf("%d", start), + End: fmt.Sprintf("%d", end), }).Sort() }, Want: &PrometheusAPIV1SeriesResponse{ Status: "success", Data: want, }, + Retries: 1000, + FailNow: true, + }) +} + +// AssertSeriesCount retrieves series count and compares it with expected one. +func AssertSeriesCount(tc *TestCase, app PrometheusQuerier, tenantID string, start, end int64, want uint64) { + tc.T().Helper() + + tc.Assert(&AssertOptions{ + Msg: "unexpected /prometheus/api/v1/series/count response", + Got: func() any { + return app.PrometheusAPIV1SeriesCount(tc.T(), QueryOpts{ + Tenant: tenantID, + Start: fmt.Sprintf("%d", start), + End: fmt.Sprintf("%d", end), + }) + }, + Want: &PrometheusAPIV1SeriesCountResponse{ + Status: "success", + Data: []uint64{want}, + }, + FailNow: true, + }) +} + +// AssertLabels retrieves label names from the storage and compares the result +// with the expected one. +func AssertLabels(tc *TestCase, app PrometheusQuerier, metricNameRE, tenantID string, start, end int64, want []string) { + tc.T().Helper() + + query := fmt.Sprintf(`{__name__=~"%s"}`, metricNameRE) + tc.Assert(&AssertOptions{ + Msg: "unexpected /prometheus/api/v1/labels response", + Got: func() any { + res := app.PrometheusAPIV1Labels(tc.T(), query, QueryOpts{ + Tenant: tenantID, + Start: fmt.Sprintf("%d", start), + End: fmt.Sprintf("%d", end), + }) + slices.Sort(res.Data) + return res + }, + Want: &PrometheusAPIV1LabelsResponse{ + Status: "success", + Data: want, + }, + FailNow: true, + }) +} + +// AssertLabelValues retrieves values for the label whose name is labelName for +// the series whose name mathes metricNameRE, compares the result with the +// expected one. +func AssertLabelValues(tc *TestCase, app PrometheusQuerier, metricNameRE, labelName, tenantID string, start, end int64, want []string) { + tc.T().Helper() + + query := fmt.Sprintf(`{__name__=~"%s"}`, metricNameRE) + tc.Assert(&AssertOptions{ + Msg: "unexpected /prometheus/api/v1/labels/.../values response", + Got: func() any { + res := app.PrometheusAPIV1LabelValues(tc.T(), labelName, query, QueryOpts{ + Tenant: tenantID, + Start: fmt.Sprintf("%d", start), + End: fmt.Sprintf("%d", end), + }) + slices.Sort(res.Data) + return res + }, + Want: &PrometheusAPIV1LabelValuesResponse{ + Status: "success", + Data: want, + }, FailNow: true, }) } // AssertQueryResults sends a data query to storage and compares the query // result with the expected one. -func AssertQueryResults(tc *TestCase, app PrometheusQuerier, metricNameRE string, start, end, step int64, want []*QueryResult) { +func AssertQueryResults(tc *TestCase, app PrometheusQuerier, metricNameRE, tenantID string, start, end, step int64, want []*QueryResult) { tc.T().Helper() query := fmt.Sprintf(`{__name__=~"%s"}`, metricNameRE) tc.Assert(&AssertOptions{ - Msg: "unexpected /api/v1/query_range response", + Msg: "unexpected /prometheus/api/v1/query_range response", Got: func() any { return app.PrometheusAPIV1QueryRange(tc.T(), query, QueryOpts{ + Tenant: tenantID, Start: fmt.Sprintf("%d", start), End: fmt.Sprintf("%d", end), Step: fmt.Sprintf("%dms", step), @@ -87,3 +185,163 @@ func AssertQueryResults(tc *TestCase, app PrometheusQuerier, metricNameRE string FailNow: true, }) } + +func AssertMetadata(tc *TestCase, app PrometheusQuerier, metricName, tenantID string, want map[string][]MetadataEntry) { + tc.T().Helper() + + tc.Assert(&AssertOptions{ + Msg: "unexpected /prometheus/api/v1/metadata response", + Got: func() any { + return app.PrometheusAPIV1Metadata(tc.T(), metricName, 0, QueryOpts{ + Tenant: tenantID, + }) + }, + Want: &PrometheusAPIV1Metadata{ + Status: "success", + Data: want, + }, + FailNow: true, + }) +} + +func AssertMetricNamesStats(tc *TestCase, app PrometheusQuerier, metricNameRE, tenantID string, want []MetricNamesStatsRecord) { + tc.T().Helper() + + tc.Assert(&AssertOptions{ + Msg: "unexpected /prometheus/api/v1/status/metric_names_stats response", + Got: func() any { + return app.PrometheusAPIV1StatusMetricNamesStats(tc.T(), "", "", metricNameRE, QueryOpts{ + Tenant: tenantID, + }) + }, + Want: MetricNamesStatsResponse{ + Records: want, + }, + FailNow: true, + }) + +} + +// GraphiteTestData holds the data samples in Graphite Pickle format, distance +// between samples in milliseconds and expected responses for various Graphite +// API endpoints. +type GraphiteTestData struct { + Samples []string + Step int64 + WantMetricsIndex []string + WantMetricsFind []GraphiteMetric + WantMetricsExpand []string + WantRenderedTargets []GraphiteRenderedTarget +} + +// GenerateGraphiteTestData generates Graphite test data. +func GenerateGraphiteTestData(prefix string, numMetrics, start, end int64) GraphiteTestData { + d := GraphiteTestData{ + Samples: make([]string, numMetrics), + Step: (end - start) / numMetrics, + WantMetricsIndex: make([]string, numMetrics), + WantMetricsFind: make([]GraphiteMetric, numMetrics), + WantMetricsExpand: make([]string, numMetrics), + WantRenderedTargets: make([]GraphiteRenderedTarget, numMetrics), + } + + datapoints := make([][2]float64, numMetrics) + for i := range numMetrics { + timestamp := (start + i*d.Step) / 1000 + datapoints[i][1] = float64(timestamp) + } + + for i := range numMetrics { + suffix := fmt.Sprintf("%04d", i) + metricName := fmt.Sprintf("%s.%s", prefix, suffix) + value := i + timestamp := (start + i*d.Step) / 1000 + sample := fmt.Sprintf(`%s %d %d`, metricName, value, timestamp) + + d.Samples[i] = sample + d.WantMetricsIndex[i] = metricName + d.WantMetricsFind[i].Id = metricName + d.WantMetricsFind[i].Text = suffix + d.WantMetricsFind[i].Leaf = 1 + d.WantMetricsExpand[i] = metricName + d.WantRenderedTargets[i].Target = metricName + d.WantRenderedTargets[i].Datapoints = slices.Clone(datapoints) + d.WantRenderedTargets[i].Datapoints[i][0] = float64(value) + } + return d +} + +// AssertGraphiteMetricsIndex retrieves all metrics by sending a request to +// /graphite/metrics/index.json and compares the result with the expected one. +func AssertGraphiteMetricsIndex(tc *TestCase, app PrometheusQuerier, tenantID string, want []string) { + tc.T().Helper() + + tc.Assert(&AssertOptions{ + Msg: "unexpected /graphite/metrics/index.json response", + Got: func() any { + return app.GraphiteMetricsIndex(tc.T(), QueryOpts{ + Tenant: tenantID, + }) + }, + Want: want, + Retries: 30, + FailNow: true, + }) + +} + +// AssertGraphiteMetricsFind finds metric names by sending a request to +// /graphite/metrics/find and compares the result with the expected one. +func AssertGraphiteMetricsFind(tc *TestCase, app PrometheusQuerier, query, tenantID string, want []GraphiteMetric) { + tc.T().Helper() + + tc.Assert(&AssertOptions{ + Msg: "unexpected /graphite/metrics/find response", + Got: func() any { + return app.GraphiteMetricsFind(tc.T(), query, QueryOpts{ + Tenant: tenantID, + }) + }, + Want: want, + FailNow: true, + }) + +} + +// AssertGraphiteMetricsFind expands metric names by sending a request to +// /graphite/metrics/expand and compares the result with the expected one. +func AssertGraphiteMetricsExpand(tc *TestCase, app PrometheusQuerier, query, tenantID string, want []string) { + tc.T().Helper() + + tc.Assert(&AssertOptions{ + Msg: "unexpected /graphite/metrics/expand response", + Got: func() any { + return app.GraphiteMetricsExpand(tc.T(), query, QueryOpts{ + Tenant: tenantID, + }) + }, + Want: want, + FailNow: true, + }) + +} + +// AssertGraphiteRender retieves metric raw data by sending a request to +// /graphite/render and compares the result with the expected one. +func AssertGraphiteRender(tc *TestCase, app PrometheusQuerier, target, tenantID string, from, until, step int64, want []GraphiteRenderedTarget) { + tc.T().Helper() + + tc.Assert(&AssertOptions{ + Msg: "unexpected /graphite/render response", + Got: func() any { + return app.GraphiteRender(tc.T(), target, QueryOpts{ + Tenant: tenantID, + From: fmt.Sprintf("%d", from/1000), + Until: fmt.Sprintf("%d", until/1000), + StorageStep: fmt.Sprintf("%dms", step), + }) + }, + Want: want, + FailNow: true, + }) +} diff --git a/apptest/tests/mixed_test.go b/apptest/tests/mixed_test.go index 214ec633c8..43198a0b5f 100644 --- a/apptest/tests/mixed_test.go +++ b/apptest/tests/mixed_test.go @@ -1,30 +1,49 @@ package tests import ( + "fmt" "os" "path/filepath" + "slices" "testing" "time" "github.com/VictoriaMetrics/VictoriaMetrics/apptest" + "github.com/google/go-cmp/cmp" ) var ( vmselectPath = os.Getenv("VM_VMSELECT_PATH") ) -func TestMixedDataRetrieval(t *testing.T) { +func TestMixedPrometheusQueries(t *testing.T) { tc := apptest.NewTestCase(t) defer tc.Stop() - const numMetrics = 1000 + const ( + accountID1 = 12 + projectID1 = 34 + accountID2 = 56 + projectID2 = 78 + numMetrics = 10 + ) + tenantID1 := fmt.Sprintf("%d:%d", accountID1, projectID1) + tenantID2 := fmt.Sprintf("%d:%d", accountID2, projectID2) start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli() end := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).UnixMilli() data := apptest.GenerateTestData("metric", numMetrics, start, end) + emptySeries := []map[string]string{} + emptyLabels := []string{} + emptyLabelValues := []string{} + emptyQueryResults := []*apptest.QueryResult{} + emptyMetadata := map[string][]apptest.MetadataEntry{} + emptyMetricNamesStats := []apptest.MetricNamesStatsRecord{} vmsingle := tc.MustStartVmsingle("vmsingle", []string{ "-storageDataPath=" + filepath.Join(tc.Dir(), "vmsingle"), "-retentionPeriod=100y", + fmt.Sprintf("-accountID=%d", accountID1), + fmt.Sprintf("-projectID=%d", projectID1), }) vmselect := tc.MustStartVmselectAt("vmselect", vmselectPath, []string{ "-storageNode=" + vmsingle.VmselectAddr(), @@ -32,9 +51,143 @@ func TestMixedDataRetrieval(t *testing.T) { vmsingle.PrometheusAPIV1ImportPrometheus(tc.T(), data.Samples, apptest.QueryOpts{}) vmsingle.ForceFlush(t) - apptest.AssertSeries(tc, vmsingle, "metric.*", start, end, data.WantSeries) - apptest.AssertQueryResults(tc, vmsingle, "metric.*", start, end, data.Step, data.WantQueryResults) - apptest.AssertSeries(tc, vmselect, "metric.*", start, end, data.WantSeries) - apptest.AssertQueryResults(tc, vmselect, "metric.*", start, end, data.Step, data.WantQueryResults) + apptest.AssertSeries(tc, vmsingle, "metric.*", "", start, end, data.WantSeries) + apptest.AssertSeriesCount(tc, vmsingle, "", start, end, numMetrics) + apptest.AssertLabels(tc, vmsingle, "metric.*", "", start, end, data.WantLabels) + apptest.AssertLabelValues(tc, vmsingle, "metric.*", "label", "", start, end, data.WantLabelValues) + apptest.AssertQueryResults(tc, vmsingle, "metric.*", "", start, end, data.Step, data.WantQueryResults) + apptest.AssertMetadata(tc, vmsingle, "", "", data.WantMetadata) + for i := range data.WantMetricNamesStats { + data.WantMetricNamesStats[i].QueryRequestsCount = 1 + } + apptest.AssertMetricNamesStats(tc, vmsingle, "", "", data.WantMetricNamesStats) + + apptest.AssertSeries(tc, vmselect, "metric.*", tenantID1, start, end, data.WantSeries) + apptest.AssertSeriesCount(tc, vmselect, tenantID1, start, end, numMetrics) + apptest.AssertLabels(tc, vmselect, "metric.*", tenantID1, start, end, data.WantLabels) + apptest.AssertLabelValues(tc, vmselect, "metric.*", "label", tenantID1, start, end, data.WantLabelValues) + apptest.AssertQueryResults(tc, vmselect, "metric.*", tenantID1, start, end, data.Step, data.WantQueryResults) + apptest.AssertMetadata(tc, vmselect, "", tenantID1, data.WantMetadata) + for i := range data.WantMetricNamesStats { + data.WantMetricNamesStats[i].QueryRequestsCount = 2 + } + apptest.AssertMetricNamesStats(tc, vmselect, "", tenantID1, data.WantMetricNamesStats) + + apptest.AssertSeries(tc, vmselect, "metric.*", tenantID2, start, end, emptySeries) + apptest.AssertSeriesCount(tc, vmselect, tenantID2, start, end, 0) + apptest.AssertLabels(tc, vmselect, "metric.*", tenantID2, start, end, emptyLabels) + apptest.AssertLabelValues(tc, vmselect, "metric.*", "label", tenantID2, start, end, emptyLabelValues) + apptest.AssertQueryResults(tc, vmselect, "metric.*", tenantID2, start, end, data.Step, emptyQueryResults) + apptest.AssertMetadata(tc, vmselect, "", tenantID2, emptyMetadata) + apptest.AssertMetricNamesStats(tc, vmselect, "", tenantID2, emptyMetricNamesStats) + + gotAdminTenantsResponse := vmselect.APIV1AdminTenants(t, apptest.QueryOpts{}) + wantAdminTenantsResponse := &apptest.AdminTenantsResponse{ + Status: "success", + Data: []string{tenantID1}, + } + if diff := cmp.Diff(wantAdminTenantsResponse, gotAdminTenantsResponse); diff != "" { + t.Fatalf("unexpected tenants (-want, +got):\n%s", diff) + } +} + +func TestMixedDeleteSeries(t *testing.T) { + tc := apptest.NewTestCase(t) + defer tc.Stop() + + const ( + accountID1 = 12 + projectID1 = 34 + accountID2 = 56 + projectID2 = 78 + numMetrics = 10 + ) + tenantID1 := fmt.Sprintf("%d:%d", accountID1, projectID1) + tenantID2 := fmt.Sprintf("%d:%d", accountID2, projectID2) + start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli() + end := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).UnixMilli() + data1 := apptest.GenerateTestData("metric1", numMetrics, start, end) + data2 := apptest.GenerateTestData("metric2", numMetrics, start, end) + emptySeries := []map[string]string{} + + vmsingle := tc.MustStartVmsingle("vmsingle", []string{ + "-storageDataPath=" + filepath.Join(tc.Dir(), "vmsingle"), + "-retentionPeriod=100y", + fmt.Sprintf("-accountID=%d", accountID1), + fmt.Sprintf("-projectID=%d", projectID1), + }) + vmselect := tc.MustStartVmselectAt("vmselect", vmselectPath, []string{ + "-storageNode=" + vmsingle.VmselectAddr(), + }) + + vmsingle.PrometheusAPIV1ImportPrometheus(tc.T(), data1.Samples, apptest.QueryOpts{}) + vmsingle.PrometheusAPIV1ImportPrometheus(tc.T(), data2.Samples, apptest.QueryOpts{}) + vmsingle.ForceFlush(t) + + wantSeries12 := slices.Concat(data1.WantSeries, data2.WantSeries) + apptest.AssertSeries(tc, vmsingle, "metric.*", "", start, end, wantSeries12) + + vmselect.PrometheusAPIV1AdminTSDBDeleteSeries(tc.T(), `{__name__=~"metric1.*"}`, apptest.QueryOpts{ + Tenant: tenantID1, + }) + apptest.AssertSeries(tc, vmsingle, "metric.*", "", start, end, data2.WantSeries) + vmselect.PrometheusAPIV1AdminTSDBDeleteSeries(tc.T(), `{__name__=~"metric2.*"}`, apptest.QueryOpts{ + Tenant: tenantID2, + }) + apptest.AssertSeries(tc, vmsingle, "metric.*", "", start, end, data2.WantSeries) + vmselect.PrometheusAPIV1AdminTSDBDeleteSeries(tc.T(), `{__name__=~"metric2.*"}`, apptest.QueryOpts{ + Tenant: tenantID1, + }) + apptest.AssertSeries(tc, vmsingle, "metric.*", "", start, end, emptySeries) +} + +func TestMixedGraphiteQueries(t *testing.T) { + tc := apptest.NewTestCase(t) + defer tc.Stop() + + const ( + accountID1 = 12 + projectID1 = 34 + accountID2 = 56 + projectID2 = 78 + numMetrics = 10 + ) + tenantID1 := fmt.Sprintf("%d:%d", accountID1, projectID1) + tenantID2 := fmt.Sprintf("%d:%d", accountID2, projectID2) + start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli() + end := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).UnixMilli() + data := apptest.GenerateGraphiteTestData("metric", numMetrics, start, end) + emptyMetricsIndex := []string{} + emptyMetricsFind := []apptest.GraphiteMetric{} + emptyMetricsExpand := []string{} + emptyRenderedTargets := []apptest.GraphiteRenderedTarget{} + + vmsingle := tc.MustStartVmsingle("vmsingle", []string{ + "-storageDataPath=" + filepath.Join(tc.Dir(), "vmsingle"), + "-retentionPeriod=100y", + fmt.Sprintf("-accountID=%d", accountID1), + fmt.Sprintf("-projectID=%d", projectID1), + }) + vmselect := tc.MustStartVmselectAt("vmselect", vmselectPath, []string{ + "-storageNode=" + vmsingle.VmselectAddr(), + }) + + vmsingle.GraphiteWrite(tc.T(), data.Samples, apptest.QueryOpts{}) + vmsingle.ForceFlush(t) + + apptest.AssertGraphiteMetricsIndex(tc, vmsingle, "", data.WantMetricsIndex) + apptest.AssertGraphiteMetricsFind(tc, vmsingle, "metric.*", "", data.WantMetricsFind) + apptest.AssertGraphiteMetricsExpand(tc, vmsingle, "metric.*", "", data.WantMetricsExpand) + apptest.AssertGraphiteRender(tc, vmsingle, "metric.*", "", start, end, data.Step, data.WantRenderedTargets) + + apptest.AssertGraphiteMetricsIndex(tc, vmselect, tenantID1, data.WantMetricsIndex) + apptest.AssertGraphiteMetricsFind(tc, vmselect, "metric.*", tenantID1, data.WantMetricsFind) + apptest.AssertGraphiteMetricsExpand(tc, vmselect, "metric.*", tenantID1, data.WantMetricsExpand) + apptest.AssertGraphiteRender(tc, vmselect, "metric.*", tenantID1, start, end, data.Step, data.WantRenderedTargets) + + apptest.AssertGraphiteMetricsIndex(tc, vmselect, tenantID2, emptyMetricsIndex) + apptest.AssertGraphiteMetricsFind(tc, vmselect, "metric.*", tenantID2, emptyMetricsFind) + apptest.AssertGraphiteMetricsExpand(tc, vmselect, "metric.*", tenantID2, emptyMetricsExpand) + apptest.AssertGraphiteRender(tc, vmselect, "metric.*", tenantID2, start, end, data.Step, emptyRenderedTargets) }