Files
VictoriaMetrics/app/vmselect/graphite/eval.go
2026-02-18 14:28:27 +01:00

241 lines
7.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package graphite
import (
"flag"
"fmt"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphiteql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
)
var maxGraphiteSeries = flag.Int("search.maxGraphiteSeries", 300e3, "The maximum number of time series, which can be scanned during queries to Graphite Render API. "+
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#render-api")
var maxGraphitePathExpressionLen = flag.Int("search.maxGraphitePathExpressionLen", 1024, "The maximum length of pathExpression field in Graphite series. "+
"Longer expressions are truncated to prevent memory exhaustion on complex nested queries. Set to 0 to disable truncation.")
type evalConfig struct {
startTime int64
endTime int64
storageStep int64
deadline searchutil.Deadline
currentTime time.Time
// xFilesFactor is used for determining when consolidateFunc must be applied.
//
// 0 means that consolidateFunc should be applied if at least a single non-NaN data point exists on the given step.
// 1 means that consolidateFunc should be applied if all the data points are non-NaN on the given step.
xFilesFactor float64
// Enforced tag filters
etfs [][]storage.TagFilter
// originalQuery contains the original query - used for debug logging.
originalQuery string
}
func (ec *evalConfig) pointsLen(step int64) int {
return int((ec.endTime - ec.startTime) / step)
}
func (ec *evalConfig) newTimestamps(step int64) []int64 {
pointsLen := ec.pointsLen(step)
timestamps := make([]int64, pointsLen)
ts := ec.startTime
for i := range pointsLen {
timestamps[i] = ts
ts += step
}
return timestamps
}
// safePathExpression creates a pathExpression string from the given expression,
// truncating it if it exceeds the maximum allowed length to prevent memory exhaustion.
func safePathExpression(expr graphiteql.Expr) string {
if expr == nil {
return ""
}
pathExpr := string(expr.AppendString(nil))
maxLen := *maxGraphitePathExpressionLen
if maxLen > 0 && len(pathExpr) > maxLen {
return pathExpr[:maxLen] + "..."
}
return pathExpr
}
type series struct {
Name string
Tags map[string]string
Timestamps []int64
Values []float64
// holds current path expression like graphite does.
pathExpression string
expr graphiteql.Expr
// consolidateFunc is applied to raw samples in order to generate data points aligned to the given step.
// see series.consolidate() function for details.
consolidateFunc aggrFunc
// xFilesFactor is used for determining when consolidateFunc must be applied.
//
// 0 means that consolidateFunc should be applied if at least a single non-NaN data point exists on the given step.
// 1 means that consolidateFunc should be applied if all the data points are non-NaN on the given step.
xFilesFactor float64
step int64
}
func (s *series) consolidate(ec *evalConfig, step int64) {
aggrFunc := s.consolidateFunc
if aggrFunc == nil {
aggrFunc = aggrAvg
}
xFilesFactor := s.xFilesFactor
if s.xFilesFactor <= 0 {
xFilesFactor = ec.xFilesFactor
}
s.summarize(aggrFunc, ec.startTime, ec.endTime, step, xFilesFactor)
}
func (s *series) summarize(aggrFunc aggrFunc, startTime, endTime, step int64, xFilesFactor float64) {
pointsLen := int((endTime - startTime) / step)
timestamps := s.Timestamps
values := s.Values
dstTimestamps := make([]int64, 0, pointsLen)
dstValues := make([]float64, 0, pointsLen)
ts := startTime
i := 0
for len(dstTimestamps) < pointsLen {
tsEnd := ts + step
j := i
for j < len(timestamps) && timestamps[j] < tsEnd {
j++
}
if i == j && i > 0 && ts-timestamps[i-1] <= 2000 {
// The current [ts ... tsEnd) interval has no samples,
// but the last sample on the previous interval [ts - step ... ts)
// is closer than 2 seconds to the current interval.
// Let's consider that this sample belongs to the current interval,
// since such discrepancy could appear because of small jitter in samples' ingestion.
i--
}
v := aggrFunc.apply(xFilesFactor, values[i:j])
dstTimestamps = append(dstTimestamps, ts)
dstValues = append(dstValues, v)
ts = tsEnd
i = j
}
// Do not reuse s.Timestamps and s.Values, since they can be too big
s.Timestamps = dstTimestamps
s.Values = dstValues
s.step = step
}
func execExpr(ec *evalConfig, query string) (nextSeriesFunc, error) {
// Validate query length to prevent memory exhaustion
maxLen := searchutil.GetMaxQueryLen()
if len(query) > maxLen {
return nil, fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
}
expr, err := graphiteql.Parse(query)
if err != nil {
return nil, fmt.Errorf("cannot parse %q: %w", query, err)
}
return evalExpr(ec, expr)
}
func evalExpr(ec *evalConfig, expr graphiteql.Expr) (nextSeriesFunc, error) {
switch t := expr.(type) {
case *graphiteql.MetricExpr:
return evalMetricExpr(ec, t)
case *graphiteql.FuncExpr:
return evalFuncExpr(ec, t)
default:
return nil, fmt.Errorf("unexpected expression type %T; want graphiteql.MetricExpr or graphiteql.FuncExpr; expr: %q", t, t.AppendString(nil))
}
}
func evalMetricExpr(ec *evalConfig, me *graphiteql.MetricExpr) (nextSeriesFunc, error) {
tfs := []storage.TagFilter{{
Key: []byte("__graphite__"),
Value: []byte(me.Query),
}}
tfss := joinTagFilterss(tfs, ec.etfs)
sq := storage.NewSearchQuery(ec.startTime, ec.endTime, tfss, *maxGraphiteSeries)
return newNextSeriesForSearchQuery(ec, sq, me)
}
func newNextSeriesForSearchQuery(ec *evalConfig, sq *storage.SearchQuery, expr graphiteql.Expr) (nextSeriesFunc, error) {
rss, err := netstorage.ProcessSearchQuery(nil, sq, ec.deadline)
if err != nil {
return nil, fmt.Errorf("cannot fetch data for %q: %w", sq, err)
}
seriesCh := make(chan *series, cgroup.AvailableCPUs())
errCh := make(chan error, 1)
go func() {
err := rss.RunParallel(nil, func(rs *netstorage.Result, _ uint) error {
nameWithTags := getCanonicalPath(&rs.MetricName)
tags := unmarshalTags(nameWithTags)
s := &series{
Name: tags["name"],
Tags: tags,
Timestamps: append([]int64{}, rs.Timestamps...),
Values: append([]float64{}, rs.Values...),
expr: expr,
pathExpression: safePathExpression(expr),
}
s.summarize(aggrAvg, ec.startTime, ec.endTime, ec.storageStep, 0)
// A negative or zero duration will cause timer.C to return immediately
remainingTimeout := ec.deadline.Deadline() - fasttime.UnixTimestamp()
t := timerpool.Get(time.Duration(remainingTimeout) * time.Second)
defer timerpool.Put(t)
select {
case seriesCh <- s:
case <-t.C:
logger.Errorf("reached timeout when processing the %s (full query: %s), it can be due to the amount of storageNodes configured in vmselect is more than vmselects available CPU count "+
"or vmselect is heavy loaded. Consider adding resources or increasing `-search.maxQueryDuration` or `timeout` parameter in the query.",
expr.AppendString(nil), ec.originalQuery)
}
return nil
})
close(seriesCh)
errCh <- err
}()
f := func() (*series, error) {
s := <-seriesCh
if s != nil {
return s, nil
}
err := <-errCh
return nil, err
}
return f, nil
}
func evalFuncExpr(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, error) {
// Do not lowercase the fe.FuncName, since Graphite function names are case-sensitive.
tf := transformFuncs[fe.FuncName]
if tf == nil {
return nil, fmt.Errorf("unknown function %q", fe.FuncName)
}
nextSeries, err := tf(ec, fe)
if err != nil {
return nil, fmt.Errorf("cannot evaluate %s: %w", fe.AppendString(nil), err)
}
return nextSeries, nil
}