mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmselect: add support of multi-tenant queries (#6346)
### Describe Your Changes Added an ability to query data across multiple tenants. See: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1434 Currently, the following endpoints work with multi-tenancy: - /prometheus/api/v1/query - /prometheus/api/v1/query_range - /prometheus/api/v1/series - /prometheus/api/v1/labels - /prometheus/api/v1/label/<label_name>/values - /prometheus/api/v1/status/active_queries - /prometheus/api/v1/status/top_queries - /prometheus/api/v1/status/tsdb - /prometheus/api/v1/export - /prometheus/api/v1/export/csv - /vmui A note regarding VMUI: endpoints such as `active_queries` and `top_queries` have been updated to indicate whether query was a single-tenant or multi-tenant, but UI needs to be updated to display this info. cc: @Loori-R --------- Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com> Signed-off-by: f41gh7 <nik@victoriametrics.com> Co-authored-by: f41gh7 <nik@victoriametrics.com>
This commit is contained in:
parent
856c189688
commit
44b071296d
24 changed files with 1274 additions and 198 deletions
|
@ -11,6 +11,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/clusternative"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/clusternative"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphite"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphite"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||||
|
@ -34,7 +36,6 @@ import (
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmselectapi"
|
||||||
"github.com/VictoriaMetrics/metrics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -268,7 +269,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||||
httpserver.Errorf(w, r, "cannot parse path %q: %s", path, err)
|
httpserver.Errorf(w, r, "cannot parse path %q: %s", path, err)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
at, err := auth.NewToken(p.AuthToken)
|
at, err := auth.NewTokenPossibleMultitenant(p.AuthToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpserver.Errorf(w, r, "auth error: %s", err)
|
httpserver.Errorf(w, r, "auth error: %s", err)
|
||||||
return true
|
return true
|
||||||
|
@ -309,6 +310,10 @@ func selectHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(p.Suffix, "graphite/") && at == nil {
|
||||||
|
httpserver.Errorf(w, r, "multi-tenant queries are not supported by Graphite endpoints")
|
||||||
|
return true
|
||||||
|
}
|
||||||
if strings.HasPrefix(p.Suffix, "graphite/tags/") && !isGraphiteTagsPath(p.Suffix[len("graphite"):]) {
|
if strings.HasPrefix(p.Suffix, "graphite/tags/") && !isGraphiteTagsPath(p.Suffix[len("graphite"):]) {
|
||||||
tagName := p.Suffix[len("graphite/tags/"):]
|
tagName := p.Suffix[len("graphite/tags/"):]
|
||||||
graphiteTagValuesRequests.Inc()
|
graphiteTagValuesRequests.Inc()
|
||||||
|
@ -651,7 +656,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||||
}
|
}
|
||||||
switch p.Suffix {
|
switch p.Suffix {
|
||||||
case "prometheus/api/v1/status/active_queries":
|
case "prometheus/api/v1/status/active_queries":
|
||||||
at, err := auth.NewToken(p.AuthToken)
|
at, err := auth.NewTokenPossibleMultitenant(p.AuthToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -660,7 +665,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||||
promql.ActiveQueriesHandler(at, w, r)
|
promql.ActiveQueriesHandler(at, w, r)
|
||||||
return true
|
return true
|
||||||
case "prometheus/api/v1/status/top_queries":
|
case "prometheus/api/v1/status/top_queries":
|
||||||
at, err := auth.NewToken(p.AuthToken)
|
at, err := auth.NewTokenPossibleMultitenant(p.AuthToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
@ -79,8 +80,9 @@ func (r *Result) reset() {
|
||||||
|
|
||||||
// Results holds results returned from ProcessSearchQuery.
|
// Results holds results returned from ProcessSearchQuery.
|
||||||
type Results struct {
|
type Results struct {
|
||||||
tr storage.TimeRange
|
shouldConvertTenantToLabels bool
|
||||||
deadline searchutils.Deadline
|
tr storage.TimeRange
|
||||||
|
deadline searchutils.Deadline
|
||||||
|
|
||||||
tbfs []*tmpBlocksFile
|
tbfs []*tmpBlocksFile
|
||||||
|
|
||||||
|
@ -265,14 +267,24 @@ func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, worke
|
||||||
// Nothing to process
|
// Nothing to process
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
cb := f
|
||||||
|
if rss.shouldConvertTenantToLabels {
|
||||||
|
cb = func(rs *Result, workerID uint) error {
|
||||||
|
// TODO: (@f41gh7) if labels duplicates will be fixed
|
||||||
|
// query will return Duplicate Output Series error
|
||||||
|
// in this case, TenantToTags must be moved into RegisterAndWriteBlock method
|
||||||
|
metricNameTenantToTags(&rs.MetricName)
|
||||||
|
return f(rs, workerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
var mustStop atomic.Bool
|
var mustStop atomic.Bool
|
||||||
initTimeseriesWork := func(tsw *timeseriesWork, pts *packedTimeseries) {
|
initTimeseriesWork := func(tsw *timeseriesWork, pts *packedTimeseries) {
|
||||||
tsw.rss = rss
|
tsw.rss = rss
|
||||||
tsw.pts = pts
|
tsw.pts = pts
|
||||||
tsw.f = f
|
tsw.f = cb
|
||||||
tsw.mustStop = &mustStop
|
tsw.mustStop = &mustStop
|
||||||
}
|
}
|
||||||
|
|
||||||
maxWorkers := MaxWorkers()
|
maxWorkers := MaxWorkers()
|
||||||
if maxWorkers == 1 || tswsLen == 1 {
|
if maxWorkers == 1 || tswsLen == 1 {
|
||||||
// It is faster to process time series in the current goroutine.
|
// It is faster to process time series in the current goroutine.
|
||||||
|
@ -834,7 +846,6 @@ func RegisterMetricNames(qt *querytracer.Tracer, mrs []storage.MetricRow, deadli
|
||||||
func DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline searchutils.Deadline) (int, error) {
|
func DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline searchutils.Deadline) (int, error) {
|
||||||
qt = qt.NewChild("delete series: %s", sq)
|
qt = qt.NewChild("delete series: %s", sq)
|
||||||
defer qt.Done()
|
defer qt.Done()
|
||||||
requestData := sq.Marshal(nil)
|
|
||||||
|
|
||||||
// Send the query to all the storage nodes in parallel.
|
// Send the query to all the storage nodes in parallel.
|
||||||
type nodeResult struct {
|
type nodeResult struct {
|
||||||
|
@ -843,25 +854,36 @@ func DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline sear
|
||||||
}
|
}
|
||||||
sns := getStorageNodes()
|
sns := getStorageNodes()
|
||||||
snr := startStorageNodesRequest(qt, sns, true, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
snr := startStorageNodesRequest(qt, sns, true, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
||||||
sn.deleteSeriesRequests.Inc()
|
err := populateSqTenantTokensIfNeeded(sq)
|
||||||
deletedCount, err := sn.deleteSeries(qt, requestData, deadline)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sn.deleteSeriesErrors.Inc()
|
return []*nodeResult{{
|
||||||
}
|
err: err,
|
||||||
return &nodeResult{
|
}}
|
||||||
deletedCount: deletedCount,
|
|
||||||
err: err,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return execSearchQuery(qt, sq, func(qt *querytracer.Tracer, requestData []byte, _ storage.TenantToken) any {
|
||||||
|
sn.deleteSeriesRequests.Inc()
|
||||||
|
deletedCount, err := sn.deleteSeries(qt, requestData, deadline)
|
||||||
|
if err != nil {
|
||||||
|
sn.deleteSeriesErrors.Inc()
|
||||||
|
}
|
||||||
|
return &nodeResult{
|
||||||
|
deletedCount: deletedCount,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collect results
|
// Collect results
|
||||||
deletedTotal := 0
|
deletedTotal := 0
|
||||||
err := snr.collectAllResults(func(result any) error {
|
err := snr.collectAllResults(func(result any) error {
|
||||||
nr := result.(*nodeResult)
|
for _, cr := range result.([]any) {
|
||||||
if nr.err != nil {
|
nr := cr.(*nodeResult)
|
||||||
return nr.err
|
if nr.err != nil {
|
||||||
|
return nr.err
|
||||||
|
}
|
||||||
|
deletedTotal += nr.deletedCount
|
||||||
}
|
}
|
||||||
deletedTotal += nr.deletedCount
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -877,7 +899,6 @@ func LabelNames(qt *querytracer.Tracer, denyPartialResponse bool, sq *storage.Se
|
||||||
if deadline.Exceeded() {
|
if deadline.Exceeded() {
|
||||||
return nil, false, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
|
return nil, false, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
|
||||||
}
|
}
|
||||||
requestData := sq.Marshal(nil)
|
|
||||||
// Send the query to all the storage nodes in parallel.
|
// Send the query to all the storage nodes in parallel.
|
||||||
type nodeResult struct {
|
type nodeResult struct {
|
||||||
labelNames []string
|
labelNames []string
|
||||||
|
@ -885,28 +906,43 @@ func LabelNames(qt *querytracer.Tracer, denyPartialResponse bool, sq *storage.Se
|
||||||
}
|
}
|
||||||
sns := getStorageNodes()
|
sns := getStorageNodes()
|
||||||
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
||||||
sn.labelNamesRequests.Inc()
|
err := populateSqTenantTokensIfNeeded(sq)
|
||||||
labelNames, err := sn.getLabelNames(qt, requestData, maxLabelNames, deadline)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sn.labelNamesErrors.Inc()
|
return []*nodeResult{{
|
||||||
err = fmt.Errorf("cannot get labels from vmstorage %s: %w", sn.connPool.Addr(), err)
|
err: err,
|
||||||
}
|
}}
|
||||||
return &nodeResult{
|
|
||||||
labelNames: labelNames,
|
|
||||||
err: err,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return execSearchQuery(qt, sq, func(qt *querytracer.Tracer, requestData []byte, _ storage.TenantToken) any {
|
||||||
|
sn.labelNamesRequests.Inc()
|
||||||
|
labelNames, err := sn.getLabelNames(qt, requestData, maxLabelNames, deadline)
|
||||||
|
if err != nil {
|
||||||
|
sn.labelNamesErrors.Inc()
|
||||||
|
err = fmt.Errorf("cannot get labels from vmstorage %s: %w", sn.connPool.Addr(), err)
|
||||||
|
}
|
||||||
|
return &nodeResult{
|
||||||
|
labelNames: labelNames,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collect results
|
// Collect results
|
||||||
var labelNames []string
|
var labelNames []string
|
||||||
isPartial, err := snr.collectResults(partialLabelNamesResults, func(result any) error {
|
isPartial, err := snr.collectResults(partialLabelNamesResults, func(result any) error {
|
||||||
nr := result.(*nodeResult)
|
for _, cr := range result.([]any) {
|
||||||
if nr.err != nil {
|
nr := cr.(*nodeResult)
|
||||||
return nr.err
|
if nr.err != nil {
|
||||||
|
return nr.err
|
||||||
|
}
|
||||||
|
labelNames = append(labelNames, nr.labelNames...)
|
||||||
}
|
}
|
||||||
labelNames = append(labelNames, nr.labelNames...)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if sq.IsMultiTenant {
|
||||||
|
labelNames = append(labelNames, []string{"vm_account_id", "vm_project_id"}...)
|
||||||
|
}
|
||||||
qt.Printf("get %d non-duplicated labels", len(labelNames))
|
qt.Printf("get %d non-duplicated labels", len(labelNames))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, isPartial, fmt.Errorf("cannot fetch labels from vmstorage nodes: %w", err)
|
return nil, isPartial, fmt.Errorf("cannot fetch labels from vmstorage nodes: %w", err)
|
||||||
|
@ -979,7 +1015,36 @@ func LabelValues(qt *querytracer.Tracer, denyPartialResponse bool, labelName str
|
||||||
if deadline.Exceeded() {
|
if deadline.Exceeded() {
|
||||||
return nil, false, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
|
return nil, false, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
|
||||||
}
|
}
|
||||||
requestData := sq.Marshal(nil)
|
|
||||||
|
if sq.IsMultiTenant && isTenancyLabel(labelName) {
|
||||||
|
tenants, err := Tenants(qt, sq.GetTimeRange(), deadline)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
switch labelName {
|
||||||
|
case "vm_account_id":
|
||||||
|
idx = 0
|
||||||
|
case "vm_project_id":
|
||||||
|
idx = 1
|
||||||
|
default:
|
||||||
|
logger.Fatalf("BUG: unexpected labeName=%q", labelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
labelValues := make([]string, 0, len(tenants))
|
||||||
|
for _, t := range tenants {
|
||||||
|
s := strings.Split(t, ":")
|
||||||
|
if len(s) != 2 {
|
||||||
|
logger.Fatalf("BUG: unexpected tenant received from storage: %q", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
labelValues = append(labelValues, s[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
labelValues = prepareLabelValues(qt, labelValues, maxLabelValues)
|
||||||
|
return labelValues, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Send the query to all the storage nodes in parallel.
|
// Send the query to all the storage nodes in parallel.
|
||||||
type nodeResult struct {
|
type nodeResult struct {
|
||||||
|
@ -988,33 +1053,49 @@ func LabelValues(qt *querytracer.Tracer, denyPartialResponse bool, labelName str
|
||||||
}
|
}
|
||||||
sns := getStorageNodes()
|
sns := getStorageNodes()
|
||||||
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
||||||
sn.labelValuesRequests.Inc()
|
err := populateSqTenantTokensIfNeeded(sq)
|
||||||
labelValues, err := sn.getLabelValues(qt, labelName, requestData, maxLabelValues, deadline)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sn.labelValuesErrors.Inc()
|
return []*nodeResult{{
|
||||||
err = fmt.Errorf("cannot get label values from vmstorage %s: %w", sn.connPool.Addr(), err)
|
err: err,
|
||||||
}
|
}}
|
||||||
return &nodeResult{
|
|
||||||
labelValues: labelValues,
|
|
||||||
err: err,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return execSearchQuery(qt, sq, func(qt *querytracer.Tracer, requestData []byte, _ storage.TenantToken) any {
|
||||||
|
sn.labelValuesRequests.Inc()
|
||||||
|
labelValues, err := sn.getLabelValues(qt, labelName, requestData, maxLabelValues, deadline)
|
||||||
|
if err != nil {
|
||||||
|
sn.labelValuesErrors.Inc()
|
||||||
|
err = fmt.Errorf("cannot get label values from vmstorage %s: %w", sn.connPool.Addr(), err)
|
||||||
|
}
|
||||||
|
return &nodeResult{
|
||||||
|
labelValues: labelValues,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collect results
|
// Collect results
|
||||||
var labelValues []string
|
var labelValues []string
|
||||||
isPartial, err := snr.collectResults(partialLabelValuesResults, func(result any) error {
|
isPartial, err := snr.collectResults(partialLabelValuesResults, func(result any) error {
|
||||||
nr := result.(*nodeResult)
|
for _, cr := range result.([]any) {
|
||||||
if nr.err != nil {
|
nr := cr.(*nodeResult)
|
||||||
return nr.err
|
if nr.err != nil {
|
||||||
|
return nr.err
|
||||||
|
}
|
||||||
|
labelValues = append(labelValues, nr.labelValues...)
|
||||||
}
|
}
|
||||||
labelValues = append(labelValues, nr.labelValues...)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
qt.Printf("get %d non-duplicated label values", len(labelValues))
|
qt.Printf("get %d non-duplicated label values", len(labelValues))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, isPartial, fmt.Errorf("cannot fetch label values from vmstorage nodes: %w", err)
|
return nil, isPartial, fmt.Errorf("cannot fetch label values from vmstorage nodes: %w", err)
|
||||||
}
|
}
|
||||||
|
labelValues = prepareLabelValues(qt, labelValues, maxLabelValues)
|
||||||
|
return labelValues, isPartial, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareLabelValues(qt *querytracer.Tracer, labelValues []string, maxLabelValues int) []string {
|
||||||
|
qt.Printf("get %d non-duplicated label values", len(labelValues))
|
||||||
// Deduplicate label values
|
// Deduplicate label values
|
||||||
labelValues = deduplicateStrings(labelValues)
|
labelValues = deduplicateStrings(labelValues)
|
||||||
qt.Printf("get %d unique label values after de-duplication", len(labelValues))
|
qt.Printf("get %d unique label values after de-duplication", len(labelValues))
|
||||||
|
@ -1024,7 +1105,7 @@ func LabelValues(qt *querytracer.Tracer, denyPartialResponse bool, labelName str
|
||||||
}
|
}
|
||||||
sort.Strings(labelValues)
|
sort.Strings(labelValues)
|
||||||
qt.Printf("sort %d label values", len(labelValues))
|
qt.Printf("sort %d label values", len(labelValues))
|
||||||
return labelValues, isPartial, nil
|
return labelValues
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenants returns tenants until the given deadline.
|
// Tenants returns tenants until the given deadline.
|
||||||
|
@ -1110,7 +1191,8 @@ func GraphiteTagValues(qt *querytracer.Tracer, accountID, projectID uint32, deny
|
||||||
//
|
//
|
||||||
// It can be used for implementing https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find
|
// It can be used for implementing https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find
|
||||||
func TagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, denyPartialResponse bool, tr storage.TimeRange, tagKey, tagValuePrefix string,
|
func TagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, denyPartialResponse bool, tr storage.TimeRange, tagKey, tagValuePrefix string,
|
||||||
delimiter byte, maxSuffixes int, deadline searchutils.Deadline) ([]string, bool, error) {
|
delimiter byte, maxSuffixes int, deadline searchutils.Deadline,
|
||||||
|
) ([]string, bool, error) {
|
||||||
qt = qt.NewChild("get tag value suffixes for tagKey=%s, tagValuePrefix=%s, maxSuffixes=%d, timeRange=%s", tagKey, tagValuePrefix, maxSuffixes, &tr)
|
qt = qt.NewChild("get tag value suffixes for tagKey=%s, tagValuePrefix=%s, maxSuffixes=%d, timeRange=%s", tagKey, tagValuePrefix, maxSuffixes, &tr)
|
||||||
defer qt.Done()
|
defer qt.Done()
|
||||||
if deadline.Exceeded() {
|
if deadline.Exceeded() {
|
||||||
|
@ -1180,7 +1262,6 @@ func TSDBStatus(qt *querytracer.Tracer, denyPartialResponse bool, sq *storage.Se
|
||||||
if deadline.Exceeded() {
|
if deadline.Exceeded() {
|
||||||
return nil, false, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
|
return nil, false, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
|
||||||
}
|
}
|
||||||
requestData := sq.Marshal(nil)
|
|
||||||
// Send the query to all the storage nodes in parallel.
|
// Send the query to all the storage nodes in parallel.
|
||||||
type nodeResult struct {
|
type nodeResult struct {
|
||||||
status *storage.TSDBStatus
|
status *storage.TSDBStatus
|
||||||
|
@ -1188,26 +1269,37 @@ func TSDBStatus(qt *querytracer.Tracer, denyPartialResponse bool, sq *storage.Se
|
||||||
}
|
}
|
||||||
sns := getStorageNodes()
|
sns := getStorageNodes()
|
||||||
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
||||||
sn.tsdbStatusRequests.Inc()
|
err := populateSqTenantTokensIfNeeded(sq)
|
||||||
status, err := sn.getTSDBStatus(qt, requestData, focusLabel, topN, deadline)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sn.tsdbStatusErrors.Inc()
|
return []*nodeResult{{
|
||||||
err = fmt.Errorf("cannot obtain tsdb status from vmstorage %s: %w", sn.connPool.Addr(), err)
|
err: err,
|
||||||
}
|
}}
|
||||||
return &nodeResult{
|
|
||||||
status: status,
|
|
||||||
err: err,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return execSearchQuery(qt, sq, func(qt *querytracer.Tracer, requestData []byte, _ storage.TenantToken) any {
|
||||||
|
sn.tsdbStatusRequests.Inc()
|
||||||
|
status, err := sn.getTSDBStatus(qt, requestData, focusLabel, topN, deadline)
|
||||||
|
if err != nil {
|
||||||
|
sn.tsdbStatusErrors.Inc()
|
||||||
|
err = fmt.Errorf("cannot obtain tsdb status from vmstorage %s: %w", sn.connPool.Addr(), err)
|
||||||
|
}
|
||||||
|
return &nodeResult{
|
||||||
|
status: status,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collect results.
|
// Collect results.
|
||||||
var statuses []*storage.TSDBStatus
|
var statuses []*storage.TSDBStatus
|
||||||
isPartial, err := snr.collectResults(partialTSDBStatusResults, func(result any) error {
|
isPartial, err := snr.collectResults(partialTSDBStatusResults, func(result any) error {
|
||||||
nr := result.(*nodeResult)
|
for _, cr := range result.([]any) {
|
||||||
if nr.err != nil {
|
nr := cr.(*nodeResult)
|
||||||
return nr.err
|
if nr.err != nil {
|
||||||
|
return nr.err
|
||||||
|
}
|
||||||
|
statuses = append(statuses, nr.status)
|
||||||
}
|
}
|
||||||
statuses = append(statuses, nr.status)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1559,7 +1651,8 @@ var metricNamePool = &sync.Pool{
|
||||||
// It is the responsibility of f to call b.UnmarshalData before reading timestamps and values from the block.
|
// It is the responsibility of f to call b.UnmarshalData before reading timestamps and values from the block.
|
||||||
// It is the responsibility of f to filter blocks according to the given tr.
|
// It is the responsibility of f to filter blocks according to the given tr.
|
||||||
func ExportBlocks(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline searchutils.Deadline,
|
func ExportBlocks(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline searchutils.Deadline,
|
||||||
f func(mn *storage.MetricName, b *storage.Block, tr storage.TimeRange, workerID uint) error) error {
|
f func(mn *storage.MetricName, b *storage.Block, tr storage.TimeRange, workerID uint) error,
|
||||||
|
) error {
|
||||||
qt = qt.NewChild("export blocks: %s", sq)
|
qt = qt.NewChild("export blocks: %s", sq)
|
||||||
defer qt.Done()
|
defer qt.Done()
|
||||||
if deadline.Exceeded() {
|
if deadline.Exceeded() {
|
||||||
|
@ -1577,6 +1670,7 @@ func ExportBlocks(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline sear
|
||||||
if err := mn.Unmarshal(mb.MetricName); err != nil {
|
if err := mn.Unmarshal(mb.MetricName); err != nil {
|
||||||
return fmt.Errorf("cannot unmarshal metricName: %w", err)
|
return fmt.Errorf("cannot unmarshal metricName: %w", err)
|
||||||
}
|
}
|
||||||
|
metricNameTenantToTags(mn)
|
||||||
if err := f(mn, &mb.Block, tr, workerID); err != nil {
|
if err := f(mn, &mb.Block, tr, workerID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1603,7 +1697,6 @@ func SearchMetricNames(qt *querytracer.Tracer, denyPartialResponse bool, sq *sto
|
||||||
if deadline.Exceeded() {
|
if deadline.Exceeded() {
|
||||||
return nil, false, fmt.Errorf("timeout exceeded before starting to search metric names: %s", deadline.String())
|
return nil, false, fmt.Errorf("timeout exceeded before starting to search metric names: %s", deadline.String())
|
||||||
}
|
}
|
||||||
requestData := sq.Marshal(nil)
|
|
||||||
|
|
||||||
// Send the query to all the storage nodes in parallel.
|
// Send the query to all the storage nodes in parallel.
|
||||||
type nodeResult struct {
|
type nodeResult struct {
|
||||||
|
@ -1612,27 +1705,47 @@ func SearchMetricNames(qt *querytracer.Tracer, denyPartialResponse bool, sq *sto
|
||||||
}
|
}
|
||||||
sns := getStorageNodes()
|
sns := getStorageNodes()
|
||||||
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
|
||||||
sn.searchMetricNamesRequests.Inc()
|
err := populateSqTenantTokensIfNeeded(sq)
|
||||||
metricNames, err := sn.processSearchMetricNames(qt, requestData, deadline)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sn.searchMetricNamesErrors.Inc()
|
return []*nodeResult{{
|
||||||
err = fmt.Errorf("cannot search metric names on vmstorage %s: %w", sn.connPool.Addr(), err)
|
err: err,
|
||||||
}
|
}}
|
||||||
return &nodeResult{
|
|
||||||
metricNames: metricNames,
|
|
||||||
err: err,
|
|
||||||
}
|
}
|
||||||
|
return execSearchQuery(qt, sq, func(qt *querytracer.Tracer, requestData []byte, t storage.TenantToken) any {
|
||||||
|
sn.searchMetricNamesRequests.Inc()
|
||||||
|
metricNames, err := sn.processSearchMetricNames(qt, requestData, deadline)
|
||||||
|
if sq.IsMultiTenant {
|
||||||
|
// TODO: (@f41gh7) this function could produce duplicate labels
|
||||||
|
// if original metricName already have tenant labels
|
||||||
|
// fix it later
|
||||||
|
suffix := marshalAsTags(t.AccountID, t.ProjectID)
|
||||||
|
suffixStr := string(suffix)
|
||||||
|
for i := range metricNames {
|
||||||
|
metricNames[i] = metricNames[i] + suffixStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
sn.searchMetricNamesErrors.Inc()
|
||||||
|
err = fmt.Errorf("cannot search metric names on vmstorage %s: %w", sn.connPool.Addr(), err)
|
||||||
|
}
|
||||||
|
return &nodeResult{
|
||||||
|
metricNames: metricNames,
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collect results.
|
// Collect results.
|
||||||
metricNamesMap := make(map[string]struct{})
|
metricNamesMap := make(map[string]struct{})
|
||||||
isPartial, err := snr.collectResults(partialSearchMetricNamesResults, func(result any) error {
|
isPartial, err := snr.collectResults(partialSearchMetricNamesResults, func(result any) error {
|
||||||
nr := result.(*nodeResult)
|
for _, cr := range result.([]any) {
|
||||||
if nr.err != nil {
|
nr := cr.(*nodeResult)
|
||||||
return nr.err
|
if nr.err != nil {
|
||||||
}
|
return nr.err
|
||||||
for _, metricName := range nr.metricNames {
|
}
|
||||||
metricNamesMap[metricName] = struct{}{}
|
for _, metricName := range nr.metricNames {
|
||||||
|
metricNamesMap[metricName] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -1644,11 +1757,22 @@ func SearchMetricNames(qt *querytracer.Tracer, denyPartialResponse bool, sq *sto
|
||||||
for metricName := range metricNamesMap {
|
for metricName := range metricNamesMap {
|
||||||
metricNames = append(metricNames, metricName)
|
metricNames = append(metricNames, metricName)
|
||||||
}
|
}
|
||||||
sort.Strings(metricNames)
|
|
||||||
qt.Printf("sort %d metric names", len(metricNames))
|
qt.Printf("sort %d metric names", len(metricNames))
|
||||||
return metricNames, isPartial, nil
|
return metricNames, isPartial, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func marshalAsTags(accountID, projectID uint32) []byte {
|
||||||
|
buf := make([]byte, 0, 64)
|
||||||
|
var tag storage.Tag
|
||||||
|
tag.Key = []byte("vm_account_id")
|
||||||
|
tag.Value = strconv.AppendUint(tag.Value, uint64(accountID), 10)
|
||||||
|
buf = tag.Marshal(buf)
|
||||||
|
tag.Key = []byte("vm_project_id")
|
||||||
|
tag.Value = strconv.AppendUint(tag.Value[:0], uint64(projectID), 10)
|
||||||
|
buf = tag.Marshal(buf)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
// limitExceededErr error generated by vmselect
|
// limitExceededErr error generated by vmselect
|
||||||
// on checking complexity limits during processing responses
|
// on checking complexity limits during processing responses
|
||||||
// from storage nodes.
|
// from storage nodes.
|
||||||
|
@ -1722,21 +1846,22 @@ func ProcessSearchQuery(qt *querytracer.Tracer, denyPartialResponse bool, sq *st
|
||||||
addrs: addrssPool[m[metricName]].addrs,
|
addrs: addrssPool[m[metricName]].addrs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rss.shouldConvertTenantToLabels = sq.IsMultiTenant
|
||||||
rss.packedTimeseries = pts
|
rss.packedTimeseries = pts
|
||||||
return &rss, isPartial, nil
|
return &rss, isPartial, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessBlocks calls processBlock per each block matching the given sq.
|
// ProcessBlocks calls processBlock per each block matching the given sq.
|
||||||
func ProcessBlocks(qt *querytracer.Tracer, denyPartialResponse bool, sq *storage.SearchQuery,
|
func ProcessBlocks(qt *querytracer.Tracer, denyPartialResponse bool, sq *storage.SearchQuery,
|
||||||
processBlock func(mb *storage.MetricBlock, workerID uint) error, deadline searchutils.Deadline) (bool, error) {
|
processBlock func(mb *storage.MetricBlock, workerID uint) error, deadline searchutils.Deadline,
|
||||||
|
) (bool, error) {
|
||||||
sns := getStorageNodes()
|
sns := getStorageNodes()
|
||||||
return processBlocks(qt, sns, denyPartialResponse, sq, processBlock, deadline)
|
return processBlocks(qt, sns, denyPartialResponse, sq, processBlock, deadline)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processBlocks(qt *querytracer.Tracer, sns []*storageNode, denyPartialResponse bool, sq *storage.SearchQuery,
|
func processBlocks(qt *querytracer.Tracer, sns []*storageNode, denyPartialResponse bool, sq *storage.SearchQuery,
|
||||||
processBlock func(mb *storage.MetricBlock, workerID uint) error, deadline searchutils.Deadline) (bool, error) {
|
processBlock func(mb *storage.MetricBlock, workerID uint) error, deadline searchutils.Deadline,
|
||||||
requestData := sq.Marshal(nil)
|
) (bool, error) {
|
||||||
|
|
||||||
// Make sure that processBlock is no longer called after the exit from processBlocks() function.
|
// Make sure that processBlock is no longer called after the exit from processBlocks() function.
|
||||||
// Use per-worker WaitGroup instead of a shared WaitGroup in order to avoid inter-CPU contention,
|
// Use per-worker WaitGroup instead of a shared WaitGroup in order to avoid inter-CPU contention,
|
||||||
// which may significantly slow down the rate of processBlock calls on multi-CPU systems.
|
// which may significantly slow down the rate of processBlock calls on multi-CPU systems.
|
||||||
|
@ -1773,12 +1898,31 @@ func processBlocks(qt *querytracer.Tracer, sns []*storageNode, denyPartialRespon
|
||||||
|
|
||||||
// Send the query to all the storage nodes in parallel.
|
// Send the query to all the storage nodes in parallel.
|
||||||
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, workerID uint, sn *storageNode) any {
|
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, workerID uint, sn *storageNode) any {
|
||||||
sn.searchRequests.Inc()
|
var err error
|
||||||
err := sn.processSearchQuery(qt, requestData, f, workerID, deadline)
|
err = populateSqTenantTokensIfNeeded(sq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sn.searchErrors.Inc()
|
return &err
|
||||||
err = fmt.Errorf("cannot perform search on vmstorage %s: %w", sn.connPool.Addr(), err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res := execSearchQuery(qt, sq, func(qt *querytracer.Tracer, rd []byte, _ storage.TenantToken) any {
|
||||||
|
sn.searchRequests.Inc()
|
||||||
|
err = sn.processSearchQuery(qt, rd, f, workerID, deadline)
|
||||||
|
if err != nil {
|
||||||
|
sn.searchErrors.Inc()
|
||||||
|
err = fmt.Errorf("cannot perform search on vmstorage %s: %w", sn.connPool.Addr(), err)
|
||||||
|
return &err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &err
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, e := range res {
|
||||||
|
e := e.(*error)
|
||||||
|
if *e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &err
|
return &err
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1803,6 +1947,21 @@ func processBlocks(qt *querytracer.Tracer, sns []*storageNode, denyPartialRespon
|
||||||
return isPartial, nil
|
return isPartial, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func populateSqTenantTokensIfNeeded(sq *storage.SearchQuery) error {
|
||||||
|
if !sq.IsMultiTenant {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sq.TagFilterss) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tts, tfss := ApplyTenantFiltersToTagFilters(sq.TenantTokens, sq.TagFilterss)
|
||||||
|
sq.TenantTokens = tts
|
||||||
|
sq.TagFilterss = tfss
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type storageNodesRequest struct {
|
type storageNodesRequest struct {
|
||||||
denyPartialResponse bool
|
denyPartialResponse bool
|
||||||
resultsCh chan rpcResult
|
resultsCh chan rpcResult
|
||||||
|
@ -1817,7 +1976,8 @@ type rpcResult struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func startStorageNodesRequest(qt *querytracer.Tracer, sns []*storageNode, denyPartialResponse bool,
|
func startStorageNodesRequest(qt *querytracer.Tracer, sns []*storageNode, denyPartialResponse bool,
|
||||||
f func(qt *querytracer.Tracer, workerID uint, sn *storageNode) any) *storageNodesRequest {
|
f func(qt *querytracer.Tracer, workerID uint, sn *storageNode) any,
|
||||||
|
) *storageNodesRequest {
|
||||||
resultsCh := make(chan rpcResult, len(sns))
|
resultsCh := make(chan rpcResult, len(sns))
|
||||||
qts := make(map[*querytracer.Tracer]struct{}, len(sns))
|
qts := make(map[*querytracer.Tracer]struct{}, len(sns))
|
||||||
for idx, sn := range sns {
|
for idx, sn := range sns {
|
||||||
|
@ -2184,7 +2344,8 @@ func (sn *storageNode) getTenants(qt *querytracer.Tracer, tr storage.TimeRange,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sn *storageNode) getTagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, tr storage.TimeRange, tagKey, tagValuePrefix string,
|
func (sn *storageNode) getTagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, tr storage.TimeRange, tagKey, tagValuePrefix string,
|
||||||
delimiter byte, maxSuffixes int, deadline searchutils.Deadline) ([]string, error) {
|
delimiter byte, maxSuffixes int, deadline searchutils.Deadline,
|
||||||
|
) ([]string, error) {
|
||||||
var suffixes []string
|
var suffixes []string
|
||||||
f := func(bc *handshake.BufferedConn) error {
|
f := func(bc *handshake.BufferedConn) error {
|
||||||
ss, err := sn.getTagValueSuffixesOnConn(bc, accountID, projectID, tr, tagKey, tagValuePrefix, delimiter, maxSuffixes)
|
ss, err := sn.getTagValueSuffixesOnConn(bc, accountID, projectID, tr, tagKey, tagValuePrefix, delimiter, maxSuffixes)
|
||||||
|
@ -2249,7 +2410,8 @@ func (sn *storageNode) processSearchMetricNames(qt *querytracer.Tracer, requestD
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sn *storageNode) processSearchQuery(qt *querytracer.Tracer, requestData []byte, processBlock func(mb *storage.MetricBlock, workerID uint) error,
|
func (sn *storageNode) processSearchQuery(qt *querytracer.Tracer, requestData []byte, processBlock func(mb *storage.MetricBlock, workerID uint) error,
|
||||||
workerID uint, deadline searchutils.Deadline) error {
|
workerID uint, deadline searchutils.Deadline,
|
||||||
|
) error {
|
||||||
f := func(bc *handshake.BufferedConn) error {
|
f := func(bc *handshake.BufferedConn) error {
|
||||||
return sn.processSearchQueryOnConn(bc, requestData, processBlock, workerID)
|
return sn.processSearchQueryOnConn(bc, requestData, processBlock, workerID)
|
||||||
}
|
}
|
||||||
|
@ -2490,8 +2652,10 @@ func (sn *storageNode) getLabelNamesOnConn(bc *handshake.BufferedConn, requestDa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxLabelValueSize = 16 * 1024 * 1024
|
const (
|
||||||
const maxTenantValueSize = 16 * 1024 * 1024 // TODO: calc 'uint32:uint32'
|
maxLabelValueSize = 16 * 1024 * 1024
|
||||||
|
maxTenantValueSize = 16 * 1024 * 1024 // TODO: calc 'uint32:uint32'
|
||||||
|
)
|
||||||
|
|
||||||
func (sn *storageNode) getLabelValuesOnConn(bc *handshake.BufferedConn, labelName string, requestData []byte, maxLabelValues int) ([]string, error) {
|
func (sn *storageNode) getLabelValuesOnConn(bc *handshake.BufferedConn, labelName string, requestData []byte, maxLabelValues int) ([]string, error) {
|
||||||
// Send the request to sn.
|
// Send the request to sn.
|
||||||
|
@ -2575,7 +2739,8 @@ func (sn *storageNode) getTenantsOnConn(bc *handshake.BufferedConn, tr storage.T
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sn *storageNode) getTagValueSuffixesOnConn(bc *handshake.BufferedConn, accountID, projectID uint32,
|
func (sn *storageNode) getTagValueSuffixesOnConn(bc *handshake.BufferedConn, accountID, projectID uint32,
|
||||||
tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte, maxSuffixes int) ([]string, error) {
|
tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte, maxSuffixes int,
|
||||||
|
) ([]string, error) {
|
||||||
// Send the request to sn.
|
// Send the request to sn.
|
||||||
if err := sendAccountIDProjectID(bc, accountID, projectID); err != nil {
|
if err := sendAccountIDProjectID(bc, accountID, projectID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -2789,7 +2954,8 @@ func (sn *storageNode) processSearchMetricNamesOnConn(bc *handshake.BufferedConn
|
||||||
const maxMetricNameSize = 64 * 1024
|
const maxMetricNameSize = 64 * 1024
|
||||||
|
|
||||||
func (sn *storageNode) processSearchQueryOnConn(bc *handshake.BufferedConn, requestData []byte,
|
func (sn *storageNode) processSearchQueryOnConn(bc *handshake.BufferedConn, requestData []byte,
|
||||||
processBlock func(mb *storage.MetricBlock, workerID uint) error, workerID uint) error {
|
processBlock func(mb *storage.MetricBlock, workerID uint) error, workerID uint,
|
||||||
|
) error {
|
||||||
// Send the request to sn.
|
// Send the request to sn.
|
||||||
if err := writeBytes(bc, requestData); err != nil {
|
if err := writeBytes(bc, requestData); err != nil {
|
||||||
return fmt.Errorf("cannot write requestData: %w", err)
|
return fmt.Errorf("cannot write requestData: %w", err)
|
||||||
|
@ -3112,3 +3278,41 @@ func (pnc *perNodeCounter) GetTotal() uint64 {
|
||||||
//
|
//
|
||||||
// See https://github.com/golang/go/blob/704401ffa06c60e059c9e6e4048045b4ff42530a/src/runtime/malloc.go#L11
|
// See https://github.com/golang/go/blob/704401ffa06c60e059c9e6e4048045b4ff42530a/src/runtime/malloc.go#L11
|
||||||
const maxFastAllocBlockSize = 32 * 1024
|
const maxFastAllocBlockSize = 32 * 1024
|
||||||
|
|
||||||
|
// execSearchQuery calls cb for with marshaled requestData for each tenant in sq.
|
||||||
|
func execSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, cb func(qt *querytracer.Tracer, requestData []byte, t storage.TenantToken) any) []any {
|
||||||
|
var requestData []byte
|
||||||
|
var results []any
|
||||||
|
|
||||||
|
for i := range sq.TenantTokens {
|
||||||
|
requestData = sq.TenantTokens[i].Marshal(requestData)
|
||||||
|
requestData = sq.MarshaWithoutTenant(requestData)
|
||||||
|
qtL := qt
|
||||||
|
if sq.IsMultiTenant && qt.Enabled() {
|
||||||
|
qtL = qt.NewChild("query for tenant: %s", sq.TenantTokens[i].String())
|
||||||
|
}
|
||||||
|
r := cb(qtL, requestData, sq.TenantTokens[i])
|
||||||
|
if sq.IsMultiTenant {
|
||||||
|
qtL.Done()
|
||||||
|
}
|
||||||
|
results = append(results, r)
|
||||||
|
requestData = requestData[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// TenantToTags moves AccountID:ProjectID to corresponding tenant tags
|
||||||
|
// Erases values from AccountID:ProjectID
|
||||||
|
// TODO: @f41gh7 this function could produce duplicates
|
||||||
|
// if original metric name have tenant labels
|
||||||
|
func metricNameTenantToTags(mn *storage.MetricName) {
|
||||||
|
|
||||||
|
buf := make([]byte, 0, 8)
|
||||||
|
buf = strconv.AppendUint(buf, uint64(mn.AccountID), 10)
|
||||||
|
mn.AddTagBytes([]byte(`vm_account_id`), buf)
|
||||||
|
buf = strconv.AppendUint(buf[:0], uint64(mn.ProjectID), 10)
|
||||||
|
mn.AddTagBytes([]byte(`vm_project_id`), buf)
|
||||||
|
mn.AccountID = 0
|
||||||
|
mn.ProjectID = 0
|
||||||
|
}
|
||||||
|
|
189
app/vmselect/netstorage/tenant_cache.go
Normal file
189
app/vmselect/netstorage/tenant_cache.go
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
package netstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tenantsCacheDuration = flag.Duration("search.tenantCacheExpireDuration", 5*time.Minute, "The expiry duration for list of tenants for multi-tenant queries.")
|
||||||
|
)
|
||||||
|
|
||||||
|
// TenantsCached returns the list of tenants available in the storage.
|
||||||
|
func TenantsCached(qt *querytracer.Tracer, tr storage.TimeRange, deadline searchutils.Deadline) ([]storage.TenantToken, error) {
|
||||||
|
qt.Printf("fetching tenants on timeRange=%s", tr.String())
|
||||||
|
|
||||||
|
cached := tenantsCacheV.get(tr)
|
||||||
|
qt.Printf("fetched %d tenants from cache", len(cached))
|
||||||
|
if len(cached) > 0 {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tenants, err := Tenants(qt, tr, deadline)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot obtain tenants: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
qt.Printf("fetched %d tenants from storage", len(tenants))
|
||||||
|
|
||||||
|
tt := make([]storage.TenantToken, len(tenants))
|
||||||
|
for i, t := range tenants {
|
||||||
|
accountID, projectID, err := auth.ParseToken(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse tenant token %q: %w", t, err)
|
||||||
|
}
|
||||||
|
tt[i].AccountID = accountID
|
||||||
|
tt[i].ProjectID = projectID
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantsCacheV.put(tr, tt)
|
||||||
|
qt.Printf("put %d tenants into cache", len(tenants))
|
||||||
|
|
||||||
|
return tt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantsCacheV = func() *tenantsCache {
|
||||||
|
tc := newTenantsCache(*tenantsCacheDuration)
|
||||||
|
return tc
|
||||||
|
}()
|
||||||
|
|
||||||
|
type tenantsCacheItem struct {
|
||||||
|
tenants []storage.TenantToken
|
||||||
|
tr storage.TimeRange
|
||||||
|
expires time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type tenantsCache struct {
|
||||||
|
// items is used for intersection matches lookup
|
||||||
|
items []*tenantsCacheItem
|
||||||
|
|
||||||
|
itemExpiration time.Duration
|
||||||
|
|
||||||
|
requests atomic.Uint64
|
||||||
|
misses atomic.Uint64
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTenantsCache(expiration time.Duration) *tenantsCache {
|
||||||
|
tc := &tenantsCache{
|
||||||
|
items: make([]*tenantsCacheItem, 0),
|
||||||
|
itemExpiration: expiration,
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.GetOrCreateGauge(`vm_cache_requests_total{type="multitenancy/tenants"}`, func() float64 {
|
||||||
|
return float64(tc.Requests())
|
||||||
|
})
|
||||||
|
metrics.GetOrCreateGauge(`vm_cache_misses_total{type="multitenancy/tenants"}`, func() float64 {
|
||||||
|
return float64(tc.Misses())
|
||||||
|
})
|
||||||
|
metrics.GetOrCreateGauge(`vm_cache_entries{type="multitenancy/tenants"}`, func() float64 {
|
||||||
|
return float64(tc.Len())
|
||||||
|
})
|
||||||
|
|
||||||
|
return tc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *tenantsCache) cleanupLocked() {
|
||||||
|
expires := time.Now().Add(tc.itemExpiration)
|
||||||
|
for i := len(tc.items) - 1; i >= 0; i-- {
|
||||||
|
if tc.items[i].expires.Before(expires) {
|
||||||
|
tc.items = append(tc.items[:i], tc.items[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *tenantsCache) put(tr storage.TimeRange, tenants []storage.TenantToken) {
|
||||||
|
tc.mu.Lock()
|
||||||
|
defer tc.mu.Unlock()
|
||||||
|
alignTrToDay(&tr)
|
||||||
|
|
||||||
|
exp := time.Now().Add(timeutil.AddJitterToDuration(tc.itemExpiration))
|
||||||
|
|
||||||
|
ci := &tenantsCacheItem{
|
||||||
|
tenants: tenants,
|
||||||
|
tr: tr,
|
||||||
|
expires: exp,
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.items = append(tc.items, ci)
|
||||||
|
}
|
||||||
|
func (tc *tenantsCache) Requests() uint64 {
|
||||||
|
return tc.requests.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *tenantsCache) Misses() uint64 {
|
||||||
|
return tc.misses.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *tenantsCache) Len() uint64 {
|
||||||
|
tc.mu.Lock()
|
||||||
|
n := len(tc.items)
|
||||||
|
tc.mu.Unlock()
|
||||||
|
return uint64(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *tenantsCache) get(tr storage.TimeRange) []storage.TenantToken {
|
||||||
|
tc.requests.Add(1)
|
||||||
|
|
||||||
|
alignTrToDay(&tr)
|
||||||
|
return tc.getInternal(tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *tenantsCache) getInternal(tr storage.TimeRange) []storage.TenantToken {
|
||||||
|
tc.mu.Lock()
|
||||||
|
defer tc.mu.Unlock()
|
||||||
|
if len(tc.items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[storage.TenantToken]struct{})
|
||||||
|
cleanupNeeded := false
|
||||||
|
for idx := range tc.items {
|
||||||
|
ci := tc.items[idx]
|
||||||
|
if ci.expires.Before(time.Now()) {
|
||||||
|
cleanupNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasIntersection(tr, ci.tr) {
|
||||||
|
for _, t := range ci.tenants {
|
||||||
|
result[t] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cleanupNeeded {
|
||||||
|
tc.cleanupLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
tenants := make([]storage.TenantToken, 0, len(result))
|
||||||
|
for t := range result {
|
||||||
|
tenants = append(tenants, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenants
|
||||||
|
}
|
||||||
|
|
||||||
|
// alignTrToDay aligns the given time range to the day boundaries
|
||||||
|
// tr.minTimestamp will be set to the start of the day
|
||||||
|
// tr.maxTimestamp will be set to the end of the day
|
||||||
|
func alignTrToDay(tr *storage.TimeRange) {
|
||||||
|
tr.MinTimestamp = timeutil.StartOfDay(tr.MinTimestamp)
|
||||||
|
tr.MaxTimestamp = timeutil.EndOfDay(tr.MaxTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasIntersection checks if there is any intersection of the given time ranges
|
||||||
|
func hasIntersection(a, b storage.TimeRange) bool {
|
||||||
|
return a.MinTimestamp <= b.MaxTimestamp && a.MaxTimestamp >= b.MinTimestamp
|
||||||
|
}
|
91
app/vmselect/netstorage/tenant_cache_test.go
Normal file
91
app/vmselect/netstorage/tenant_cache_test.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package netstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetchingTenants(t *testing.T) {
|
||||||
|
tc := newTenantsCache(5 * time.Second)
|
||||||
|
|
||||||
|
dayMs := (time.Hour * 24 * 1000).Milliseconds()
|
||||||
|
|
||||||
|
tc.put(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 0}, []storage.TenantToken{
|
||||||
|
{AccountID: 1, ProjectID: 1},
|
||||||
|
{AccountID: 1, ProjectID: 0},
|
||||||
|
})
|
||||||
|
tc.put(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: dayMs - 1}, []storage.TenantToken{
|
||||||
|
{AccountID: 1, ProjectID: 1},
|
||||||
|
{AccountID: 1, ProjectID: 0},
|
||||||
|
})
|
||||||
|
tc.put(storage.TimeRange{MinTimestamp: dayMs, MaxTimestamp: 2*dayMs - 1}, []storage.TenantToken{
|
||||||
|
{AccountID: 2, ProjectID: 1},
|
||||||
|
{AccountID: 2, ProjectID: 0},
|
||||||
|
})
|
||||||
|
tc.put(storage.TimeRange{MinTimestamp: 2 * dayMs, MaxTimestamp: 3*dayMs - 1}, []storage.TenantToken{
|
||||||
|
{AccountID: 3, ProjectID: 1},
|
||||||
|
{AccountID: 3, ProjectID: 0},
|
||||||
|
})
|
||||||
|
|
||||||
|
f := func(tr storage.TimeRange, expectedTenants []storage.TenantToken) {
|
||||||
|
t.Helper()
|
||||||
|
tenants := tc.get(tr)
|
||||||
|
|
||||||
|
if len(tenants) == 0 && len(tenants) == len(expectedTenants) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortTenants := func(t []storage.TenantToken) func(i, j int) bool {
|
||||||
|
return func(i, j int) bool {
|
||||||
|
if t[i].AccountID == t[j].AccountID {
|
||||||
|
return t[i].ProjectID < t[j].ProjectID
|
||||||
|
}
|
||||||
|
return t[i].AccountID < t[j].AccountID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(tenants, sortTenants(tenants))
|
||||||
|
sort.Slice(expectedTenants, sortTenants(expectedTenants))
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tenants, expectedTenants) {
|
||||||
|
t.Fatalf("unexpected tenants; got %v; want %v", tenants, expectedTenants)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic time range coverage
|
||||||
|
f(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 0}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}})
|
||||||
|
f(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 100}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}})
|
||||||
|
f(storage.TimeRange{MinTimestamp: dayMs, MaxTimestamp: dayMs}, []storage.TenantToken{{AccountID: 2, ProjectID: 1}, {AccountID: 2, ProjectID: 0}})
|
||||||
|
f(storage.TimeRange{MinTimestamp: 2 * dayMs, MaxTimestamp: 2 * dayMs}, []storage.TenantToken{{AccountID: 3, ProjectID: 1}, {AccountID: 3, ProjectID: 0}})
|
||||||
|
f(storage.TimeRange{MinTimestamp: 3 * dayMs, MaxTimestamp: 3*dayMs + 1}, []storage.TenantToken{})
|
||||||
|
|
||||||
|
// Time range inside existing range
|
||||||
|
f(storage.TimeRange{MinTimestamp: dayMs / 2, MaxTimestamp: dayMs/2 + 100}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}})
|
||||||
|
f(storage.TimeRange{MinTimestamp: dayMs + dayMs/2, MaxTimestamp: dayMs + dayMs/2 + 100}, []storage.TenantToken{{AccountID: 2, ProjectID: 1}, {AccountID: 2, ProjectID: 0}})
|
||||||
|
f(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: dayMs / 2}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}})
|
||||||
|
f(storage.TimeRange{MinTimestamp: dayMs / 2, MaxTimestamp: dayMs - 1}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}})
|
||||||
|
|
||||||
|
// Overlapping time ranges
|
||||||
|
f(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 2*dayMs - 1}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}, {AccountID: 2, ProjectID: 1}, {AccountID: 2, ProjectID: 0}})
|
||||||
|
f(storage.TimeRange{MinTimestamp: dayMs / 2, MaxTimestamp: dayMs + dayMs/2}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}, {AccountID: 2, ProjectID: 1}, {AccountID: 2, ProjectID: 0}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasIntersection(t *testing.T) {
|
||||||
|
f := func(inner, outer storage.TimeRange, expected bool) {
|
||||||
|
t.Helper()
|
||||||
|
if hasIntersection(inner, outer) != expected {
|
||||||
|
t.Fatalf("unexpected result for inner=%+v, outer=%+v", inner, outer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 150}, storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 0}, true)
|
||||||
|
f(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 150}, storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 100}, true)
|
||||||
|
f(storage.TimeRange{MinTimestamp: 50, MaxTimestamp: 150}, storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 100}, true)
|
||||||
|
f(storage.TimeRange{MinTimestamp: 50, MaxTimestamp: 150}, storage.TimeRange{MinTimestamp: 10, MaxTimestamp: 80}, true)
|
||||||
|
|
||||||
|
f(storage.TimeRange{MinTimestamp: 0, MaxTimestamp: 50}, storage.TimeRange{MinTimestamp: 60, MaxTimestamp: 100}, false)
|
||||||
|
f(storage.TimeRange{MinTimestamp: 100, MaxTimestamp: 150}, storage.TimeRange{MinTimestamp: 60, MaxTimestamp: 80}, false)
|
||||||
|
}
|
129
app/vmselect/netstorage/tenant_filters.go
Normal file
129
app/vmselect/netstorage/tenant_filters.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
package netstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTenantTokensFromFilters returns the list of tenant tokens and the list of filters without tenant filters.
|
||||||
|
func GetTenantTokensFromFilters(qt *querytracer.Tracer, tr storage.TimeRange, tfs [][]storage.TagFilter, deadline searchutils.Deadline) ([]storage.TenantToken, [][]storage.TagFilter, error) {
|
||||||
|
tenants, err := TenantsCached(qt, tr, deadline)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot obtain tenants: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantFilters, otherFilters := splitFiltersByType(tfs)
|
||||||
|
|
||||||
|
tts, err := applyFiltersToTenants(tenants, tenantFilters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot apply filters to tenants: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tts, otherFilters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitFiltersByType(tfs [][]storage.TagFilter) ([][]storage.TagFilter, [][]storage.TagFilter) {
|
||||||
|
if len(tfs) == 0 {
|
||||||
|
return nil, tfs
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantFilters := make([][]storage.TagFilter, 0, len(tfs))
|
||||||
|
otherFilters := make([][]storage.TagFilter, 0, len(tfs))
|
||||||
|
for _, f := range tfs {
|
||||||
|
ffs := make([]storage.TagFilter, 0, len(f))
|
||||||
|
offs := make([]storage.TagFilter, 0, len(f))
|
||||||
|
for _, tf := range f {
|
||||||
|
if !isTenancyLabel(string(tf.Key)) {
|
||||||
|
offs = append(offs, tf)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ffs = append(ffs, tf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ffs) > 0 {
|
||||||
|
tenantFilters = append(tenantFilters, ffs)
|
||||||
|
}
|
||||||
|
if len(offs) > 0 {
|
||||||
|
otherFilters = append(otherFilters, offs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tenantFilters, otherFilters
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyTenantFiltersToTagFilters applies the given tenant filters to the given tag filters.
|
||||||
|
func ApplyTenantFiltersToTagFilters(tts []storage.TenantToken, tfs [][]storage.TagFilter) ([]storage.TenantToken, [][]storage.TagFilter) {
|
||||||
|
tenantFilters, otherFilters := splitFiltersByType(tfs)
|
||||||
|
if len(tenantFilters) == 0 {
|
||||||
|
return tts, otherFilters
|
||||||
|
}
|
||||||
|
|
||||||
|
tts, err := applyFiltersToTenants(tts, tenantFilters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return tts, otherFilters
|
||||||
|
}
|
||||||
|
|
||||||
|
func tagFiltersToString(tfs []storage.TagFilter) string {
|
||||||
|
a := make([]string, len(tfs))
|
||||||
|
for i, tf := range tfs {
|
||||||
|
a[i] = tf.String()
|
||||||
|
}
|
||||||
|
return "{" + strings.Join(a, ",") + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyFiltersToTenants applies the given filters to the given tenants.
|
||||||
|
// It returns the filtered tenants.
|
||||||
|
func applyFiltersToTenants(tenants []storage.TenantToken, filters [][]storage.TagFilter) ([]storage.TenantToken, error) {
|
||||||
|
// fast path - return all tenants if no filters are given
|
||||||
|
if len(filters) == 0 {
|
||||||
|
return tenants, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resultingTokens := make([]storage.TenantToken, 0, len(tenants))
|
||||||
|
lbs := make([][]prompbmarshal.Label, 0, len(filters))
|
||||||
|
lbsAux := make([]prompbmarshal.Label, 0, len(filters))
|
||||||
|
for _, token := range tenants {
|
||||||
|
lbsAuxLen := len(lbsAux)
|
||||||
|
lbsAux = append(lbsAux, prompbmarshal.Label{
|
||||||
|
Name: "vm_account_id",
|
||||||
|
Value: fmt.Sprintf("%d", token.AccountID),
|
||||||
|
}, prompbmarshal.Label{
|
||||||
|
Name: "vm_project_id",
|
||||||
|
Value: fmt.Sprintf("%d", token.ProjectID),
|
||||||
|
})
|
||||||
|
|
||||||
|
lbs = append(lbs, lbsAux[lbsAuxLen:])
|
||||||
|
}
|
||||||
|
|
||||||
|
promIfs := make([]promrelabel.IfExpression, len(filters))
|
||||||
|
for i, tags := range filters {
|
||||||
|
filter := tagFiltersToString(tags)
|
||||||
|
err := promIfs[i].Parse(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse if expression from filters %v: %s", filter, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, lb := range lbs {
|
||||||
|
for _, promIf := range promIfs {
|
||||||
|
if promIf.Match(lb) {
|
||||||
|
resultingTokens = append(resultingTokens, tenants[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultingTokens, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTenancyLabel returns true if the given label name is used for tenancy.
|
||||||
|
func isTenancyLabel(name string) bool {
|
||||||
|
return name == "vm_account_id" || name == "vm_project_id"
|
||||||
|
}
|
52
app/vmselect/netstorage/tenant_filters_test.go
Normal file
52
app/vmselect/netstorage/tenant_filters_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package netstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApplyFiltersToTenants(t *testing.T) {
|
||||||
|
f := func(filters [][]storage.TagFilter, tenants []storage.TenantToken, expectedTenants []storage.TenantToken) {
|
||||||
|
tenantsResult, err := applyFiltersToTenants(tenants, filters)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tenantsResult, expectedTenants) {
|
||||||
|
t.Fatalf("unexpected tenants result; got %v; want %v", tenantsResult, expectedTenants)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f(nil, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}})
|
||||||
|
|
||||||
|
f([][]storage.TagFilter{{{Key: []byte("vm_account_id"), Value: []byte("1"), IsNegative: false, IsRegexp: false}}}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}})
|
||||||
|
f([][]storage.TagFilter{{{Key: []byte("vm_account_id"), Value: []byte("1"), IsNegative: false, IsRegexp: false}, {Key: []byte("vm_project_id"), Value: []byte("0"), IsNegative: false, IsRegexp: false}}}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}}, []storage.TenantToken{{AccountID: 1, ProjectID: 0}})
|
||||||
|
|
||||||
|
f([][]storage.TagFilter{{{Key: []byte("vm_account_id"), Value: []byte("1[0-9]+"), IsNegative: false, IsRegexp: true}}}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 12323, ProjectID: 0}, {AccountID: 12323, ProjectID: 3}, {AccountID: 345, ProjectID: 0}}, []storage.TenantToken{{AccountID: 12323, ProjectID: 0}, {AccountID: 12323, ProjectID: 3}})
|
||||||
|
|
||||||
|
f([][]storage.TagFilter{{{Key: []byte("vm_account_id"), Value: []byte("1"), IsNegative: false, IsRegexp: false}, {Key: []byte("vm_project_id"), Value: []byte("0"), IsNegative: true, IsRegexp: false}}}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}, {AccountID: 1, ProjectID: 0}}, []storage.TenantToken{{AccountID: 1, ProjectID: 1}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsTenancyLabel(t *testing.T) {
|
||||||
|
f := func(label string, expected bool) {
|
||||||
|
t.Helper()
|
||||||
|
isTenancyLabel := isTenancyLabel(label)
|
||||||
|
if isTenancyLabel != expected {
|
||||||
|
t.Fatalf("unexpected result for label %q; got %v; want %v", label, isTenancyLabel, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f("vm_account_id", true)
|
||||||
|
f("vm_project_id", true)
|
||||||
|
|
||||||
|
// Test that the label is case-insensitive
|
||||||
|
f("VM_account_id", false)
|
||||||
|
f("VM_project_id", false)
|
||||||
|
|
||||||
|
// non-tenancy labels
|
||||||
|
f("job", false)
|
||||||
|
f("instance", false)
|
||||||
|
|
||||||
|
}
|
|
@ -13,6 +13,11 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
"github.com/VictoriaMetrics/metricsql"
|
||||||
|
|
||||||
|
"github.com/valyala/fastjson/fastfloat"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/querystats"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/querystats"
|
||||||
|
@ -28,9 +33,6 @@ import (
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
"github.com/VictoriaMetrics/metrics"
|
|
||||||
"github.com/VictoriaMetrics/metricsql"
|
|
||||||
"github.com/valyala/fastjson/fastfloat"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -124,7 +126,10 @@ func FederateHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter,
|
||||||
if cp.IsDefaultTimeRange() {
|
if cp.IsDefaultTimeRange() {
|
||||||
cp.start = cp.end - lookbackDelta
|
cp.start = cp.end - lookbackDelta
|
||||||
}
|
}
|
||||||
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, *maxFederateSeries)
|
sq, err := getSearchQuery(nil, at, cp, *maxFederateSeries)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot obtain search query: %w", err)
|
||||||
|
}
|
||||||
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
||||||
rss, isPartial, err := netstorage.ProcessSearchQuery(nil, denyPartialResponse, sq, cp.deadline)
|
rss, isPartial, err := netstorage.ProcessSearchQuery(nil, denyPartialResponse, sq, cp.deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -170,7 +175,10 @@ func ExportCSVHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter
|
||||||
fieldNames := strings.Split(format, ",")
|
fieldNames := strings.Split(format, ",")
|
||||||
reduceMemUsage := httputils.GetBool(r, "reduce_mem_usage")
|
reduceMemUsage := httputils.GetBool(r, "reduce_mem_usage")
|
||||||
|
|
||||||
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, *maxExportSeries)
|
sq, err := getSearchQuery(nil, at, cp, *maxExportSeries)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot obtain search query: %w", err)
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||||
bw := bufferedwriter.Get(w)
|
bw := bufferedwriter.Get(w)
|
||||||
defer bufferedwriter.Put(bw)
|
defer bufferedwriter.Put(bw)
|
||||||
|
@ -250,7 +258,10 @@ func ExportNativeHandler(startTime time.Time, at *auth.Token, w http.ResponseWri
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, *maxExportSeries)
|
sq, err := getSearchQuery(nil, at, cp, *maxExportSeries)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot obtain search query: %w", err)
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "VictoriaMetrics/native")
|
w.Header().Set("Content-Type", "VictoriaMetrics/native")
|
||||||
bw := bufferedwriter.Get(w)
|
bw := bufferedwriter.Get(w)
|
||||||
defer bufferedwriter.Put(bw)
|
defer bufferedwriter.Put(bw)
|
||||||
|
@ -392,7 +403,11 @@ func exportHandler(qt *querytracer.Tracer, at *auth.Token, w http.ResponseWriter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, *maxExportSeries)
|
sq, err := getSearchQuery(qt, at, cp, *maxExportSeries)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot obtain search query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
|
||||||
doneCh := make(chan error, 1)
|
doneCh := make(chan error, 1)
|
||||||
|
@ -450,7 +465,7 @@ func exportHandler(qt *querytracer.Tracer, at *auth.Token, w http.ResponseWriter
|
||||||
doneCh <- err
|
doneCh <- err
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
err := <-doneCh
|
err = <-doneCh
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot send data to remote client: %w", err)
|
return fmt.Errorf("cannot send data to remote client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -597,12 +612,14 @@ func LabelValuesHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.To
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
||||||
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, *maxLabelsAPISeries)
|
sq, err := getSearchQuery(qt, at, cp, *maxLabelsAPISeries)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
labelValues, isPartial, err := netstorage.LabelValues(qt, denyPartialResponse, labelName, sq, limit, cp.deadline)
|
labelValues, isPartial, err := netstorage.LabelValues(qt, denyPartialResponse, labelName, sq, limit, cp.deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot obtain values for label %q: %w", labelName, err)
|
return fmt.Errorf("cannot obtain values for label %q: %w", labelName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
bw := bufferedwriter.Get(w)
|
bw := bufferedwriter.Get(w)
|
||||||
defer bufferedwriter.Put(bw)
|
defer bufferedwriter.Put(bw)
|
||||||
|
@ -661,9 +678,12 @@ func TSDBStatusHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Tok
|
||||||
topN = n
|
topN = n
|
||||||
}
|
}
|
||||||
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
||||||
start := int64(date*secsPerDay) * 1000
|
cp.start = int64(date*secsPerDay) * 1000
|
||||||
end := int64((date+1)*secsPerDay)*1000 - 1
|
cp.end = int64((date+1)*secsPerDay)*1000 - 1
|
||||||
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, start, end, cp.filterss, *maxTSDBStatusSeries)
|
sq, err := getSearchQuery(qt, at, cp, *maxTSDBStatusSeries)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
status, isPartial, err := netstorage.TSDBStatus(qt, denyPartialResponse, sq, focusLabel, topN, cp.deadline)
|
status, isPartial, err := netstorage.TSDBStatus(qt, denyPartialResponse, sq, focusLabel, topN, cp.deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot obtain tsdb stats: %w", err)
|
return fmt.Errorf("cannot obtain tsdb stats: %w", err)
|
||||||
|
@ -696,7 +716,10 @@ func LabelsHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Token,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
||||||
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, *maxLabelsAPISeries)
|
sq, err := getSearchQuery(qt, at, cp, *maxLabelsAPISeries)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
labels, isPartial, err := netstorage.LabelNames(qt, denyPartialResponse, sq, limit, cp.deadline)
|
labels, isPartial, err := netstorage.LabelNames(qt, denyPartialResponse, sq, limit, cp.deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot obtain labels: %w", err)
|
return fmt.Errorf("cannot obtain labels: %w", err)
|
||||||
|
@ -712,6 +735,18 @@ func LabelsHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Token,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSearchQuery(qt *querytracer.Tracer, at *auth.Token, cp *commonParams, maxSeries int) (*storage.SearchQuery, error) {
|
||||||
|
if at != nil {
|
||||||
|
return storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, maxSeries), nil
|
||||||
|
}
|
||||||
|
tt, tfs, err := netstorage.GetTenantTokensFromFilters(qt, storage.TimeRange{MinTimestamp: cp.start, MaxTimestamp: cp.end}, cp.filterss, cp.deadline)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot obtain tenant tokens: %w", err)
|
||||||
|
}
|
||||||
|
sq := storage.NewMultiTenantSearchQuery(tt, cp.start, cp.end, tfs, maxSeries)
|
||||||
|
return sq, 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.
|
||||||
|
@ -756,8 +791,10 @@ func SeriesHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Token,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
sq, err := getSearchQuery(qt, at, cp, *maxSeriesLimit)
|
||||||
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, *maxSeriesLimit)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
denyPartialResponse := httputils.GetDenyPartialResponse(r)
|
||||||
metricNames, isPartial, err := netstorage.SearchMetricNames(qt, denyPartialResponse, sq, cp.deadline)
|
metricNames, isPartial, err := netstorage.SearchMetricNames(qt, denyPartialResponse, sq, cp.deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -883,7 +920,6 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Token, w
|
||||||
}
|
}
|
||||||
qs := &promql.QueryStats{}
|
qs := &promql.QueryStats{}
|
||||||
ec := &promql.EvalConfig{
|
ec := &promql.EvalConfig{
|
||||||
AuthToken: at,
|
|
||||||
Start: start,
|
Start: start,
|
||||||
End: start,
|
End: start,
|
||||||
Step: step,
|
Step: step,
|
||||||
|
@ -902,6 +938,11 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Token, w
|
||||||
DenyPartialResponse: httputils.GetDenyPartialResponse(r),
|
DenyPartialResponse: httputils.GetDenyPartialResponse(r),
|
||||||
QueryStats: qs,
|
QueryStats: qs,
|
||||||
}
|
}
|
||||||
|
err = populateAuthTokens(qt, ec, at, deadline)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot populate auth tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
result, err := promql.Exec(qt, ec, query, true)
|
result, err := promql.Exec(qt, ec, query, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error when executing query=%q for (time=%d, step=%d): %w", query, start, step, err)
|
return fmt.Errorf("error when executing query=%q for (time=%d, step=%d): %w", query, start, step, err)
|
||||||
|
@ -993,7 +1034,6 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Tok
|
||||||
|
|
||||||
qs := &promql.QueryStats{}
|
qs := &promql.QueryStats{}
|
||||||
ec := &promql.EvalConfig{
|
ec := &promql.EvalConfig{
|
||||||
AuthToken: at,
|
|
||||||
Start: start,
|
Start: start,
|
||||||
End: end,
|
End: end,
|
||||||
Step: step,
|
Step: step,
|
||||||
|
@ -1012,6 +1052,11 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Tok
|
||||||
DenyPartialResponse: httputils.GetDenyPartialResponse(r),
|
DenyPartialResponse: httputils.GetDenyPartialResponse(r),
|
||||||
QueryStats: qs,
|
QueryStats: qs,
|
||||||
}
|
}
|
||||||
|
err = populateAuthTokens(qt, ec, at, deadline)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot populate auth tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
result, err := promql.Exec(qt, ec, query, false)
|
result, err := promql.Exec(qt, ec, query, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1043,6 +1088,30 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, at *auth.Tok
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func populateAuthTokens(qt *querytracer.Tracer, ec *promql.EvalConfig, at *auth.Token, deadline searchutils.Deadline) error {
|
||||||
|
if at != nil {
|
||||||
|
ec.AuthTokens = []*auth.Token{at}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tt, tfs, err := netstorage.GetTenantTokensFromFilters(qt, storage.TimeRange{MinTimestamp: ec.Start, MaxTimestamp: ec.End}, ec.EnforcedTagFilterss, deadline)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot obtain tenant tokens for the given search query: %w", err)
|
||||||
|
}
|
||||||
|
ec.EnforcedTagFilterss = tfs
|
||||||
|
|
||||||
|
ats := make([]*auth.Token, len(tt))
|
||||||
|
for i, t := range tt {
|
||||||
|
ats[i] = &auth.Token{
|
||||||
|
AccountID: t.AccountID,
|
||||||
|
ProjectID: t.ProjectID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ec.AuthTokens = ats
|
||||||
|
ec.IsMultiTenant = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func removeEmptyValuesAndTimeseries(tss []netstorage.Result) []netstorage.Result {
|
func removeEmptyValuesAndTimeseries(tss []netstorage.Result) []netstorage.Result {
|
||||||
dst := tss[:0]
|
dst := tss[:0]
|
||||||
for i := range tss {
|
for i := range tss {
|
||||||
|
|
|
@ -19,16 +19,16 @@ import (
|
||||||
// If at is nil, then all the active queries across all the tenants are written.
|
// If at is nil, then all the active queries across all the tenants are written.
|
||||||
func ActiveQueriesHandler(at *auth.Token, w http.ResponseWriter, _ *http.Request) {
|
func ActiveQueriesHandler(at *auth.Token, w http.ResponseWriter, _ *http.Request) {
|
||||||
aqes := activeQueriesV.GetAll()
|
aqes := activeQueriesV.GetAll()
|
||||||
if at != nil {
|
|
||||||
// Filter out queries, which do not belong to at.
|
// Filter out queries, which do not belong to at.
|
||||||
dst := aqes[:0]
|
// if at is nil, then all the queries are returned for multi-tenant request
|
||||||
for _, aqe := range aqes {
|
dst := aqes[:0]
|
||||||
if aqe.accountID == at.AccountID && aqe.projectID == at.ProjectID {
|
for _, aqe := range aqes {
|
||||||
dst = append(dst, aqe)
|
if at == nil || (aqe.accountID == at.AccountID && aqe.projectID == at.ProjectID) {
|
||||||
}
|
dst = append(dst, aqe)
|
||||||
}
|
}
|
||||||
aqes = dst
|
|
||||||
}
|
}
|
||||||
|
aqes = dst
|
||||||
writeActiveQueries(w, aqes)
|
writeActiveQueries(w, aqes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,8 +42,8 @@ func writeActiveQueries(w http.ResponseWriter, aqes []activeQueryEntry) {
|
||||||
fmt.Fprintf(w, `{"status":"ok","data":[`)
|
fmt.Fprintf(w, `{"status":"ok","data":[`)
|
||||||
for i, aqe := range aqes {
|
for i, aqe := range aqes {
|
||||||
d := now.Sub(aqe.startTime)
|
d := now.Sub(aqe.startTime)
|
||||||
fmt.Fprintf(w, `{"duration":"%.3fs","id":"%016X","remote_addr":%s,"account_id":"%d","project_id":"%d","query":%s,"start":%d,"end":%d,"step":%d}`,
|
fmt.Fprintf(w, `{"duration":"%.3fs","id":"%016X","remote_addr":%s,"account_id":"%d","project_id":"%d","query":%s,"start":%d,"end":%d,"step":%d,"is_multitenant":%v}`,
|
||||||
d.Seconds(), aqe.qid, aqe.quotedRemoteAddr, aqe.accountID, aqe.projectID, stringsutil.JSONString(aqe.q), aqe.start, aqe.end, aqe.step)
|
d.Seconds(), aqe.qid, aqe.quotedRemoteAddr, aqe.accountID, aqe.projectID, stringsutil.JSONString(aqe.q), aqe.start, aqe.end, aqe.step, aqe.isMultitenant)
|
||||||
if i+1 < len(aqes) {
|
if i+1 < len(aqes) {
|
||||||
fmt.Fprintf(w, `,`)
|
fmt.Fprintf(w, `,`)
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@ type activeQueryEntry struct {
|
||||||
quotedRemoteAddr string
|
quotedRemoteAddr string
|
||||||
q string
|
q string
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
|
isMultitenant bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newActiveQueries() *activeQueries {
|
func newActiveQueries() *activeQueries {
|
||||||
|
@ -78,8 +79,12 @@ func newActiveQueries() *activeQueries {
|
||||||
|
|
||||||
func (aq *activeQueries) Add(ec *EvalConfig, q string) uint64 {
|
func (aq *activeQueries) Add(ec *EvalConfig, q string) uint64 {
|
||||||
var aqe activeQueryEntry
|
var aqe activeQueryEntry
|
||||||
aqe.accountID = ec.AuthToken.AccountID
|
if ec.IsMultiTenant {
|
||||||
aqe.projectID = ec.AuthToken.ProjectID
|
aqe.isMultitenant = true
|
||||||
|
} else {
|
||||||
|
aqe.accountID = ec.AuthTokens[0].AccountID
|
||||||
|
aqe.projectID = ec.AuthTokens[0].ProjectID
|
||||||
|
}
|
||||||
aqe.start = ec.Start
|
aqe.start = ec.Start
|
||||||
aqe.end = ec.End
|
aqe.end = ec.End
|
||||||
aqe.step = ec.Step
|
aqe.step = ec.Step
|
||||||
|
|
|
@ -12,6 +12,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
"github.com/VictoriaMetrics/metricsql"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
|
@ -26,8 +29,6 @@ import (
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
||||||
"github.com/VictoriaMetrics/metrics"
|
|
||||||
"github.com/VictoriaMetrics/metricsql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -109,10 +110,12 @@ func alignStartEnd(start, end, step int64) (int64, int64) {
|
||||||
|
|
||||||
// EvalConfig is the configuration required for query evaluation via Exec
|
// EvalConfig is the configuration required for query evaluation via Exec
|
||||||
type EvalConfig struct {
|
type EvalConfig struct {
|
||||||
AuthToken *auth.Token
|
AuthTokens []*auth.Token
|
||||||
Start int64
|
IsMultiTenant bool
|
||||||
End int64
|
|
||||||
Step int64
|
Start int64
|
||||||
|
End int64
|
||||||
|
Step int64
|
||||||
|
|
||||||
// MaxSeries is the maximum number of time series, which can be scanned by the query.
|
// MaxSeries is the maximum number of time series, which can be scanned by the query.
|
||||||
// Zero means 'no limit'
|
// Zero means 'no limit'
|
||||||
|
@ -160,7 +163,8 @@ type EvalConfig struct {
|
||||||
// copyEvalConfig returns src copy.
|
// copyEvalConfig returns src copy.
|
||||||
func copyEvalConfig(src *EvalConfig) *EvalConfig {
|
func copyEvalConfig(src *EvalConfig) *EvalConfig {
|
||||||
var ec EvalConfig
|
var ec EvalConfig
|
||||||
ec.AuthToken = src.AuthToken
|
ec.AuthTokens = src.AuthTokens
|
||||||
|
ec.IsMultiTenant = src.IsMultiTenant
|
||||||
ec.Start = src.Start
|
ec.Start = src.Start
|
||||||
ec.End = src.End
|
ec.End = src.End
|
||||||
ec.Step = src.Step
|
ec.Step = src.Step
|
||||||
|
@ -963,7 +967,7 @@ func evalRollupFuncWithSubquery(qt *querytracer.Tracer, ec *EvalConfig, funcName
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries)
|
sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries)
|
||||||
preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps)
|
preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps, ec.IsMultiTenant)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1107,13 +1111,18 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
||||||
}
|
}
|
||||||
return offset >= maxOffset
|
return offset >= maxOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
at := ec.AuthTokens[0]
|
||||||
|
if ec.IsMultiTenant {
|
||||||
|
at = nil
|
||||||
|
}
|
||||||
deleteCachedSeries := func(qt *querytracer.Tracer) {
|
deleteCachedSeries := func(qt *querytracer.Tracer) {
|
||||||
rollupResultCacheV.DeleteInstantValues(qt, ec.AuthToken, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
rollupResultCacheV.DeleteInstantValues(qt, at, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
||||||
}
|
}
|
||||||
getCachedSeries := func(qt *querytracer.Tracer) ([]*timeseries, int64, error) {
|
getCachedSeries := func(qt *querytracer.Tracer) ([]*timeseries, int64, error) {
|
||||||
again:
|
again:
|
||||||
offset := int64(0)
|
offset := int64(0)
|
||||||
tssCached := rollupResultCacheV.GetInstantValues(qt, ec.AuthToken, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
tssCached := rollupResultCacheV.GetInstantValues(qt, at, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
||||||
ec.QueryStats.addSeriesFetched(len(tssCached))
|
ec.QueryStats.addSeriesFetched(len(tssCached))
|
||||||
if len(tssCached) == 0 {
|
if len(tssCached) == 0 {
|
||||||
// Cache miss. Re-populate the missing data.
|
// Cache miss. Re-populate the missing data.
|
||||||
|
@ -1139,7 +1148,7 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
||||||
tss, err := evalAt(qt, timestamp, window)
|
tss, err := evalAt(qt, timestamp, window)
|
||||||
return tss, 0, err
|
return tss, 0, err
|
||||||
}
|
}
|
||||||
rollupResultCacheV.PutInstantValues(qt, ec.AuthToken, expr, window, ec.Step, ec.EnforcedTagFilterss, tss)
|
rollupResultCacheV.PutInstantValues(qt, at, expr, window, ec.Step, ec.EnforcedTagFilterss, tss)
|
||||||
return tss, offset, nil
|
return tss, offset, nil
|
||||||
}
|
}
|
||||||
// Cache hit. Verify whether it is OK to use the cached data.
|
// Cache hit. Verify whether it is OK to use the cached data.
|
||||||
|
@ -1707,7 +1716,7 @@ func evalRollupFuncNoCache(qt *querytracer.Tracer, ec *EvalConfig, funcName stri
|
||||||
}
|
}
|
||||||
// Obtain rollup configs before fetching data from db, so type errors could be caught earlier.
|
// Obtain rollup configs before fetching data from db, so type errors could be caught earlier.
|
||||||
sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries)
|
sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries)
|
||||||
preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps)
|
preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps, ec.IsMultiTenant)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1724,7 +1733,18 @@ func evalRollupFuncNoCache(qt *querytracer.Tracer, ec *EvalConfig, funcName stri
|
||||||
} else {
|
} else {
|
||||||
minTimestamp -= ec.Step
|
minTimestamp -= ec.Step
|
||||||
}
|
}
|
||||||
sq := storage.NewSearchQuery(ec.AuthToken.AccountID, ec.AuthToken.ProjectID, minTimestamp, ec.End, tfss, ec.MaxSeries)
|
var sq *storage.SearchQuery
|
||||||
|
|
||||||
|
if ec.IsMultiTenant {
|
||||||
|
ts := make([]storage.TenantToken, len(ec.AuthTokens))
|
||||||
|
for i, at := range ec.AuthTokens {
|
||||||
|
ts[i].ProjectID = at.ProjectID
|
||||||
|
ts[i].AccountID = at.AccountID
|
||||||
|
}
|
||||||
|
sq = storage.NewMultiTenantSearchQuery(ts, minTimestamp, ec.End, tfss, ec.MaxSeries)
|
||||||
|
} else {
|
||||||
|
sq = storage.NewSearchQuery(ec.AuthTokens[0].AccountID, ec.AuthTokens[0].ProjectID, minTimestamp, ec.End, tfss, ec.MaxSeries)
|
||||||
|
}
|
||||||
rss, isPartial, err := netstorage.ProcessSearchQuery(qt, ec.DenyPartialResponse, sq, ec.Deadline)
|
rss, isPartial, err := netstorage.ProcessSearchQuery(qt, ec.DenyPartialResponse, sq, ec.Deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1955,8 +1975,6 @@ var bbPool bytesutil.ByteBufferPool
|
||||||
func evalNumber(ec *EvalConfig, n float64) []*timeseries {
|
func evalNumber(ec *EvalConfig, n float64) []*timeseries {
|
||||||
var ts timeseries
|
var ts timeseries
|
||||||
ts.denyReuse = true
|
ts.denyReuse = true
|
||||||
ts.MetricName.AccountID = ec.AuthToken.AccountID
|
|
||||||
ts.MetricName.ProjectID = ec.AuthToken.ProjectID
|
|
||||||
timestamps := ec.getSharedTimestamps()
|
timestamps := ec.getSharedTimestamps()
|
||||||
values := make([]float64, len(timestamps))
|
values := make([]float64, len(timestamps))
|
||||||
for i := range timestamps {
|
for i := range timestamps {
|
||||||
|
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metricsql"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
"github.com/VictoriaMetrics/metricsql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetCommonLabelFilters(t *testing.T) {
|
func TestGetCommonLabelFilters(t *testing.T) {
|
||||||
|
|
|
@ -10,14 +10,15 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
"github.com/VictoriaMetrics/metricsql"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/querystats"
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/querystats"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
"github.com/VictoriaMetrics/metrics"
|
|
||||||
"github.com/VictoriaMetrics/metricsql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -39,10 +40,14 @@ var (
|
||||||
func Exec(qt *querytracer.Tracer, ec *EvalConfig, q string, isFirstPointOnly bool) ([]netstorage.Result, error) {
|
func Exec(qt *querytracer.Tracer, ec *EvalConfig, q string, isFirstPointOnly bool) ([]netstorage.Result, error) {
|
||||||
if querystats.Enabled() {
|
if querystats.Enabled() {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
ac := ec.AuthToken
|
|
||||||
defer func() {
|
defer func() {
|
||||||
querystats.RegisterQuery(ac.AccountID, ac.ProjectID, q, ec.End-ec.Start, startTime)
|
|
||||||
ec.QueryStats.addExecutionTimeMsec(startTime)
|
ec.QueryStats.addExecutionTimeMsec(startTime)
|
||||||
|
if ec.IsMultiTenant {
|
||||||
|
querystats.RegisterQueryMultiTenant(q, ec.End-ec.Start, startTime)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
at := ec.AuthTokens[0]
|
||||||
|
querystats.RegisterQuery(at.AccountID, at.ProjectID, q, ec.End-ec.Start, startTime)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,10 +65,11 @@ func TestExecSuccess(t *testing.T) {
|
||||||
f := func(q string, resultExpected []netstorage.Result) {
|
f := func(q string, resultExpected []netstorage.Result) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ec := &EvalConfig{
|
ec := &EvalConfig{
|
||||||
AuthToken: &auth.Token{
|
AuthTokens: []*auth.Token{{
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
},
|
}},
|
||||||
|
|
||||||
Start: start,
|
Start: start,
|
||||||
End: end,
|
End: end,
|
||||||
Step: step,
|
Step: step,
|
||||||
|
@ -82,7 +83,7 @@ func TestExecSuccess(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf(`unexpected error when executing %q: %s`, q, err)
|
t.Fatalf(`unexpected error when executing %q: %s`, q, err)
|
||||||
}
|
}
|
||||||
testResultsEqual(t, result, resultExpected)
|
testResultsEqual(t, result, resultExpected, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9345,10 +9346,10 @@ func TestExecError(t *testing.T) {
|
||||||
f := func(q string) {
|
f := func(q string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ec := &EvalConfig{
|
ec := &EvalConfig{
|
||||||
AuthToken: &auth.Token{
|
AuthTokens: []*auth.Token{{
|
||||||
AccountID: 123,
|
AccountID: 123,
|
||||||
ProjectID: 567,
|
ProjectID: 567,
|
||||||
},
|
}},
|
||||||
Start: 1000,
|
Start: 1000,
|
||||||
End: 2000,
|
End: 2000,
|
||||||
Step: 100,
|
Step: 100,
|
||||||
|
@ -9631,7 +9632,7 @@ func TestExecError(t *testing.T) {
|
||||||
f(`rollup_candlestick(time(), "foo")`)
|
f(`rollup_candlestick(time(), "foo")`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testResultsEqual(t *testing.T, result, resultExpected []netstorage.Result) {
|
func testResultsEqual(t *testing.T, result, resultExpected []netstorage.Result, verifyTenant bool) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if len(result) != len(resultExpected) {
|
if len(result) != len(resultExpected) {
|
||||||
t.Fatalf(`unexpected timeseries count; got %d; want %d`, len(result), len(resultExpected))
|
t.Fatalf(`unexpected timeseries count; got %d; want %d`, len(result), len(resultExpected))
|
||||||
|
@ -9639,17 +9640,17 @@ func testResultsEqual(t *testing.T, result, resultExpected []netstorage.Result)
|
||||||
for i := range result {
|
for i := range result {
|
||||||
r := &result[i]
|
r := &result[i]
|
||||||
rExpected := &resultExpected[i]
|
rExpected := &resultExpected[i]
|
||||||
testMetricNamesEqual(t, &r.MetricName, &rExpected.MetricName, i)
|
testMetricNamesEqual(t, &r.MetricName, &rExpected.MetricName, verifyTenant, i)
|
||||||
testRowsEqual(t, r.Values, r.Timestamps, rExpected.Values, rExpected.Timestamps)
|
testRowsEqual(t, r.Values, r.Timestamps, rExpected.Values, rExpected.Timestamps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMetricNamesEqual(t *testing.T, mn, mnExpected *storage.MetricName, pos int) {
|
func testMetricNamesEqual(t *testing.T, mn, mnExpected *storage.MetricName, verifyTenant bool, pos int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if mn.AccountID != mnExpected.AccountID {
|
if verifyTenant && mn.AccountID != mnExpected.AccountID {
|
||||||
t.Fatalf(`unexpected accountID; got %d; want %d`, mn.AccountID, mnExpected.AccountID)
|
t.Fatalf(`unexpected accountID; got %d; want %d`, mn.AccountID, mnExpected.AccountID)
|
||||||
}
|
}
|
||||||
if mn.ProjectID != mnExpected.ProjectID {
|
if verifyTenant && mn.ProjectID != mnExpected.ProjectID {
|
||||||
t.Fatalf(`unexpected projectID; got %d; want %d`, mn.ProjectID, mnExpected.ProjectID)
|
t.Fatalf(`unexpected projectID; got %d; want %d`, mn.ProjectID, mnExpected.ProjectID)
|
||||||
}
|
}
|
||||||
if string(mn.MetricGroup) != string(mnExpected.MetricGroup) {
|
if string(mn.MetricGroup) != string(mnExpected.MetricGroup) {
|
||||||
|
|
|
@ -369,7 +369,7 @@ func getRollupTag(expr metricsql.Expr) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start, end, step int64, maxPointsPerSeries int,
|
func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start, end, step int64, maxPointsPerSeries int,
|
||||||
window, lookbackDelta int64, sharedTimestamps []int64) (
|
window, lookbackDelta int64, sharedTimestamps []int64, isMultiTenant bool) (
|
||||||
func(values []float64, timestamps []int64), []*rollupConfig, error) {
|
func(values []float64, timestamps []int64), []*rollupConfig, error) {
|
||||||
preFunc := func(_ []float64, _ []int64) {}
|
preFunc := func(_ []float64, _ []int64) {}
|
||||||
funcName = strings.ToLower(funcName)
|
funcName = strings.ToLower(funcName)
|
||||||
|
@ -395,6 +395,7 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
|
||||||
Timestamps: sharedTimestamps,
|
Timestamps: sharedTimestamps,
|
||||||
isDefaultRollup: funcName == "default_rollup",
|
isDefaultRollup: funcName == "default_rollup",
|
||||||
samplesScannedPerCall: samplesScannedPerCall,
|
samplesScannedPerCall: samplesScannedPerCall,
|
||||||
|
isMultiTenant: isMultiTenant,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,6 +587,10 @@ type rollupConfig struct {
|
||||||
//
|
//
|
||||||
// If zero, then it is considered that Func scans all the samples passed to it.
|
// If zero, then it is considered that Func scans all the samples passed to it.
|
||||||
samplesScannedPerCall int
|
samplesScannedPerCall int
|
||||||
|
|
||||||
|
// Whether the rollup is used in multi-tenant mode.
|
||||||
|
// This is used in order to populate labels with tenancy information.
|
||||||
|
isMultiTenant bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *rollupConfig) getTimestamps() []int64 {
|
func (rc *rollupConfig) getTimestamps() []int64 {
|
||||||
|
|
|
@ -9,6 +9,10 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/fastcache"
|
||||||
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
"github.com/VictoriaMetrics/metricsql"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
||||||
|
@ -21,9 +25,6 @@ import (
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/workingsetcache"
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
|
||||||
"github.com/VictoriaMetrics/metrics"
|
|
||||||
"github.com/VictoriaMetrics/metricsql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -176,7 +177,6 @@ func (rrc *rollupResultCache) GetInstantValues(qt *querytracer.Tracer, at *auth.
|
||||||
// Obtain instant values from the cache
|
// Obtain instant values from the cache
|
||||||
bb := bbPool.Get()
|
bb := bbPool.Get()
|
||||||
defer bbPool.Put(bb)
|
defer bbPool.Put(bb)
|
||||||
|
|
||||||
bb.B = marshalRollupResultCacheKeyForInstantValues(bb.B[:0], at, expr, window, step, etfss)
|
bb.B = marshalRollupResultCacheKeyForInstantValues(bb.B[:0], at, expr, window, step, etfss)
|
||||||
tss, ok := rrc.getSeriesFromCache(qt, bb.B)
|
tss, ok := rrc.getSeriesFromCache(qt, bb.B)
|
||||||
if !ok || len(tss) == 0 {
|
if !ok || len(tss) == 0 {
|
||||||
|
@ -207,7 +207,6 @@ func (rrc *rollupResultCache) PutInstantValues(qt *querytracer.Tracer, at *auth.
|
||||||
|
|
||||||
bb := bbPool.Get()
|
bb := bbPool.Get()
|
||||||
defer bbPool.Put(bb)
|
defer bbPool.Put(bb)
|
||||||
|
|
||||||
bb.B = marshalRollupResultCacheKeyForInstantValues(bb.B[:0], at, expr, window, step, etfss)
|
bb.B = marshalRollupResultCacheKeyForInstantValues(bb.B[:0], at, expr, window, step, etfss)
|
||||||
_ = rrc.putSeriesToCache(qt, bb.B, step, tss)
|
_ = rrc.putSeriesToCache(qt, bb.B, step, tss)
|
||||||
}
|
}
|
||||||
|
@ -215,12 +214,10 @@ func (rrc *rollupResultCache) PutInstantValues(qt *querytracer.Tracer, at *auth.
|
||||||
func (rrc *rollupResultCache) DeleteInstantValues(qt *querytracer.Tracer, at *auth.Token, expr metricsql.Expr, window, step int64, etfss [][]storage.TagFilter) {
|
func (rrc *rollupResultCache) DeleteInstantValues(qt *querytracer.Tracer, at *auth.Token, expr metricsql.Expr, window, step int64, etfss [][]storage.TagFilter) {
|
||||||
bb := bbPool.Get()
|
bb := bbPool.Get()
|
||||||
defer bbPool.Put(bb)
|
defer bbPool.Put(bb)
|
||||||
|
|
||||||
bb.B = marshalRollupResultCacheKeyForInstantValues(bb.B[:0], at, expr, window, step, etfss)
|
bb.B = marshalRollupResultCacheKeyForInstantValues(bb.B[:0], at, expr, window, step, etfss)
|
||||||
if !rrc.putSeriesToCache(qt, bb.B, step, nil) {
|
if !rrc.putSeriesToCache(qt, bb.B, step, nil) {
|
||||||
logger.Panicf("BUG: cannot store zero series to cache")
|
logger.Panicf("BUG: cannot store zero series to cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
if qt.Enabled() {
|
if qt.Enabled() {
|
||||||
query := string(expr.AppendString(nil))
|
query := string(expr.AppendString(nil))
|
||||||
query = stringsutil.LimitStringLen(query, 300)
|
query = stringsutil.LimitStringLen(query, 300)
|
||||||
|
@ -239,8 +236,12 @@ func (rrc *rollupResultCache) GetSeries(qt *querytracer.Tracer, ec *EvalConfig,
|
||||||
// Obtain tss from the cache.
|
// Obtain tss from the cache.
|
||||||
bb := bbPool.Get()
|
bb := bbPool.Get()
|
||||||
defer bbPool.Put(bb)
|
defer bbPool.Put(bb)
|
||||||
|
at := ec.AuthTokens[0]
|
||||||
|
if ec.IsMultiTenant {
|
||||||
|
at = nil
|
||||||
|
}
|
||||||
|
|
||||||
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], ec.AuthToken, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], at, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
||||||
metainfoBuf := rrc.c.Get(nil, bb.B)
|
metainfoBuf := rrc.c.Get(nil, bb.B)
|
||||||
if len(metainfoBuf) == 0 {
|
if len(metainfoBuf) == 0 {
|
||||||
qt.Printf("nothing found")
|
qt.Printf("nothing found")
|
||||||
|
@ -262,7 +263,7 @@ func (rrc *rollupResultCache) GetSeries(qt *querytracer.Tracer, ec *EvalConfig,
|
||||||
if !ok {
|
if !ok {
|
||||||
mi.RemoveKey(key)
|
mi.RemoveKey(key)
|
||||||
metainfoBuf = mi.Marshal(metainfoBuf[:0])
|
metainfoBuf = mi.Marshal(metainfoBuf[:0])
|
||||||
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], ec.AuthToken, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], ec.AuthTokens[0], expr, window, ec.Step, ec.EnforcedTagFilterss)
|
||||||
rrc.c.Set(bb.B, metainfoBuf)
|
rrc.c.Set(bb.B, metainfoBuf)
|
||||||
return nil, ec.Start
|
return nil, ec.Start
|
||||||
}
|
}
|
||||||
|
@ -368,7 +369,11 @@ func (rrc *rollupResultCache) PutSeries(qt *querytracer.Tracer, ec *EvalConfig,
|
||||||
metainfoBuf := bbPool.Get()
|
metainfoBuf := bbPool.Get()
|
||||||
defer bbPool.Put(metainfoBuf)
|
defer bbPool.Put(metainfoBuf)
|
||||||
|
|
||||||
metainfoKey.B = marshalRollupResultCacheKeyForSeries(metainfoKey.B[:0], ec.AuthToken, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
at := ec.AuthTokens[0]
|
||||||
|
if ec.IsMultiTenant {
|
||||||
|
at = nil
|
||||||
|
}
|
||||||
|
metainfoKey.B = marshalRollupResultCacheKeyForSeries(metainfoKey.B[:0], at, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
||||||
metainfoBuf.B = rrc.c.Get(metainfoBuf.B[:0], metainfoKey.B)
|
metainfoBuf.B = rrc.c.Get(metainfoBuf.B[:0], metainfoKey.B)
|
||||||
var mi rollupResultCacheMetainfo
|
var mi rollupResultCacheMetainfo
|
||||||
if len(metainfoBuf.B) > 0 {
|
if len(metainfoBuf.B) > 0 {
|
||||||
|
@ -508,8 +513,10 @@ func marshalRollupResultCacheKeyForSeries(dst []byte, at *auth.Token, expr metri
|
||||||
dst = append(dst, rollupResultCacheVersion)
|
dst = append(dst, rollupResultCacheVersion)
|
||||||
dst = encoding.MarshalUint64(dst, rollupResultCacheKeyPrefix.Load())
|
dst = encoding.MarshalUint64(dst, rollupResultCacheKeyPrefix.Load())
|
||||||
dst = append(dst, rollupResultCacheTypeSeries)
|
dst = append(dst, rollupResultCacheTypeSeries)
|
||||||
dst = encoding.MarshalUint32(dst, at.AccountID)
|
if at != nil {
|
||||||
dst = encoding.MarshalUint32(dst, at.ProjectID)
|
dst = encoding.MarshalUint32(dst, at.AccountID)
|
||||||
|
dst = encoding.MarshalUint32(dst, at.ProjectID)
|
||||||
|
}
|
||||||
dst = encoding.MarshalInt64(dst, window)
|
dst = encoding.MarshalInt64(dst, window)
|
||||||
dst = encoding.MarshalInt64(dst, step)
|
dst = encoding.MarshalInt64(dst, step)
|
||||||
dst = marshalTagFiltersForRollupResultCacheKey(dst, etfs)
|
dst = marshalTagFiltersForRollupResultCacheKey(dst, etfs)
|
||||||
|
@ -521,8 +528,10 @@ func marshalRollupResultCacheKeyForInstantValues(dst []byte, at *auth.Token, exp
|
||||||
dst = append(dst, rollupResultCacheVersion)
|
dst = append(dst, rollupResultCacheVersion)
|
||||||
dst = encoding.MarshalUint64(dst, rollupResultCacheKeyPrefix.Load())
|
dst = encoding.MarshalUint64(dst, rollupResultCacheKeyPrefix.Load())
|
||||||
dst = append(dst, rollupResultCacheTypeInstantValues)
|
dst = append(dst, rollupResultCacheTypeInstantValues)
|
||||||
dst = encoding.MarshalUint32(dst, at.AccountID)
|
if at != nil {
|
||||||
dst = encoding.MarshalUint32(dst, at.ProjectID)
|
dst = encoding.MarshalUint32(dst, at.AccountID)
|
||||||
|
dst = encoding.MarshalUint32(dst, at.ProjectID)
|
||||||
|
}
|
||||||
dst = encoding.MarshalInt64(dst, window)
|
dst = encoding.MarshalInt64(dst, window)
|
||||||
dst = encoding.MarshalInt64(dst, step)
|
dst = encoding.MarshalInt64(dst, step)
|
||||||
dst = marshalTagFiltersForRollupResultCacheKey(dst, etfs)
|
dst = marshalTagFiltersForRollupResultCacheKey(dst, etfs)
|
||||||
|
|
|
@ -4,10 +4,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metricsql"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||||
"github.com/VictoriaMetrics/metricsql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRollupResultCacheInitStop(t *testing.T) {
|
func TestRollupResultCacheInitStop(t *testing.T) {
|
||||||
|
@ -40,10 +41,10 @@ func TestRollupResultCache(t *testing.T) {
|
||||||
Step: 200,
|
Step: 200,
|
||||||
MaxPointsPerSeries: 1e4,
|
MaxPointsPerSeries: 1e4,
|
||||||
|
|
||||||
AuthToken: &auth.Token{
|
AuthTokens: []*auth.Token{{
|
||||||
AccountID: 333,
|
AccountID: 333,
|
||||||
ProjectID: 843,
|
ProjectID: 843,
|
||||||
},
|
}},
|
||||||
|
|
||||||
MayCache: true,
|
MayCache: true,
|
||||||
}
|
}
|
||||||
|
@ -322,7 +323,60 @@ func TestRollupResultCache(t *testing.T) {
|
||||||
}
|
}
|
||||||
testTimeseriesEqual(t, tss, tssExpected)
|
testTimeseriesEqual(t, tss, tssExpected)
|
||||||
})
|
})
|
||||||
|
t.Run("multi-tenant cache can be retrieved", func(t *testing.T) {
|
||||||
|
ResetRollupResultCache()
|
||||||
|
tssGolden := []*timeseries{
|
||||||
|
{
|
||||||
|
MetricName: storage.MetricName{
|
||||||
|
AccountID: 0,
|
||||||
|
ProjectID: 0,
|
||||||
|
},
|
||||||
|
Timestamps: []int64{800, 1000, 1200},
|
||||||
|
Values: []float64{0, 1, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MetricName: storage.MetricName{
|
||||||
|
AccountID: 0,
|
||||||
|
ProjectID: 1,
|
||||||
|
},
|
||||||
|
Timestamps: []int64{800, 1000, 1200},
|
||||||
|
Values: []float64{0, 1, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MetricName: storage.MetricName{
|
||||||
|
AccountID: 1,
|
||||||
|
ProjectID: 1,
|
||||||
|
},
|
||||||
|
Timestamps: []int64{800, 1000, 1200},
|
||||||
|
Values: []float64{0, 1, 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ecL := copyEvalConfig(ec)
|
||||||
|
ecL.Start = 800
|
||||||
|
ecL.AuthTokens = []*auth.Token{
|
||||||
|
{
|
||||||
|
AccountID: 0,
|
||||||
|
ProjectID: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountID: 0,
|
||||||
|
ProjectID: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountID: 1,
|
||||||
|
ProjectID: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ecL.IsMultiTenant = true
|
||||||
|
rollupResultCacheV.PutSeries(nil, ecL, fe, window, tssGolden)
|
||||||
|
|
||||||
|
tss, newStart := rollupResultCacheV.GetSeries(nil, ecL, fe, window)
|
||||||
|
if newStart != 1400 {
|
||||||
|
t.Fatalf("unexpected newStart; got %d; want %d", newStart, 1400)
|
||||||
|
}
|
||||||
|
|
||||||
|
testTimeseriesEqual(t, tss, tssGolden)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMergeSeries(t *testing.T) {
|
func TestMergeSeries(t *testing.T) {
|
||||||
|
@ -511,7 +565,7 @@ func testTimeseriesEqual(t *testing.T, tss, tssExpected []*timeseries) {
|
||||||
}
|
}
|
||||||
for i, ts := range tss {
|
for i, ts := range tss {
|
||||||
tsExpected := tssExpected[i]
|
tsExpected := tssExpected[i]
|
||||||
testMetricNamesEqual(t, &ts.MetricName, &tsExpected.MetricName, i)
|
testMetricNamesEqual(t, &ts.MetricName, &tsExpected.MetricName, true, i)
|
||||||
testRowsEqual(t, ts.Values, ts.Timestamps, tsExpected.Values, tsExpected.Timestamps)
|
testRowsEqual(t, ts.Values, ts.Timestamps, tsExpected.Values, tsExpected.Timestamps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -287,8 +287,9 @@ func marshalMetricTagsFast(dst []byte, tags []storage.Tag) []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalMetricNameSorted(dst []byte, mn *storage.MetricName) []byte {
|
func marshalMetricNameSorted(dst []byte, mn *storage.MetricName) []byte {
|
||||||
// Do not marshal AccountID and ProjectID, since they are unused.
|
|
||||||
dst = marshalBytesFast(dst, mn.MetricGroup)
|
dst = marshalBytesFast(dst, mn.MetricGroup)
|
||||||
|
dst = encoding.MarshalUint32(dst, mn.AccountID)
|
||||||
|
dst = encoding.MarshalUint32(dst, mn.ProjectID)
|
||||||
return marshalMetricTagsSorted(dst, mn)
|
return marshalMetricTagsSorted(dst, mn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,14 @@ func RegisterQuery(accountID, projectID uint32, query string, timeRangeMsecs int
|
||||||
qsTracker.registerQuery(accountID, projectID, query, timeRangeMsecs, startTime)
|
qsTracker.registerQuery(accountID, projectID, query, timeRangeMsecs, startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterQueryMultiTenant registers the query on the given timeRangeMsecs, which has been started at startTime.
|
||||||
|
//
|
||||||
|
// RegisterQueryMultiTenant must be called when the query is finished.
|
||||||
|
func RegisterQueryMultiTenant(query string, timeRangeMsecs int64, startTime time.Time) {
|
||||||
|
initOnce.Do(initQueryStats)
|
||||||
|
qsTracker.registerQueryMultiTenant(query, timeRangeMsecs, startTime)
|
||||||
|
}
|
||||||
|
|
||||||
// WriteJSONQueryStats writes query stats to given writer in json format.
|
// WriteJSONQueryStats writes query stats to given writer in json format.
|
||||||
func WriteJSONQueryStats(w io.Writer, topN int, maxLifetime time.Duration) {
|
func WriteJSONQueryStats(w io.Writer, topN int, maxLifetime time.Duration) {
|
||||||
initOnce.Do(initQueryStats)
|
initOnce.Do(initQueryStats)
|
||||||
|
@ -66,6 +74,7 @@ type queryStatRecord struct {
|
||||||
timeRangeSecs int64
|
timeRangeSecs int64
|
||||||
registerTime time.Time
|
registerTime time.Time
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
|
multiTenant bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type queryStatKey struct {
|
type queryStatKey struct {
|
||||||
|
@ -73,6 +82,7 @@ type queryStatKey struct {
|
||||||
projectID uint32
|
projectID uint32
|
||||||
query string
|
query string
|
||||||
timeRangeSecs int64
|
timeRangeSecs int64
|
||||||
|
multiTenant bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type accountProjectFilter struct {
|
type accountProjectFilter struct {
|
||||||
|
@ -100,7 +110,7 @@ func (qst *queryStatsTracker) writeJSONQueryStats(w io.Writer, topN int, apFilte
|
||||||
fmt.Fprintf(w, `"topByCount":[`)
|
fmt.Fprintf(w, `"topByCount":[`)
|
||||||
topByCount := qst.getTopByCount(topN, apFilter, maxLifetime)
|
topByCount := qst.getTopByCount(topN, apFilter, maxLifetime)
|
||||||
for i, r := range topByCount {
|
for i, r := range topByCount {
|
||||||
fmt.Fprintf(w, `{"accountID":%d,"projectID":%d,"query":%s,"timeRangeSeconds":%d,"count":%d}`, r.accountID, r.projectID, stringsutil.JSONString(r.query), r.timeRangeSecs, r.count)
|
fmt.Fprintf(w, `{"accountID":%d,"projectID":%d,"query":%s,"timeRangeSeconds":%d,"count":%d,"multiTenant":%v}`, r.accountID, r.projectID, stringsutil.JSONString(r.query), r.timeRangeSecs, r.count, r.multiTenant)
|
||||||
if i+1 < len(topByCount) {
|
if i+1 < len(topByCount) {
|
||||||
fmt.Fprintf(w, `,`)
|
fmt.Fprintf(w, `,`)
|
||||||
}
|
}
|
||||||
|
@ -108,8 +118,8 @@ func (qst *queryStatsTracker) writeJSONQueryStats(w io.Writer, topN int, apFilte
|
||||||
fmt.Fprintf(w, `],"topByAvgDuration":[`)
|
fmt.Fprintf(w, `],"topByAvgDuration":[`)
|
||||||
topByAvgDuration := qst.getTopByAvgDuration(topN, apFilter, maxLifetime)
|
topByAvgDuration := qst.getTopByAvgDuration(topN, apFilter, maxLifetime)
|
||||||
for i, r := range topByAvgDuration {
|
for i, r := range topByAvgDuration {
|
||||||
fmt.Fprintf(w, `{"accountID":%d,"projectID":%d,"query":%s,"timeRangeSeconds":%d,"avgDurationSeconds":%.3f,"count":%d}`,
|
fmt.Fprintf(w, `{"accountID":%d,"projectID":%d,"query":%s,"timeRangeSeconds":%d,"avgDurationSeconds":%.3f,"count":%d,"multiTenant": %v}`,
|
||||||
r.accountID, r.projectID, stringsutil.JSONString(r.query), r.timeRangeSecs, r.duration.Seconds(), r.count)
|
r.accountID, r.projectID, stringsutil.JSONString(r.query), r.timeRangeSecs, r.duration.Seconds(), r.count, r.multiTenant)
|
||||||
if i+1 < len(topByAvgDuration) {
|
if i+1 < len(topByAvgDuration) {
|
||||||
fmt.Fprintf(w, `,`)
|
fmt.Fprintf(w, `,`)
|
||||||
}
|
}
|
||||||
|
@ -117,8 +127,8 @@ func (qst *queryStatsTracker) writeJSONQueryStats(w io.Writer, topN int, apFilte
|
||||||
fmt.Fprintf(w, `],"topBySumDuration":[`)
|
fmt.Fprintf(w, `],"topBySumDuration":[`)
|
||||||
topBySumDuration := qst.getTopBySumDuration(topN, apFilter, maxLifetime)
|
topBySumDuration := qst.getTopBySumDuration(topN, apFilter, maxLifetime)
|
||||||
for i, r := range topBySumDuration {
|
for i, r := range topBySumDuration {
|
||||||
fmt.Fprintf(w, `{"accountID":%d,"projectID":%d,"query":%s,"timeRangeSeconds":%d,"sumDurationSeconds":%.3f,"count":%d}`,
|
fmt.Fprintf(w, `{"accountID":%d,"projectID":%d,"query":%s,"timeRangeSeconds":%d,"sumDurationSeconds":%.3f,"count":%d,"multiTenant":%v}`,
|
||||||
r.accountID, r.projectID, stringsutil.JSONString(r.query), r.timeRangeSecs, r.duration.Seconds(), r.count)
|
r.accountID, r.projectID, stringsutil.JSONString(r.query), r.timeRangeSecs, r.duration.Seconds(), r.count, r.multiTenant)
|
||||||
if i+1 < len(topBySumDuration) {
|
if i+1 < len(topBySumDuration) {
|
||||||
fmt.Fprintf(w, `,`)
|
fmt.Fprintf(w, `,`)
|
||||||
}
|
}
|
||||||
|
@ -151,6 +161,30 @@ func (qst *queryStatsTracker) registerQuery(accountID, projectID uint32, query s
|
||||||
r.duration = duration
|
r.duration = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qst *queryStatsTracker) registerQueryMultiTenant(query string, timeRangeMsecs int64, startTime time.Time) {
|
||||||
|
registerTime := time.Now()
|
||||||
|
duration := registerTime.Sub(startTime)
|
||||||
|
if duration < *minQueryDuration {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
qst.mu.Lock()
|
||||||
|
defer qst.mu.Unlock()
|
||||||
|
|
||||||
|
a := qst.a
|
||||||
|
idx := qst.nextIdx
|
||||||
|
if idx >= uint(len(a)) {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
qst.nextIdx = idx + 1
|
||||||
|
r := &a[idx]
|
||||||
|
r.multiTenant = true
|
||||||
|
r.query = query
|
||||||
|
r.timeRangeSecs = timeRangeMsecs / 1000
|
||||||
|
r.registerTime = registerTime
|
||||||
|
r.duration = duration
|
||||||
|
}
|
||||||
|
|
||||||
func (r *queryStatRecord) matches(apFilter *accountProjectFilter, currentTime time.Time, maxLifetime time.Duration) bool {
|
func (r *queryStatRecord) matches(apFilter *accountProjectFilter, currentTime time.Time, maxLifetime time.Duration) bool {
|
||||||
if r.query == "" || currentTime.Sub(r.registerTime) > maxLifetime {
|
if r.query == "" || currentTime.Sub(r.registerTime) > maxLifetime {
|
||||||
return false
|
return false
|
||||||
|
@ -167,6 +201,7 @@ func (r *queryStatRecord) key() queryStatKey {
|
||||||
projectID: r.projectID,
|
projectID: r.projectID,
|
||||||
query: r.query,
|
query: r.query,
|
||||||
timeRangeSecs: r.timeRangeSecs,
|
timeRangeSecs: r.timeRangeSecs,
|
||||||
|
multiTenant: r.multiTenant,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,6 +225,7 @@ func (qst *queryStatsTracker) getTopByCount(topN int, apFilter *accountProjectFi
|
||||||
query: k.query,
|
query: k.query,
|
||||||
timeRangeSecs: k.timeRangeSecs,
|
timeRangeSecs: k.timeRangeSecs,
|
||||||
count: count,
|
count: count,
|
||||||
|
multiTenant: k.multiTenant,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Slice(a, func(i, j int) bool {
|
sort.Slice(a, func(i, j int) bool {
|
||||||
|
@ -207,6 +243,7 @@ type queryStatByCount struct {
|
||||||
query string
|
query string
|
||||||
timeRangeSecs int64
|
timeRangeSecs int64
|
||||||
count int
|
count int
|
||||||
|
multiTenant bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qst *queryStatsTracker) getTopByAvgDuration(topN int, apFilter *accountProjectFilter, maxLifetime time.Duration) []queryStatByDuration {
|
func (qst *queryStatsTracker) getTopByAvgDuration(topN int, apFilter *accountProjectFilter, maxLifetime time.Duration) []queryStatByDuration {
|
||||||
|
@ -237,6 +274,7 @@ func (qst *queryStatsTracker) getTopByAvgDuration(topN int, apFilter *accountPro
|
||||||
timeRangeSecs: k.timeRangeSecs,
|
timeRangeSecs: k.timeRangeSecs,
|
||||||
duration: ks.sum / time.Duration(ks.count),
|
duration: ks.sum / time.Duration(ks.count),
|
||||||
count: ks.count,
|
count: ks.count,
|
||||||
|
multiTenant: k.multiTenant,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Slice(a, func(i, j int) bool {
|
sort.Slice(a, func(i, j int) bool {
|
||||||
|
@ -255,6 +293,7 @@ type queryStatByDuration struct {
|
||||||
timeRangeSecs int64
|
timeRangeSecs int64
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
count int
|
count int
|
||||||
|
multiTenant bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qst *queryStatsTracker) getTopBySumDuration(topN int, apFilter *accountProjectFilter, maxLifetime time.Duration) []queryStatByDuration {
|
func (qst *queryStatsTracker) getTopBySumDuration(topN int, apFilter *accountProjectFilter, maxLifetime time.Duration) []queryStatByDuration {
|
||||||
|
@ -285,6 +324,7 @@ func (qst *queryStatsTracker) getTopBySumDuration(topN int, apFilter *accountPro
|
||||||
timeRangeSecs: k.timeRangeSecs,
|
timeRangeSecs: k.timeRangeSecs,
|
||||||
duration: kd.sum,
|
duration: kd.sum,
|
||||||
count: kd.count,
|
count: kd.count,
|
||||||
|
multiTenant: k.multiTenant,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Slice(a, func(i, j int) bool {
|
sort.Slice(a, func(i, j int) bool {
|
||||||
|
|
|
@ -113,8 +113,55 @@ such as [Graphite](https://docs.victoriametrics.com/#how-to-send-data-from-graph
|
||||||
[InfluxDB line protocol via TCP and UDP](https://docs.victoriametrics.com/#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf) and
|
[InfluxDB line protocol via TCP and UDP](https://docs.victoriametrics.com/#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf) and
|
||||||
[OpenTSDB telnet put protocol](https://docs.victoriametrics.com/#sending-data-via-telnet-put-protocol).
|
[OpenTSDB telnet put protocol](https://docs.victoriametrics.com/#sending-data-via-telnet-put-protocol).
|
||||||
|
|
||||||
|
|
||||||
|
`vmselect` can execute queries over multiple [tenants](#multitenancy) via special `multitenant` endpoints `http://vmselect:8481/select/multitenant/<suffix>`.
|
||||||
|
Currently supported endpoints for `<suffix>` are:
|
||||||
|
- `/prometheus/api/v1/query`
|
||||||
|
- `/prometheus/api/v1/query_range`
|
||||||
|
- `/prometheus/api/v1/series`
|
||||||
|
- `/prometheus/api/v1/labels`
|
||||||
|
- `/prometheus/api/v1/label/<label_name>/values`
|
||||||
|
- `/prometheus/api/v1/status/active_queries`
|
||||||
|
- `/prometheus/api/v1/status/top_queries`
|
||||||
|
- `/prometheus/api/v1/status/tsdb`
|
||||||
|
- `/prometheus/api/v1/export`
|
||||||
|
- `/prometheus/api/v1/export/csv`
|
||||||
|
- `/vmui`
|
||||||
|
|
||||||
|
It is possible to explicitly specify `accountID` and `projectID` for querying multiple tenants via `vm_account_id` and `vm_project_id` labels in the query.
|
||||||
|
Alternatively, it is possible to use [`extra_filters[]` and `extra_label`](https://docs.victoriametrics.com/#prometheus-querying-api-enhancements)
|
||||||
|
query args to apply additional filters for the query.
|
||||||
|
|
||||||
|
For example, the following query fetches the total number of time series for the tenants `accountID=42` and `accountID=7, projectID=9`:
|
||||||
|
```
|
||||||
|
up{vm_account_id="7", vm_project_id="9" or vm_account_id="42"}
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to achieve the same via `extra_filters[]` and `extra_label` query args, the following query must be used:
|
||||||
|
```
|
||||||
|
curl 'http://vmselect:8481/select/multitenant/prometheus/api/v1/query' \
|
||||||
|
-d 'query=up' \
|
||||||
|
-d 'extra_filters[]={vm_account_id="7",vm_project_id="9"}' \
|
||||||
|
-d 'extra_filters[]={vm_account_id="42"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The precedence for applying filters for tenants follows this order:
|
||||||
|
|
||||||
|
1. filters tenants from `extra_label` and `extra_filters` query arguments label selectors.
|
||||||
|
These filters have the highest priority and are applied first when provided through the query arguments.
|
||||||
|
|
||||||
|
2. filters tenants from labels selectors defined at metricsQL query expression.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Note that `vm_account_id` and `vm_project_id` labels support all operators for label matching. For example:
|
||||||
|
```
|
||||||
|
up{vm_account_id!="42"} # selects all the time series except those belonging to accountID=42
|
||||||
|
up{vm_account_id=~"4.*"} # selects all the time series belonging to accountIDs starting with 4
|
||||||
|
```
|
||||||
|
|
||||||
**Security considerations:** it is recommended restricting access to `multitenant` endpoints only to trusted sources,
|
**Security considerations:** it is recommended restricting access to `multitenant` endpoints only to trusted sources,
|
||||||
since untrusted source may break per-tenant data by writing unwanted samples to arbitrary tenants.
|
since untrusted source may break per-tenant data by writing unwanted samples or get access to data of arbitrary tenants.
|
||||||
|
|
||||||
|
|
||||||
## Binaries
|
## Binaries
|
||||||
|
@ -1596,6 +1643,8 @@ Below is the output for `/path/to/vmselect -help`:
|
||||||
-search.inmemoryBufSizeBytes size
|
-search.inmemoryBufSizeBytes size
|
||||||
Size for in-memory data blocks used during processing search requests. By default, the size is automatically calculated based on available memory. Adjust this flag value if you observe that vm_tmp_blocks_max_inmemory_file_size_bytes metric constantly shows much higher values than vm_tmp_blocks_inmemory_file_size_bytes. See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6851
|
Size for in-memory data blocks used during processing search requests. By default, the size is automatically calculated based on available memory. Adjust this flag value if you observe that vm_tmp_blocks_max_inmemory_file_size_bytes metric constantly shows much higher values than vm_tmp_blocks_inmemory_file_size_bytes. See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6851
|
||||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||||
|
-search.tenantCacheExpireDuration duration
|
||||||
|
The expiry duration for list of tenants for multi-tenant queries. (default 5m0s)
|
||||||
-search.treatDotsAsIsInRegexps
|
-search.treatDotsAsIsInRegexps
|
||||||
Whether to treat dots as is in regexp label filters used in queries. For example, foo{bar=~"a.b.c"} will be automatically converted to foo{bar=~"a\\.b\\.c"}, i.e. all the dots in regexp filters will be automatically escaped in order to match only dot char instead of matching any char. Dots in ".+", ".*" and ".{n}" regexps aren't escaped. This option is DEPRECATED in favor of {__graphite__="a.*.c"} syntax for selecting metrics matching the given Graphite metrics filter
|
Whether to treat dots as is in regexp label filters used in queries. For example, foo{bar=~"a.b.c"} will be automatically converted to foo{bar=~"a\\.b\\.c"}, i.e. all the dots in regexp filters will be automatically escaped in order to match only dot char instead of matching any char. Dots in ".+", ".*" and ".{n}" regexps aren't escaped. This option is DEPRECATED in favor of {__graphite__="a.*.c"} syntax for selecting metrics matching the given Graphite metrics filter
|
||||||
-selectNode array
|
-selectNode array
|
||||||
|
|
|
@ -44,25 +44,35 @@ func NewTokenPossibleMultitenant(authToken string) (*Token, error) {
|
||||||
|
|
||||||
// Init initializes t from authToken.
|
// Init initializes t from authToken.
|
||||||
func (t *Token) Init(authToken string) error {
|
func (t *Token) Init(authToken string) error {
|
||||||
|
accountID, projectID, err := ParseToken(authToken)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse authToken %q: %w", authToken, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Set(accountID, projectID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseToken parses authToken and returns accountID and projectID from it.
|
||||||
|
func ParseToken(authToken string) (uint32, uint32, error) {
|
||||||
tmp := strings.Split(authToken, ":")
|
tmp := strings.Split(authToken, ":")
|
||||||
if len(tmp) > 2 {
|
if len(tmp) > 2 {
|
||||||
return fmt.Errorf("unexpected number of items in authToken %q; got %d; want 1 or 2", authToken, len(tmp))
|
return 0, 0, fmt.Errorf("unexpected number of items in authToken %q; got %d; want 1 or 2", authToken, len(tmp))
|
||||||
}
|
}
|
||||||
n, err := strconv.ParseUint(tmp[0], 10, 32)
|
n, err := strconv.ParseUint(tmp[0], 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot parse accountID from %q: %w", tmp[0], err)
|
return 0, 0, fmt.Errorf("cannot parse accountID from %q: %w", tmp[0], err)
|
||||||
}
|
}
|
||||||
accountID := uint32(n)
|
accountID := uint32(n)
|
||||||
projectID := uint32(0)
|
projectID := uint32(0)
|
||||||
if len(tmp) > 1 {
|
if len(tmp) > 1 {
|
||||||
n, err := strconv.ParseUint(tmp[1], 10, 32)
|
n, err := strconv.ParseUint(tmp[1], 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot parse projectID from %q: %w", tmp[1], err)
|
return 0, 0, fmt.Errorf("cannot parse projectID from %q: %w", tmp[1], err)
|
||||||
}
|
}
|
||||||
projectID = uint32(n)
|
projectID = uint32(n)
|
||||||
}
|
}
|
||||||
t.Set(accountID, projectID)
|
return accountID, projectID, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets accountID and projectID for the t.
|
// Set sets accountID and projectID for the t.
|
||||||
|
|
|
@ -277,6 +277,9 @@ type SearchQuery struct {
|
||||||
AccountID uint32
|
AccountID uint32
|
||||||
ProjectID uint32
|
ProjectID uint32
|
||||||
|
|
||||||
|
TenantTokens []TenantToken
|
||||||
|
IsMultiTenant bool
|
||||||
|
|
||||||
// The time range for searching time series
|
// The time range for searching time series
|
||||||
MinTimestamp int64
|
MinTimestamp int64
|
||||||
MaxTimestamp int64
|
MaxTimestamp int64
|
||||||
|
@ -306,12 +309,53 @@ func NewSearchQuery(accountID, projectID uint32, start, end int64, tagFilterss [
|
||||||
maxMetrics = 2e9
|
maxMetrics = 2e9
|
||||||
}
|
}
|
||||||
return &SearchQuery{
|
return &SearchQuery{
|
||||||
AccountID: accountID,
|
|
||||||
ProjectID: projectID,
|
|
||||||
MinTimestamp: start,
|
MinTimestamp: start,
|
||||||
MaxTimestamp: end,
|
MaxTimestamp: end,
|
||||||
TagFilterss: tagFilterss,
|
TagFilterss: tagFilterss,
|
||||||
MaxMetrics: maxMetrics,
|
MaxMetrics: maxMetrics,
|
||||||
|
TenantTokens: []TenantToken{
|
||||||
|
{
|
||||||
|
AccountID: accountID,
|
||||||
|
ProjectID: projectID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TenantToken represents a tenant (accountID, projectID) pair.
|
||||||
|
type TenantToken struct {
|
||||||
|
AccountID uint32
|
||||||
|
ProjectID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns string representation of t.
|
||||||
|
func (t *TenantToken) String() string {
|
||||||
|
return fmt.Sprintf("{accountID=%d, projectID=%d}", t.AccountID, t.ProjectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal appends marshaled t to dst and returns the result.
|
||||||
|
func (t *TenantToken) Marshal(dst []byte) []byte {
|
||||||
|
dst = encoding.MarshalUint32(dst, t.AccountID)
|
||||||
|
dst = encoding.MarshalUint32(dst, t.ProjectID)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultiTenantSearchQuery creates new search query for the given args.
|
||||||
|
func NewMultiTenantSearchQuery(tenants []TenantToken, start, end int64, tagFilterss [][]TagFilter, maxMetrics int) *SearchQuery {
|
||||||
|
if start < 0 {
|
||||||
|
// This is needed for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5553
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if maxMetrics <= 0 {
|
||||||
|
maxMetrics = 2e9
|
||||||
|
}
|
||||||
|
return &SearchQuery{
|
||||||
|
TenantTokens: tenants,
|
||||||
|
MinTimestamp: start,
|
||||||
|
MaxTimestamp: end,
|
||||||
|
TagFilterss: tagFilterss,
|
||||||
|
MaxMetrics: maxMetrics,
|
||||||
|
IsMultiTenant: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,7 +456,15 @@ func (sq *SearchQuery) String() string {
|
||||||
}
|
}
|
||||||
start := TimestampToHumanReadableFormat(sq.MinTimestamp)
|
start := TimestampToHumanReadableFormat(sq.MinTimestamp)
|
||||||
end := TimestampToHumanReadableFormat(sq.MaxTimestamp)
|
end := TimestampToHumanReadableFormat(sq.MaxTimestamp)
|
||||||
return fmt.Sprintf("accountID=%d, projectID=%d, filters=%s, timeRange=[%s..%s]", sq.AccountID, sq.ProjectID, a, start, end)
|
if !sq.IsMultiTenant {
|
||||||
|
return fmt.Sprintf("accountID=%d, projectID=%d, filters=%s, timeRange=[%s..%s]", sq.AccountID, sq.ProjectID, a, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
tts := make([]string, len(sq.TenantTokens))
|
||||||
|
for i, tt := range sq.TenantTokens {
|
||||||
|
tts[i] = tt.String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("tenants=[%s], filters=%s, timeRange=[%s..%s]", strings.Join(tts, ","), a, start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tagFiltersToString(tfs []TagFilter) string {
|
func tagFiltersToString(tfs []TagFilter) string {
|
||||||
|
@ -423,10 +475,9 @@ func tagFiltersToString(tfs []TagFilter) string {
|
||||||
return "{" + strings.Join(a, ",") + "}"
|
return "{" + strings.Join(a, ",") + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal appends marshaled sq to dst and returns the result.
|
// MarshaWithoutTenant appends marshaled sq without AccountID/ProjectID to dst and returns the result.
|
||||||
func (sq *SearchQuery) Marshal(dst []byte) []byte {
|
// It is expected that TenantToken is already marshaled to dst.
|
||||||
dst = encoding.MarshalUint32(dst, sq.AccountID)
|
func (sq *SearchQuery) MarshaWithoutTenant(dst []byte) []byte {
|
||||||
dst = encoding.MarshalUint32(dst, sq.ProjectID)
|
|
||||||
dst = encoding.MarshalVarInt64(dst, sq.MinTimestamp)
|
dst = encoding.MarshalVarInt64(dst, sq.MinTimestamp)
|
||||||
dst = encoding.MarshalVarInt64(dst, sq.MaxTimestamp)
|
dst = encoding.MarshalVarInt64(dst, sq.MaxTimestamp)
|
||||||
dst = encoding.MarshalVarUint64(dst, uint64(len(sq.TagFilterss)))
|
dst = encoding.MarshalVarUint64(dst, uint64(len(sq.TagFilterss)))
|
||||||
|
|
|
@ -29,7 +29,12 @@ func TestSearchQueryMarshalUnmarshal(t *testing.T) {
|
||||||
// Skip nil sq1.
|
// Skip nil sq1.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
buf = sq1.Marshal(buf[:0])
|
tt := TenantToken{
|
||||||
|
AccountID: sq1.AccountID,
|
||||||
|
ProjectID: sq1.ProjectID,
|
||||||
|
}
|
||||||
|
buf = tt.Marshal(buf[:0])
|
||||||
|
buf = sq1.MarshaWithoutTenant(buf)
|
||||||
|
|
||||||
tail, err := sq2.Unmarshal(buf)
|
tail, err := sq2.Unmarshal(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/metrics"
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||||
"github.com/VictoriaMetrics/metrics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TenantID defines metric tenant.
|
// TenantID defines metric tenant.
|
||||||
|
@ -21,6 +22,8 @@ type CounterMap struct {
|
||||||
|
|
||||||
// do not use atomic.Pointer, since the stored map there is already a pointer type.
|
// do not use atomic.Pointer, since the stored map there is already a pointer type.
|
||||||
m atomic.Value
|
m atomic.Value
|
||||||
|
// mt holds value for multi-tenant metrics.
|
||||||
|
mt atomic.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCounterMap creates new CounterMap for the given metric.
|
// NewCounterMap creates new CounterMap for the given metric.
|
||||||
|
@ -34,11 +37,15 @@ func NewCounterMap(metric string) *CounterMap {
|
||||||
|
|
||||||
// Get returns counter for the given at
|
// Get returns counter for the given at
|
||||||
func (cm *CounterMap) Get(at *auth.Token) *metrics.Counter {
|
func (cm *CounterMap) Get(at *auth.Token) *metrics.Counter {
|
||||||
|
if at == nil {
|
||||||
|
return cm.GetByTenant(nil)
|
||||||
|
}
|
||||||
|
|
||||||
key := TenantID{
|
key := TenantID{
|
||||||
AccountID: at.AccountID,
|
AccountID: at.AccountID,
|
||||||
ProjectID: at.ProjectID,
|
ProjectID: at.ProjectID,
|
||||||
}
|
}
|
||||||
return cm.GetByTenant(key)
|
return cm.GetByTenant(&key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultiAdd adds multiple values grouped by auth.Token
|
// MultiAdd adds multiple values grouped by auth.Token
|
||||||
|
@ -49,9 +56,19 @@ func (cm *CounterMap) MultiAdd(perTenantValues map[auth.Token]int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByTenant returns counter for the given key.
|
// GetByTenant returns counter for the given key.
|
||||||
func (cm *CounterMap) GetByTenant(key TenantID) *metrics.Counter {
|
func (cm *CounterMap) GetByTenant(key *TenantID) *metrics.Counter {
|
||||||
|
if key == nil {
|
||||||
|
mtm := cm.mt.Load()
|
||||||
|
if mtm == nil {
|
||||||
|
mtc := metrics.GetOrCreateCounter(createMetricNameMultitenant(cm.metric))
|
||||||
|
cm.mt.Store(mtc)
|
||||||
|
return mtc
|
||||||
|
}
|
||||||
|
return mtm.(*metrics.Counter)
|
||||||
|
}
|
||||||
|
|
||||||
m := cm.m.Load().(map[TenantID]*metrics.Counter)
|
m := cm.m.Load().(map[TenantID]*metrics.Counter)
|
||||||
if c := m[key]; c != nil {
|
if c := m[*key]; c != nil {
|
||||||
// Fast path - the counter for k already exists.
|
// Fast path - the counter for k already exists.
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
@ -61,9 +78,9 @@ func (cm *CounterMap) GetByTenant(key TenantID) *metrics.Counter {
|
||||||
for k, c := range m {
|
for k, c := range m {
|
||||||
newM[k] = c
|
newM[k] = c
|
||||||
}
|
}
|
||||||
metricName := createMetricName(cm.metric, key)
|
metricName := createMetricName(cm.metric, *key)
|
||||||
c := metrics.GetOrCreateCounter(metricName)
|
c := metrics.GetOrCreateCounter(metricName)
|
||||||
newM[key] = c
|
newM[*key] = c
|
||||||
cm.m.Store(newM)
|
cm.m.Store(newM)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
@ -79,3 +96,15 @@ func createMetricName(metric string, key TenantID) string {
|
||||||
// Metric with labels.
|
// Metric with labels.
|
||||||
return fmt.Sprintf(`%s,accountID="%d",projectID="%d"}`, metric[:len(metric)-1], key.AccountID, key.ProjectID)
|
return fmt.Sprintf(`%s,accountID="%d",projectID="%d"}`, metric[:len(metric)-1], key.AccountID, key.ProjectID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createMetricNameMultitenant(metric string) string {
|
||||||
|
if len(metric) == 0 {
|
||||||
|
logger.Panicf("BUG: metric cannot be empty")
|
||||||
|
}
|
||||||
|
if metric[len(metric)-1] != '}' {
|
||||||
|
// Metric without labels.
|
||||||
|
return fmt.Sprintf(`%s{accountID="multitenant",projectID="multitenant"}`, metric)
|
||||||
|
}
|
||||||
|
// Metric with labels.
|
||||||
|
return fmt.Sprintf(`%s,accountID="multitenant",projectID="multitenant"}`, metric[:len(metric)-1])
|
||||||
|
}
|
||||||
|
|
|
@ -17,3 +17,15 @@ func AddJitterToDuration(d time.Duration) time.Duration {
|
||||||
p := float64(fastrand.Uint32()) / (1 << 32)
|
p := float64(fastrand.Uint32()) / (1 << 32)
|
||||||
return d + time.Duration(p*float64(dv))
|
return d + time.Duration(p*float64(dv))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartOfDay returns the start of the day for the given timestamp.
|
||||||
|
// Timestamp is in milliseconds.
|
||||||
|
func StartOfDay(ts int64) int64 {
|
||||||
|
return ts - (ts % 86400000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndOfDay returns the end of the day for the given timestamp.
|
||||||
|
// Timestamp is in milliseconds.
|
||||||
|
func EndOfDay(ts int64) int64 {
|
||||||
|
return StartOfDay(ts) + 86400000 - 1
|
||||||
|
}
|
||||||
|
|
|
@ -25,3 +25,45 @@ func TestAddJitterToDuration(t *testing.T) {
|
||||||
f(time.Hour)
|
f(time.Hour)
|
||||||
f(24 * time.Hour)
|
f(24 * time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStartOfDay(t *testing.T) {
|
||||||
|
f := func(original, expected time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
result := StartOfDay(original.UnixMilli())
|
||||||
|
if result != expected.UnixMilli() {
|
||||||
|
t.Fatalf("unexpected result; got %d; want %d", result, expected.UnixMilli())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f(
|
||||||
|
time.Date(2021, 1, 1, 1, 1, 1, 0, time.UTC),
|
||||||
|
time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
f(
|
||||||
|
time.Date(2021, 1, 1, 23, 59, 59, 999999999, time.UTC),
|
||||||
|
time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndOfDay(t *testing.T) {
|
||||||
|
f := func(original, expected time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
result := EndOfDay(original.UnixMilli())
|
||||||
|
if result != expected.UnixMilli() {
|
||||||
|
t.Fatalf("unexpected result; got %d; want %d", result, expected.UnixMilli())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f(
|
||||||
|
time.Date(2021, 1, 1, 1, 1, 1, 0, time.UTC),
|
||||||
|
time.Date(2021, 1, 1, 23, 59, 59, 999999999, time.UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
f(
|
||||||
|
time.Date(2021, 1, 1, 23, 59, 59, 999999999, time.UTC),
|
||||||
|
time.Date(2021, 1, 1, 23, 59, 59, 999999999, time.UTC),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue