mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 05:15:57 +08:00
feat: add querier interface and initial implementation (#2782)
This commit is contained in:
parent
7415de4751
commit
04a9de1e32
1
Makefile
1
Makefile
@ -140,5 +140,6 @@ test:
|
|||||||
go test ./pkg/query-service/app/metrics/...
|
go test ./pkg/query-service/app/metrics/...
|
||||||
go test ./pkg/query-service/cache/...
|
go test ./pkg/query-service/cache/...
|
||||||
go test ./pkg/query-service/app/...
|
go test ./pkg/query-service/app/...
|
||||||
|
go test ./pkg/query-service/app/querier/...
|
||||||
go test ./pkg/query-service/converter/...
|
go test ./pkg/query-service/converter/...
|
||||||
go test ./pkg/query-service/formatter/...
|
go test ./pkg/query-service/formatter/...
|
||||||
|
@ -3,8 +3,10 @@ package v3
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||||
)
|
)
|
||||||
@ -403,3 +405,12 @@ func PrepareMetricQuery(start, end int64, queryType v3.QueryType, panelType v3.P
|
|||||||
}
|
}
|
||||||
return query, err
|
return query, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BuildPromQuery(promQuery *v3.PromQuery, step, start, end int64) *model.QueryRangeParams {
|
||||||
|
return &model.QueryRangeParams{
|
||||||
|
Query: promQuery.Query,
|
||||||
|
Start: time.UnixMilli(start),
|
||||||
|
End: time.UnixMilli(end),
|
||||||
|
Step: time.Duration(step * int64(time.Second)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
426
pkg/query-service/app/querier/querier.go
Normal file
426
pkg/query-service/app/querier/querier.go
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
package querier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
logsV3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
|
||||||
|
metricsV3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
|
||||||
|
tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3"
|
||||||
|
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/cache/status"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type missInterval struct {
|
||||||
|
start, end int64 // in milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
type querier struct {
|
||||||
|
cache cache.Cache
|
||||||
|
reader interfaces.Reader
|
||||||
|
keyGenerator cache.KeyGenerator
|
||||||
|
|
||||||
|
fluxInterval time.Duration
|
||||||
|
|
||||||
|
// used for testing
|
||||||
|
// TODO(srikanthccv): remove this once we have a proper mock
|
||||||
|
testingMode bool
|
||||||
|
queriesExecuted []string
|
||||||
|
returnedSeries []*v3.Series
|
||||||
|
returnedErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuerierOptions struct {
|
||||||
|
Reader interfaces.Reader
|
||||||
|
Cache cache.Cache
|
||||||
|
KeyGenerator cache.KeyGenerator
|
||||||
|
FluxInterval time.Duration
|
||||||
|
|
||||||
|
// used for testing
|
||||||
|
TestingMode bool
|
||||||
|
ReturnedSeries []*v3.Series
|
||||||
|
ReturnedErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQuerier(opts QuerierOptions) interfaces.Querier {
|
||||||
|
return &querier{
|
||||||
|
cache: opts.Cache,
|
||||||
|
reader: opts.Reader,
|
||||||
|
keyGenerator: opts.KeyGenerator,
|
||||||
|
fluxInterval: opts.FluxInterval,
|
||||||
|
|
||||||
|
testingMode: opts.TestingMode,
|
||||||
|
returnedSeries: opts.ReturnedSeries,
|
||||||
|
returnedErr: opts.ReturnedErr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *querier) execClickHouseQuery(ctx context.Context, query string) ([]*v3.Series, error) {
|
||||||
|
q.queriesExecuted = append(q.queriesExecuted, query)
|
||||||
|
if q.testingMode && q.reader == nil {
|
||||||
|
return q.returnedSeries, q.returnedErr
|
||||||
|
}
|
||||||
|
return q.reader.GetTimeSeriesResultV3(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *querier) execPromQuery(ctx context.Context, params *model.QueryRangeParams) ([]*v3.Series, error) {
|
||||||
|
q.queriesExecuted = append(q.queriesExecuted, params.Query)
|
||||||
|
if q.testingMode && q.reader == nil {
|
||||||
|
return q.returnedSeries, q.returnedErr
|
||||||
|
}
|
||||||
|
promResult, _, err := q.reader.GetQueryRangeResult(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
matrix, promErr := promResult.Matrix()
|
||||||
|
if promErr != nil {
|
||||||
|
return nil, promErr
|
||||||
|
}
|
||||||
|
var seriesList []*v3.Series
|
||||||
|
for _, v := range matrix {
|
||||||
|
var s v3.Series
|
||||||
|
s.Labels = v.Metric.Copy().Map()
|
||||||
|
for idx := range v.Points {
|
||||||
|
p := v.Points[idx]
|
||||||
|
s.Points = append(s.Points, v3.Point{Timestamp: p.T, Value: p.V})
|
||||||
|
}
|
||||||
|
seriesList = append(seriesList, &s)
|
||||||
|
}
|
||||||
|
return seriesList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findMissingTimeRanges finds the missing time ranges in the seriesList
|
||||||
|
// and returns a list of miss structs, It takes the fluxInterval into
|
||||||
|
// account to find the missing time ranges.
|
||||||
|
//
|
||||||
|
// The [End - fluxInterval, End] is always added to the list of misses, because
|
||||||
|
// the data might still be in flux and not yet available in the database.
|
||||||
|
func findMissingTimeRanges(start, end int64, seriesList []*v3.Series, fluxInterval time.Duration) (misses []missInterval) {
|
||||||
|
var cachedStart, cachedEnd int64
|
||||||
|
for idx := range seriesList {
|
||||||
|
series := seriesList[idx]
|
||||||
|
for pointIdx := range series.Points {
|
||||||
|
point := series.Points[pointIdx]
|
||||||
|
if cachedStart == 0 || point.Timestamp < cachedStart {
|
||||||
|
cachedStart = point.Timestamp
|
||||||
|
}
|
||||||
|
if cachedEnd == 0 || point.Timestamp > cachedEnd {
|
||||||
|
cachedEnd = point.Timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude the flux interval from the cached end time
|
||||||
|
cachedEnd = int64(
|
||||||
|
math.Min(
|
||||||
|
float64(cachedEnd),
|
||||||
|
float64(time.Now().UnixMilli()-fluxInterval.Milliseconds()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// There are five cases to consider
|
||||||
|
// 1. Cached time range is a subset of the requested time range
|
||||||
|
// 2. Cached time range is a superset of the requested time range
|
||||||
|
// 3. Cached time range is a left overlap of the requested time range
|
||||||
|
// 4. Cached time range is a right overlap of the requested time range
|
||||||
|
// 5. Cached time range is a disjoint of the requested time range
|
||||||
|
if cachedStart >= start && cachedEnd <= end {
|
||||||
|
// Case 1: Cached time range is a subset of the requested time range
|
||||||
|
// Add misses for the left and right sides of the cached time range
|
||||||
|
misses = append(misses, missInterval{start: start, end: cachedStart - 1})
|
||||||
|
misses = append(misses, missInterval{start: cachedEnd + 1, end: end})
|
||||||
|
} else if cachedStart <= start && cachedEnd >= end {
|
||||||
|
// Case 2: Cached time range is a superset of the requested time range
|
||||||
|
// No misses
|
||||||
|
} else if cachedStart <= start && cachedEnd >= start {
|
||||||
|
// Case 3: Cached time range is a left overlap of the requested time range
|
||||||
|
// Add a miss for the left side of the cached time range
|
||||||
|
misses = append(misses, missInterval{start: cachedEnd + 1, end: end})
|
||||||
|
} else if cachedStart <= end && cachedEnd >= end {
|
||||||
|
// Case 4: Cached time range is a right overlap of the requested time range
|
||||||
|
// Add a miss for the right side of the cached time range
|
||||||
|
misses = append(misses, missInterval{start: start, end: cachedStart - 1})
|
||||||
|
} else {
|
||||||
|
// Case 5: Cached time range is a disjoint of the requested time range
|
||||||
|
// Add a miss for the entire requested time range
|
||||||
|
misses = append(misses, missInterval{start: start, end: end})
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the struts with start > end
|
||||||
|
var validMisses []missInterval
|
||||||
|
for idx := range misses {
|
||||||
|
miss := misses[idx]
|
||||||
|
if miss.start <= miss.end {
|
||||||
|
validMisses = append(validMisses, miss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validMisses
|
||||||
|
}
|
||||||
|
|
||||||
|
// findMissingTimeRanges finds the missing time ranges in the cached data
|
||||||
|
// and returns them as a list of misses
|
||||||
|
func (q *querier) findMissingTimeRanges(start, end, step int64, cachedData []byte) (misses []missInterval) {
|
||||||
|
var cachedSeriesList []*v3.Series
|
||||||
|
if err := json.Unmarshal(cachedData, &cachedSeriesList); err != nil {
|
||||||
|
// In case of error, we return the entire range as a miss
|
||||||
|
return []missInterval{{start: start, end: end}}
|
||||||
|
}
|
||||||
|
return findMissingTimeRanges(start, end, cachedSeriesList, q.fluxInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func labelsToString(labels map[string]string) string {
|
||||||
|
type label struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
var labelsList []label
|
||||||
|
for k, v := range labels {
|
||||||
|
labelsList = append(labelsList, label{Key: k, Value: v})
|
||||||
|
}
|
||||||
|
sort.Slice(labelsList, func(i, j int) bool {
|
||||||
|
return labelsList[i].Key < labelsList[j].Key
|
||||||
|
})
|
||||||
|
labelKVs := make([]string, len(labelsList))
|
||||||
|
for idx := range labelsList {
|
||||||
|
labelKVs[idx] = labelsList[idx].Key + "=" + labelsList[idx].Value
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{%s}", strings.Join(labelKVs, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeSerieses(cachedSeries, missedSeries []*v3.Series) []*v3.Series {
|
||||||
|
// Merge the missed series with the cached series by timestamp
|
||||||
|
mergedSeries := make([]*v3.Series, 0)
|
||||||
|
seriesesByLabels := make(map[string]*v3.Series)
|
||||||
|
for idx := range cachedSeries {
|
||||||
|
series := cachedSeries[idx]
|
||||||
|
seriesesByLabels[labelsToString(series.Labels)] = series
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range missedSeries {
|
||||||
|
series := missedSeries[idx]
|
||||||
|
if _, ok := seriesesByLabels[labelsToString(series.Labels)]; !ok {
|
||||||
|
seriesesByLabels[labelsToString(series.Labels)] = series
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seriesesByLabels[labelsToString(series.Labels)].Points = append(seriesesByLabels[labelsToString(series.Labels)].Points, series.Points...)
|
||||||
|
}
|
||||||
|
// Sort the points in each series by timestamp
|
||||||
|
for idx := range seriesesByLabels {
|
||||||
|
series := seriesesByLabels[idx]
|
||||||
|
series.SortPoints()
|
||||||
|
mergedSeries = append(mergedSeries, series)
|
||||||
|
}
|
||||||
|
return mergedSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *querier) runBuilderQueries(ctx context.Context, params *v3.QueryRangeParamsV3, fields map[string]v3.AttributeKey, keys map[string]v3.AttributeKey) ([]*v3.Series, error, map[string]string) {
|
||||||
|
|
||||||
|
cacheKeys := q.keyGenerator.GenerateKeys(params)
|
||||||
|
|
||||||
|
seriesList := make([]*v3.Series, 0)
|
||||||
|
errQueriesByName := make(map[string]string)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for queryName, builderQuery := range params.CompositeQuery.BuilderQueries {
|
||||||
|
|
||||||
|
// TODO: add support for logs and traces
|
||||||
|
if builderQuery.DataSource == v3.DataSourceLogs {
|
||||||
|
query, err := logsV3.PrepareLogsQuery(params.Start, params.End, params.CompositeQuery.QueryType, params.CompositeQuery.PanelType, builderQuery, fields)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
series, err := q.execClickHouseQuery(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seriesList = append(seriesList, series...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if builderQuery.DataSource == v3.DataSourceTraces {
|
||||||
|
query, err := tracesV3.PrepareTracesQuery(params.Start, params.End, params.CompositeQuery.QueryType, params.CompositeQuery.PanelType, builderQuery, keys)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
series, err := q.execClickHouseQuery(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seriesList = append(seriesList, series...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := cacheKeys[queryName]
|
||||||
|
var cachedData []byte
|
||||||
|
if !params.NoCache {
|
||||||
|
var retrieveStatus status.RetrieveStatus
|
||||||
|
cachedData, retrieveStatus, err = q.cache.Retrieve(cacheKey, true)
|
||||||
|
zap.L().Debug("cache retrieve status", zap.String("status", retrieveStatus.String()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
misses := q.findMissingTimeRanges(params.Start, params.End, params.Step, cachedData)
|
||||||
|
missedSeries := make([]*v3.Series, 0)
|
||||||
|
cachedSeries := make([]*v3.Series, 0)
|
||||||
|
for _, miss := range misses {
|
||||||
|
query, err := metricsV3.PrepareMetricQuery(
|
||||||
|
miss.start,
|
||||||
|
miss.end,
|
||||||
|
params.CompositeQuery.QueryType,
|
||||||
|
params.CompositeQuery.PanelType,
|
||||||
|
builderQuery,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
series, err := q.execClickHouseQuery(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missedSeries = append(missedSeries, series...)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(cachedData, &cachedSeries); err != nil && cachedData != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mergedSeries := mergeSerieses(cachedSeries, missedSeries)
|
||||||
|
|
||||||
|
seriesList = append(seriesList, mergedSeries...)
|
||||||
|
// Cache the seriesList for future queries
|
||||||
|
if len(missedSeries) > 0 {
|
||||||
|
mergedSeriesData, err := json.Marshal(mergedSeries)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = q.cache.Store(cacheKey, mergedSeriesData, time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errQueriesByName) > 0 {
|
||||||
|
err = fmt.Errorf("error in builder queries")
|
||||||
|
}
|
||||||
|
return seriesList, err, errQueriesByName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParamsV3) ([]*v3.Series, error, map[string]string) {
|
||||||
|
seriesList := make([]*v3.Series, 0)
|
||||||
|
errQueriesByName := make(map[string]string)
|
||||||
|
var err error
|
||||||
|
for queryName, promQuery := range params.CompositeQuery.PromQueries {
|
||||||
|
cacheKey := q.keyGenerator.GenerateKeys(params)[queryName]
|
||||||
|
var cachedData []byte
|
||||||
|
var retrieveStatus status.RetrieveStatus
|
||||||
|
if !params.NoCache {
|
||||||
|
cachedData, retrieveStatus, err = q.cache.Retrieve(cacheKey, true)
|
||||||
|
zap.L().Debug("cache retrieve status", zap.String("status", retrieveStatus.String()))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
misses := q.findMissingTimeRanges(params.Start, params.End, params.Step, cachedData)
|
||||||
|
missedSeries := make([]*v3.Series, 0)
|
||||||
|
cachedSeries := make([]*v3.Series, 0)
|
||||||
|
for _, miss := range misses {
|
||||||
|
query := metricsV3.BuildPromQuery(
|
||||||
|
promQuery,
|
||||||
|
params.Step,
|
||||||
|
miss.start,
|
||||||
|
miss.end,
|
||||||
|
)
|
||||||
|
series, err := q.execPromQuery(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missedSeries = append(missedSeries, series...)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(cachedData, &cachedSeries); err != nil && cachedData != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mergedSeries := mergeSerieses(cachedSeries, missedSeries)
|
||||||
|
|
||||||
|
seriesList = append(seriesList, mergedSeries...)
|
||||||
|
// Cache the seriesList for future queries
|
||||||
|
if len(missedSeries) > 0 {
|
||||||
|
mergedSeriesData, err := json.Marshal(mergedSeries)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = q.cache.Store(cacheKey, mergedSeriesData, time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errQueriesByName) > 0 {
|
||||||
|
err = fmt.Errorf("error in prom queries")
|
||||||
|
}
|
||||||
|
return seriesList, err, errQueriesByName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *querier) runClickHouseQueries(ctx context.Context, params *v3.QueryRangeParamsV3) ([]*v3.Series, error, map[string]string) {
|
||||||
|
seriesList := make([]*v3.Series, 0)
|
||||||
|
errQueriesByName := make(map[string]string)
|
||||||
|
var err error
|
||||||
|
for queryName, clickHouseQuery := range params.CompositeQuery.ClickHouseQueries {
|
||||||
|
series, err := q.execClickHouseQuery(ctx, clickHouseQuery.Query)
|
||||||
|
if err != nil {
|
||||||
|
errQueriesByName[queryName] = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seriesList = append(seriesList, series...)
|
||||||
|
}
|
||||||
|
if len(errQueriesByName) > 0 {
|
||||||
|
err = fmt.Errorf("error in clickhouse queries")
|
||||||
|
}
|
||||||
|
return seriesList, err, errQueriesByName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *querier) QueryRange(ctx context.Context, params *v3.QueryRangeParamsV3, fields map[string]v3.AttributeKey, keys map[string]v3.AttributeKey) ([]*v3.Series, error, map[string]string) {
|
||||||
|
var seriesList []*v3.Series
|
||||||
|
var err error
|
||||||
|
var errQueriesByName map[string]string
|
||||||
|
if params.CompositeQuery != nil {
|
||||||
|
switch params.CompositeQuery.QueryType {
|
||||||
|
case v3.QueryTypeBuilder:
|
||||||
|
seriesList, err, errQueriesByName = q.runBuilderQueries(ctx, params, fields, keys)
|
||||||
|
case v3.QueryTypePromQL:
|
||||||
|
seriesList, err, errQueriesByName = q.runPromQueries(ctx, params)
|
||||||
|
case v3.QueryTypeClickHouseSQL:
|
||||||
|
seriesList, err, errQueriesByName = q.runClickHouseQueries(ctx, params)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("invalid query type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seriesList, err, errQueriesByName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *querier) QueriesExecuted() []string {
|
||||||
|
return q.queriesExecuted
|
||||||
|
}
|
507
pkg/query-service/app/querier/querier_test.go
Normal file
507
pkg/query-service/app/querier/querier_test.go
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
package querier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/cache/inmemory"
|
||||||
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindMissingTimeRangesZeroFreshNess(t *testing.T) {
|
||||||
|
// There are five scenarios:
|
||||||
|
// 1. Cached time range is a subset of the requested time range
|
||||||
|
// 2. Cached time range is a superset of the requested time range
|
||||||
|
// 3. Cached time range is a left overlap of the requested time range
|
||||||
|
// 4. Cached time range is a right overlap of the requested time range
|
||||||
|
// 5. Cached time range is a disjoint of the requested time range
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
requestedStart int64 // in milliseconds
|
||||||
|
requestedEnd int64 // in milliseconds
|
||||||
|
cachedSeries []*v3.Series
|
||||||
|
expectedMiss []missInterval
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cached time range is a subset of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 60*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 120*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMiss: []missInterval{
|
||||||
|
{
|
||||||
|
start: 1675115596722,
|
||||||
|
end: 1675115596722 + 60*60*1000 - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: 1675115596722 + 120*60*1000 + 1,
|
||||||
|
end: 1675115596722 + 180*60*1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cached time range is a superset of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 60*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 120*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 180*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMiss: []missInterval{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cached time range is a left overlap of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 60*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 120*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMiss: []missInterval{
|
||||||
|
{
|
||||||
|
start: 1675115596722 + 120*60*1000 + 1,
|
||||||
|
end: 1675115596722 + 180*60*1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cached time range is a right overlap of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 60*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 120*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 180*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMiss: []missInterval{
|
||||||
|
{
|
||||||
|
start: 1675115596722,
|
||||||
|
end: 1675115596722 + 60*60*1000 - 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cached time range is a disjoint of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 240*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 300*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 360*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMiss: []missInterval{
|
||||||
|
{
|
||||||
|
start: 1675115596722,
|
||||||
|
end: 1675115596722 + 180*60*1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
misses := findMissingTimeRanges(tc.requestedStart, tc.requestedEnd, tc.cachedSeries, 0*time.Minute)
|
||||||
|
if len(misses) != len(tc.expectedMiss) {
|
||||||
|
t.Errorf("expected %d misses, got %d", len(tc.expectedMiss), len(misses))
|
||||||
|
}
|
||||||
|
for i, miss := range misses {
|
||||||
|
if miss.start != tc.expectedMiss[i].start {
|
||||||
|
t.Errorf("expected start %d, got %d", tc.expectedMiss[i].start, miss.start)
|
||||||
|
}
|
||||||
|
if miss.end != tc.expectedMiss[i].end {
|
||||||
|
t.Errorf("expected end %d, got %d", tc.expectedMiss[i].end, miss.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindMissingTimeRangesWithFluxInterval(t *testing.T) {
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
requestedStart int64
|
||||||
|
requestedEnd int64
|
||||||
|
cachedSeries []*v3.Series
|
||||||
|
fluxInterval time.Duration
|
||||||
|
expectedMiss []missInterval
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cached time range is a subset of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 60*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 120*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fluxInterval: 5 * time.Minute,
|
||||||
|
expectedMiss: []missInterval{
|
||||||
|
{
|
||||||
|
start: 1675115596722,
|
||||||
|
end: 1675115596722 + 60*60*1000 - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: 1675115596722 + 120*60*1000 + 1,
|
||||||
|
end: 1675115596722 + 180*60*1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cached time range is a superset of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 60*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 120*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 180*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fluxInterval: 5 * time.Minute,
|
||||||
|
expectedMiss: []missInterval{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cache time range is a left overlap of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 60*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 120*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fluxInterval: 5 * time.Minute,
|
||||||
|
expectedMiss: []missInterval{
|
||||||
|
{
|
||||||
|
start: 1675115596722 + 120*60*1000 + 1,
|
||||||
|
end: 1675115596722 + 180*60*1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cache time range is a right overlap of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 60*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 120*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 180*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fluxInterval: 5 * time.Minute,
|
||||||
|
expectedMiss: []missInterval{
|
||||||
|
{
|
||||||
|
start: 1675115596722,
|
||||||
|
end: 1675115596722 + 60*60*1000 - 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cache time range is a disjoint of the requested time range",
|
||||||
|
requestedStart: 1675115596722,
|
||||||
|
requestedEnd: 1675115596722 + 180*60*1000,
|
||||||
|
cachedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 240*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 300*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Timestamp: 1675115596722 + 360*60*1000,
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fluxInterval: 5 * time.Minute,
|
||||||
|
expectedMiss: []missInterval{
|
||||||
|
{
|
||||||
|
start: 1675115596722,
|
||||||
|
end: 1675115596722 + 180*60*1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
misses := findMissingTimeRanges(tc.requestedStart, tc.requestedEnd, tc.cachedSeries, tc.fluxInterval)
|
||||||
|
if len(misses) != len(tc.expectedMiss) {
|
||||||
|
t.Errorf("expected %d misses, got %d", len(tc.expectedMiss), len(misses))
|
||||||
|
}
|
||||||
|
for i, miss := range misses {
|
||||||
|
if miss.start != tc.expectedMiss[i].start {
|
||||||
|
t.Errorf("expected start %d, got %d", tc.expectedMiss[i].start, miss.start)
|
||||||
|
}
|
||||||
|
if miss.end != tc.expectedMiss[i].end {
|
||||||
|
t.Errorf("expected end %d, got %d", tc.expectedMiss[i].end, miss.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryRange(t *testing.T) {
|
||||||
|
params := []*v3.QueryRangeParamsV3{
|
||||||
|
{
|
||||||
|
Start: 1675115596722,
|
||||||
|
End: 1675115596722 + 120*60*1000,
|
||||||
|
Step: 5 * time.Minute.Microseconds(),
|
||||||
|
CompositeQuery: &v3.CompositeQuery{
|
||||||
|
QueryType: v3.QueryTypeBuilder,
|
||||||
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||||
|
"A": {
|
||||||
|
QueryName: "A",
|
||||||
|
AggregateAttribute: v3.AttributeKey{Key: "http_server_requests_seconds_count", Type: v3.AttributeKeyTypeUnspecified, DataType: "float64", IsColumn: true},
|
||||||
|
Filters: &v3.FilterSet{
|
||||||
|
Operator: "AND",
|
||||||
|
Items: []v3.FilterItem{
|
||||||
|
{
|
||||||
|
Key: v3.AttributeKey{Key: "method", IsColumn: false},
|
||||||
|
Operator: "=",
|
||||||
|
Value: "GET",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBy: []v3.AttributeKey{
|
||||||
|
{Key: "service_name", IsColumn: false},
|
||||||
|
{Key: "method", IsColumn: false},
|
||||||
|
},
|
||||||
|
AggregateOperator: v3.AggregateOperatorSumRate,
|
||||||
|
Expression: "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Start: 1675115596722 + 60*60*1000,
|
||||||
|
End: 1675115596722 + 180*60*1000,
|
||||||
|
Step: 5 * time.Minute.Microseconds(),
|
||||||
|
CompositeQuery: &v3.CompositeQuery{
|
||||||
|
QueryType: v3.QueryTypeBuilder,
|
||||||
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||||
|
"A": {
|
||||||
|
QueryName: "A",
|
||||||
|
AggregateAttribute: v3.AttributeKey{Key: "http_server_requests_seconds_count", Type: v3.AttributeKeyTypeUnspecified, DataType: "float64", IsColumn: true},
|
||||||
|
Filters: &v3.FilterSet{
|
||||||
|
Operator: "AND",
|
||||||
|
Items: []v3.FilterItem{
|
||||||
|
{
|
||||||
|
Key: v3.AttributeKey{Key: "method", IsColumn: false},
|
||||||
|
Operator: "=",
|
||||||
|
Value: "GET",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBy: []v3.AttributeKey{
|
||||||
|
{Key: "service_name", IsColumn: false},
|
||||||
|
{Key: "method", IsColumn: false},
|
||||||
|
},
|
||||||
|
AggregateOperator: v3.AggregateOperatorSumRate,
|
||||||
|
Expression: "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cache := inmemory.New(&inmemory.Options{TTL: 5 * time.Minute, CleanupInterval: 10 * time.Minute})
|
||||||
|
opts := QuerierOptions{
|
||||||
|
Cache: cache,
|
||||||
|
Reader: nil,
|
||||||
|
FluxInterval: 5 * time.Minute,
|
||||||
|
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||||
|
|
||||||
|
TestingMode: true,
|
||||||
|
ReturnedSeries: []*v3.Series{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"method": "GET",
|
||||||
|
"service_name": "test",
|
||||||
|
"__name__": "http_server_requests_seconds_count",
|
||||||
|
},
|
||||||
|
Points: []v3.Point{
|
||||||
|
{Timestamp: 1675115596722, Value: 1},
|
||||||
|
{Timestamp: 1675115596722 + 60*60*1000, Value: 2},
|
||||||
|
{Timestamp: 1675115596722 + 120*60*1000, Value: 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
q := NewQuerier(opts)
|
||||||
|
expectedTimeRangeInQueryString := []string{
|
||||||
|
fmt.Sprintf("timestamp_ms >= %d AND timestamp_ms <= %d", 1675115596722, 1675115596722+120*60*1000),
|
||||||
|
fmt.Sprintf("timestamp_ms >= %d AND timestamp_ms <= %d", 1675115596722+120*60*1000+1, 1675115596722+180*60*1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, param := range params {
|
||||||
|
_, err, errByName := q.QueryRange(context.Background(), param, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error, got %s", err)
|
||||||
|
}
|
||||||
|
if len(errByName) > 0 {
|
||||||
|
t.Errorf("expected no error, got %v", errByName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(q.QueriesExecuted()[i], expectedTimeRangeInQueryString[i]) {
|
||||||
|
t.Errorf("expected query to contain %s, got %s", expectedTimeRangeInQueryString[i], q.QueriesExecuted()[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/SigNoz/govaluate"
|
"github.com/SigNoz/govaluate"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -192,3 +193,82 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3, args ...in
|
|||||||
}
|
}
|
||||||
return queries, nil
|
return queries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cacheKeyGenerator implements the cache.KeyGenerator interface
|
||||||
|
type cacheKeyGenerator struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func expressionToKey(expression *govaluate.EvaluableExpression, keys map[string]string) string {
|
||||||
|
|
||||||
|
var modified []govaluate.ExpressionToken
|
||||||
|
tokens := expression.Tokens()
|
||||||
|
for idx := range tokens {
|
||||||
|
token := tokens[idx]
|
||||||
|
if token.Kind == govaluate.VARIABLE {
|
||||||
|
token.Value = keys[fmt.Sprintf("%s", token.Value)]
|
||||||
|
token.Meta = keys[fmt.Sprintf("%s", token.Meta)]
|
||||||
|
}
|
||||||
|
modified = append(modified, token)
|
||||||
|
}
|
||||||
|
// err should be nil here since the expression is already validated
|
||||||
|
formula, _ := govaluate.NewEvaluableExpressionFromTokens(modified)
|
||||||
|
return formula.ExpressionString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cacheKeyGenerator) GenerateKeys(params *v3.QueryRangeParamsV3) map[string]string {
|
||||||
|
keys := make(map[string]string)
|
||||||
|
|
||||||
|
// Build keys for each builder query
|
||||||
|
for queryName, query := range params.CompositeQuery.BuilderQueries {
|
||||||
|
if query.Expression == queryName {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
// We need to build uniqe cache query for BuilderQuery
|
||||||
|
|
||||||
|
parts = append(parts, fmt.Sprintf("source=%s", query.DataSource))
|
||||||
|
parts = append(parts, fmt.Sprintf("step=%d", query.StepInterval))
|
||||||
|
parts = append(parts, fmt.Sprintf("aggregate=%s", query.AggregateOperator))
|
||||||
|
|
||||||
|
if query.AggregateAttribute.Key != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("aggregateAttribute=%s", query.AggregateAttribute.CacheKey()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Filters != nil && len(query.Filters.Items) > 0 {
|
||||||
|
for idx, filter := range query.Filters.Items {
|
||||||
|
parts = append(parts, fmt.Sprintf("filter-%d=%s", idx, filter.CacheKey()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query.GroupBy) > 0 {
|
||||||
|
for idx, groupBy := range query.GroupBy {
|
||||||
|
parts = append(parts, fmt.Sprintf("groupBy-%d=%s", idx, groupBy.CacheKey()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query.Having) > 0 {
|
||||||
|
for idx, having := range query.Having {
|
||||||
|
parts = append(parts, fmt.Sprintf("having-%d=%s", idx, having.CacheKey()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.Join(parts, "&")
|
||||||
|
keys[queryName] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build keys for each expression
|
||||||
|
for _, query := range params.CompositeQuery.BuilderQueries {
|
||||||
|
if query.Expression != query.QueryName {
|
||||||
|
expression, _ := govaluate.NewEvaluableExpressionWithFunctions(query.Expression, EvalFuncs)
|
||||||
|
|
||||||
|
expressionCacheKey := expressionToKey(expression, keys)
|
||||||
|
keys[query.QueryName] = expressionCacheKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKeyGenerator() cache.KeyGenerator {
|
||||||
|
return &cacheKeyGenerator{}
|
||||||
|
}
|
||||||
|
@ -94,3 +94,10 @@ type Reader interface {
|
|||||||
QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error)
|
QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error)
|
||||||
CheckClickHouse(ctx context.Context) error
|
CheckClickHouse(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Querier interface {
|
||||||
|
QueryRange(context.Context, *v3.QueryRangeParamsV3, map[string]v3.AttributeKey, map[string]v3.AttributeKey) ([]*v3.Series, error, map[string]string)
|
||||||
|
|
||||||
|
// test helpers
|
||||||
|
QueriesExecuted() []string
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ package v3
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -283,6 +284,10 @@ type AttributeKey struct {
|
|||||||
IsColumn bool `json:"isColumn"`
|
IsColumn bool `json:"isColumn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AttributeKey) CacheKey() string {
|
||||||
|
return fmt.Sprintf("%s-%s-%s-%t", a.Key, a.DataType, a.Type, a.IsColumn)
|
||||||
|
}
|
||||||
|
|
||||||
func (a AttributeKey) Validate() error {
|
func (a AttributeKey) Validate() error {
|
||||||
switch a.DataType {
|
switch a.DataType {
|
||||||
case AttributeKeyDataTypeBool, AttributeKeyDataTypeInt64, AttributeKeyDataTypeFloat64, AttributeKeyDataTypeString, AttributeKeyDataTypeUnspecified:
|
case AttributeKeyDataTypeBool, AttributeKeyDataTypeInt64, AttributeKeyDataTypeFloat64, AttributeKeyDataTypeString, AttributeKeyDataTypeUnspecified:
|
||||||
@ -319,6 +324,7 @@ type QueryRangeParamsV3 struct {
|
|||||||
Step int64 `json:"step"`
|
Step int64 `json:"step"`
|
||||||
CompositeQuery *CompositeQuery `json:"compositeQuery"`
|
CompositeQuery *CompositeQuery `json:"compositeQuery"`
|
||||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||||
|
NoCache bool `json:"noCache"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromQuery struct {
|
type PromQuery struct {
|
||||||
@ -534,6 +540,10 @@ type FilterItem struct {
|
|||||||
Operator FilterOperator `json:"op"`
|
Operator FilterOperator `json:"op"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FilterItem) CacheKey() string {
|
||||||
|
return fmt.Sprintf("key:%s,op:%s,value:%v", f.Key.CacheKey(), f.Operator, f.Value)
|
||||||
|
}
|
||||||
|
|
||||||
type OrderBy struct {
|
type OrderBy struct {
|
||||||
ColumnName string `json:"columnName"`
|
ColumnName string `json:"columnName"`
|
||||||
Order string `json:"order"`
|
Order string `json:"order"`
|
||||||
@ -545,6 +555,10 @@ type Having struct {
|
|||||||
Value interface{} `json:"value"`
|
Value interface{} `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Having) CacheKey() string {
|
||||||
|
return fmt.Sprintf("column:%s,op:%s,value:%v", h.ColumnName, h.Operator, h.Value)
|
||||||
|
}
|
||||||
|
|
||||||
type QueryRangeResponse struct {
|
type QueryRangeResponse struct {
|
||||||
ResultType string `json:"resultType"`
|
ResultType string `json:"resultType"`
|
||||||
Result []*Result `json:"result"`
|
Result []*Result `json:"result"`
|
||||||
@ -561,6 +575,12 @@ type Series struct {
|
|||||||
Points []Point `json:"values"`
|
Points []Point `json:"values"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Series) SortPoints() {
|
||||||
|
sort.Slice(s.Points, func(i, j int) bool {
|
||||||
|
return s.Points[i].Timestamp < s.Points[j].Timestamp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type Row struct {
|
type Row struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]interface{} `json:"data"`
|
||||||
@ -577,6 +597,21 @@ func (p *Point) MarshalJSON() ([]byte, error) {
|
|||||||
return json.Marshal(map[string]interface{}{"timestamp": p.Timestamp, "value": v})
|
return json.Marshal(map[string]interface{}{"timestamp": p.Timestamp, "value": v})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
|
func (p *Point) UnmarshalJSON(data []byte) error {
|
||||||
|
var v struct {
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Timestamp = v.Timestamp
|
||||||
|
var err error
|
||||||
|
p.Value, err = strconv.ParseFloat(v.Value, 64)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// ExploreQuery is a query for the explore page
|
// ExploreQuery is a query for the explore page
|
||||||
// It is a composite query with a source page name
|
// It is a composite query with a source page name
|
||||||
// The source page name is used to identify the page that initiated the query
|
// The source page name is used to identify the page that initiated the query
|
||||||
|
Loading…
x
Reference in New Issue
Block a user