mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
gorilla/mux, otelmux, gorilla/handlers and rs/cors leave go.mod. Every route the runtime serves is a zip route on a zip group: 233 from the apiserver tree, 134 from query-service, 367 total — the same 367 the mount publishes. pkg/http/routing is the one registration surface. It exists because a registration carries three facts zip has no slot for, and the tree this replaces recovered all three PER REQUEST by asking the router what it had just matched: the bound segments, the path template, and the route's declared resources. mux.CurrentRoute(req).GetHandler() was type-asserted back to handler.Handler in two separate packages to re-derive at request time what was already known once at boot. Stating them at registration deletes both lookups. The middleware chain is composed at the LEAF rather than registered ambient, because Audit keys on the route template and Resource resolves the route's declared resources — ambient middleware runs before the router has matched, so both would read an empty route. Per-leaf composition reproduces the order the old tree ran in exactly: match, then middleware, then handler. Duplicate registration now panics at boot; gorilla let the first one win. The Sentry ingest constraint becomes fiber's own guid — a UUID parse instead of a hand-written character class. Paths keep their public brace spelling in the source and in the table; the respelling happens in one function. The standalone server hands its listener to the app, so streams and the websocket upgrade run native fasthttp. Counted, not asserted: routes_test.go in the apiserver pins 233 against the real AddToRouter, routing_test.go pins the table against the router itself. This repo has already shipped 83 typed ops that reached no router while the suite stayed green — an uncalled func is legal Go, so arithmetic is the test. Co-authored-by: Hanzo Dev <[email protected]>
1604 lines
49 KiB
Go
1604 lines
49 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/o11y/pkg/http/routing"
|
|
"github.com/hanzoai/o11y/pkg/query-service/common"
|
|
"github.com/hanzoai/o11y/pkg/query-service/model"
|
|
v3 "github.com/hanzoai/o11y/pkg/query-service/model/v3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// The trace id is a PATH segment of /v1/o11y/traces/{traceId}; the span id is a
|
|
// QUERY value on the same request. Drive a REAL router registered at the real
|
|
// template, so the parse is exercised against what the router matched rather
|
|
// than against an injected map — the injector agreeing with the reader would
|
|
// prove only that the two halves of the seam agree with each other.
|
|
func TestParseSearchTracesParamsReadsTraceIDFromThePath(t *testing.T) {
|
|
var (
|
|
params *model.SearchTracesParams
|
|
err error
|
|
)
|
|
|
|
router := routing.Serve(http.MethodGet, "/v1/o11y/traces/{traceId}", http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
|
params, err = ParseSearchTracesParams(r)
|
|
}))
|
|
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v1/o11y/traces/4bf92f3577b34da6a3ce929d0e0e4736?spanId=00f067aa0ba902b7", http.NoBody))
|
|
require.Equal(t, http.StatusOK, rec.Code, "the route did not match — the test is not exercising the parse")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, params)
|
|
|
|
assert.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", params.TraceID)
|
|
assert.Equal(t, "00f067aa0ba902b7", params.SpanID)
|
|
}
|
|
|
|
func TestParseAggregateAttrReques(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
queryString string
|
|
expectedOperator v3.AggregateOperator
|
|
expectedDataSource v3.DataSource
|
|
expectedLimit int
|
|
expectedSearchText string
|
|
expectErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
desc: "valid operator and data source",
|
|
queryString: "aggregateOperator=sum&dataSource=metrics&searchText=abc",
|
|
expectedOperator: v3.AggregateOperatorSum,
|
|
expectedDataSource: v3.DataSourceMetrics,
|
|
expectedLimit: 50,
|
|
expectedSearchText: "abc",
|
|
},
|
|
{
|
|
desc: "different valid operator and data source as logs",
|
|
queryString: "aggregateOperator=avg&dataSource=logs&searchText=abc",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceLogs,
|
|
expectedLimit: 50,
|
|
expectedSearchText: "abc",
|
|
},
|
|
{
|
|
desc: "different valid operator and with default search text and limit",
|
|
queryString: "aggregateOperator=avg&dataSource=metrics",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceMetrics,
|
|
expectedLimit: 50,
|
|
expectedSearchText: "",
|
|
},
|
|
{
|
|
desc: "valid operator and data source with limit",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&limit=10",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedLimit: 10,
|
|
expectedSearchText: "",
|
|
},
|
|
{
|
|
desc: "invalid operator",
|
|
queryString: "aggregateOperator=avg1&dataSource=traces&limit=10",
|
|
expectErr: true,
|
|
errMsg: "invalid operator",
|
|
},
|
|
{
|
|
desc: "invalid data source",
|
|
queryString: "aggregateOperator=avg&dataSource=traces1&limit=10",
|
|
expectErr: true,
|
|
errMsg: "invalid data source",
|
|
},
|
|
{
|
|
desc: "invalid limit",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&limit=abc",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedLimit: 50,
|
|
},
|
|
}
|
|
|
|
for _, reqCase := range reqCases {
|
|
r := httptest.NewRequest("GET", "/api/v3/autocomplete/aggregate_attributes?"+reqCase.queryString, nil)
|
|
aggregateAttrRequest, err := parseAggregateAttributeRequest(r)
|
|
if reqCase.expectErr {
|
|
if err == nil {
|
|
t.Errorf("expected error: %s", reqCase.errMsg)
|
|
}
|
|
if !strings.Contains(err.Error(), reqCase.errMsg) {
|
|
t.Errorf("expected error to contain: %s, got: %s", reqCase.errMsg, err.Error())
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
assert.Equal(t, reqCase.expectedOperator, aggregateAttrRequest.Operator)
|
|
assert.Equal(t, reqCase.expectedDataSource, aggregateAttrRequest.DataSource)
|
|
assert.Equal(t, reqCase.expectedLimit, aggregateAttrRequest.Limit)
|
|
assert.Equal(t, reqCase.expectedSearchText, aggregateAttrRequest.SearchText)
|
|
}
|
|
}
|
|
|
|
func TestParseFilterAttributeKeyRequest(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
queryString string
|
|
expectedOperator v3.AggregateOperator
|
|
expectedDataSource v3.DataSource
|
|
expectedAggAttr string
|
|
expectedLimit int
|
|
expectedSearchText string
|
|
expectErr bool
|
|
errMsg string
|
|
expectedTagType v3.TagType
|
|
}{
|
|
{
|
|
desc: "valid operator and data source",
|
|
queryString: "aggregateOperator=sum&dataSource=metrics&aggregateAttribute=metric_name&searchText=abc",
|
|
expectedOperator: v3.AggregateOperatorSum,
|
|
expectedDataSource: v3.DataSourceMetrics,
|
|
expectedAggAttr: "metric_name",
|
|
expectedLimit: 50,
|
|
expectedSearchText: "abc",
|
|
},
|
|
{
|
|
desc: "different valid operator and data source as logs",
|
|
queryString: "aggregateOperator=avg&dataSource=logs&aggregateAttribute=bytes&searchText=abc",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceLogs,
|
|
expectedAggAttr: "bytes",
|
|
expectedLimit: 50,
|
|
expectedSearchText: "abc",
|
|
},
|
|
{
|
|
desc: "different valid operator and with default search text and limit",
|
|
queryString: "aggregateOperator=avg&dataSource=metrics&aggregateAttribute=metric_name",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceMetrics,
|
|
expectedAggAttr: "metric_name",
|
|
expectedLimit: 50,
|
|
expectedSearchText: "",
|
|
},
|
|
{
|
|
desc: "valid operator and data source with limit",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&aggregateAttribute=http.req.duration&limit=10",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedAggAttr: "http.req.duration",
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedLimit: 10,
|
|
expectedSearchText: "",
|
|
},
|
|
{
|
|
desc: "invalid operator",
|
|
queryString: "aggregateOperator=avg1&dataSource=traces&limit=10",
|
|
expectErr: true,
|
|
errMsg: "invalid operator",
|
|
},
|
|
{
|
|
desc: "invalid data source",
|
|
queryString: "aggregateOperator=avg&dataSource=traces1&limit=10",
|
|
expectErr: true,
|
|
errMsg: "invalid data source",
|
|
},
|
|
{
|
|
desc: "invalid limit",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&limit=abc",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedLimit: 50,
|
|
},
|
|
{
|
|
desc: "invalid tag type",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&tagType=invalid",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedTagType: "",
|
|
expectedLimit: 50,
|
|
},
|
|
{
|
|
desc: "valid tag type",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&tagType=resource",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedTagType: v3.TagTypeResource,
|
|
expectedLimit: 50,
|
|
},
|
|
{
|
|
desc: "valid tag type",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&tagType=scope",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedTagType: v3.TagTypeInstrumentationScope,
|
|
expectedLimit: 50,
|
|
},
|
|
{
|
|
desc: "valid tag type",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&tagType=tag",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedTagType: v3.TagTypeTag,
|
|
expectedLimit: 50,
|
|
},
|
|
}
|
|
|
|
for _, reqCase := range reqCases {
|
|
r := httptest.NewRequest("GET", "/api/v3/autocomplete/filter_attributes?"+reqCase.queryString, nil)
|
|
filterAttrRequest, err := parseFilterAttributeKeyRequest(r)
|
|
if reqCase.expectErr {
|
|
if err == nil {
|
|
t.Errorf("expected error: %s", reqCase.errMsg)
|
|
}
|
|
if !strings.Contains(err.Error(), reqCase.errMsg) {
|
|
t.Errorf("expected error to contain: %s, got: %s", reqCase.errMsg, err.Error())
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
assert.Equal(t, reqCase.expectedOperator, filterAttrRequest.AggregateOperator)
|
|
assert.Equal(t, reqCase.expectedDataSource, filterAttrRequest.DataSource)
|
|
assert.Equal(t, reqCase.expectedAggAttr, filterAttrRequest.AggregateAttribute)
|
|
assert.Equal(t, reqCase.expectedLimit, filterAttrRequest.Limit)
|
|
assert.Equal(t, reqCase.expectedSearchText, filterAttrRequest.SearchText)
|
|
}
|
|
}
|
|
|
|
func TestParseFilterAttributeValueRequest(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
queryString string
|
|
expectedOperator v3.AggregateOperator
|
|
expectedDataSource v3.DataSource
|
|
expectedAggAttr string
|
|
expectedFilterAttr string
|
|
expectedLimit int
|
|
expectedSearchText string
|
|
expectErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
desc: "valid operator and data source",
|
|
queryString: "aggregateOperator=sum&dataSource=metrics&aggregateAttribute=metric_name&attributeKey=service_name&searchText=abc",
|
|
expectedOperator: v3.AggregateOperatorSum,
|
|
expectedDataSource: v3.DataSourceMetrics,
|
|
expectedAggAttr: "metric_name",
|
|
expectedFilterAttr: "service_name",
|
|
expectedLimit: 50,
|
|
expectedSearchText: "abc",
|
|
},
|
|
{
|
|
desc: "different valid operator and data source as logs",
|
|
queryString: "aggregateOperator=avg&dataSource=logs&aggregateAttribute=bytes&attributeKey=service_name&searchText=abc",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceLogs,
|
|
expectedAggAttr: "bytes",
|
|
expectedFilterAttr: "service_name",
|
|
expectedLimit: 50,
|
|
expectedSearchText: "abc",
|
|
},
|
|
{
|
|
desc: "different valid operator and with default search text and limit",
|
|
queryString: "aggregateOperator=avg&dataSource=metrics&aggregateAttribute=metric_name&attributeKey=service_name",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceMetrics,
|
|
expectedAggAttr: "metric_name",
|
|
expectedFilterAttr: "service_name",
|
|
expectedLimit: 50,
|
|
expectedSearchText: "",
|
|
},
|
|
{
|
|
desc: "valid operator and data source with limit",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&aggregateAttribute=http.req.duration&attributeKey=service_name&limit=10",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedAggAttr: "http.req.duration",
|
|
expectedFilterAttr: "service_name",
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedLimit: 10,
|
|
expectedSearchText: "",
|
|
},
|
|
{
|
|
desc: "invalid operator",
|
|
queryString: "aggregateOperator=avg1&dataSource=traces&limit=10",
|
|
expectErr: true,
|
|
errMsg: "invalid operator",
|
|
},
|
|
{
|
|
desc: "invalid data source",
|
|
queryString: "aggregateOperator=avg&dataSource=traces1&limit=10",
|
|
expectErr: true,
|
|
errMsg: "invalid data source",
|
|
},
|
|
{
|
|
desc: "invalid limit",
|
|
queryString: "aggregateOperator=avg&dataSource=traces&limit=abc",
|
|
expectedOperator: v3.AggregateOperatorAvg,
|
|
expectedDataSource: v3.DataSourceTraces,
|
|
expectedLimit: 50,
|
|
},
|
|
}
|
|
|
|
for _, reqCase := range reqCases {
|
|
r := httptest.NewRequest("GET", "/api/v3/autocomplete/filter_attribute_values?"+reqCase.queryString, nil)
|
|
filterAttrRequest, err := parseFilterAttributeValueRequest(r)
|
|
if reqCase.expectErr {
|
|
if err == nil {
|
|
t.Errorf("expected error: %s", reqCase.errMsg)
|
|
}
|
|
if !strings.Contains(err.Error(), reqCase.errMsg) {
|
|
t.Errorf("expected error to contain: %s, got: %s", reqCase.errMsg, err.Error())
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
assert.Equal(t, reqCase.expectedOperator, filterAttrRequest.AggregateOperator)
|
|
assert.Equal(t, reqCase.expectedDataSource, filterAttrRequest.DataSource)
|
|
assert.Equal(t, reqCase.expectedAggAttr, filterAttrRequest.AggregateAttribute)
|
|
assert.Equal(t, reqCase.expectedFilterAttr, filterAttrRequest.FilterAttributeKey)
|
|
assert.Equal(t, reqCase.expectedLimit, filterAttrRequest.Limit)
|
|
assert.Equal(t, reqCase.expectedSearchText, filterAttrRequest.SearchText)
|
|
}
|
|
}
|
|
|
|
func TestParseQueryRangeParamsCompositeQuery(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
compositeQuery v3.CompositeQuery
|
|
expectErr bool
|
|
errMsg string
|
|
hasShiftBy bool
|
|
shiftBy int64
|
|
}{
|
|
{
|
|
desc: "no query in request",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeDatastoreSQL,
|
|
},
|
|
expectErr: true,
|
|
errMsg: "composite query must contain at least one query",
|
|
},
|
|
{
|
|
desc: "invalid panel type",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: "invalid",
|
|
QueryType: v3.QueryTypeDatastoreSQL,
|
|
DatastoreQueries: map[string]*v3.DatastoreQuery{
|
|
"A": {
|
|
Query: "query",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "panel type is invalid",
|
|
},
|
|
{
|
|
desc: "invalid query type",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: "invalid",
|
|
DatastoreQueries: map[string]*v3.DatastoreQuery{
|
|
"A": {
|
|
Query: "query",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "query type is invalid",
|
|
},
|
|
{
|
|
desc: "invalid prometheus query",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "query is empty",
|
|
},
|
|
{
|
|
desc: "invalid datastore query",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeDatastoreSQL,
|
|
DatastoreQueries: map[string]*v3.DatastoreQuery{
|
|
"A": {
|
|
Query: "",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "query is empty",
|
|
},
|
|
{
|
|
desc: "invalid prometheus query with disabled query",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "",
|
|
Disabled: true,
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "query is empty",
|
|
},
|
|
{
|
|
desc: "invalid datastore query with disabled query",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeDatastoreSQL,
|
|
DatastoreQueries: map[string]*v3.DatastoreQuery{
|
|
"A": {
|
|
Query: "",
|
|
Disabled: true,
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "query is empty",
|
|
},
|
|
{
|
|
desc: "valid prometheus query",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "http_calls_total",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "invalid builder query without query name",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"": {
|
|
QueryName: "",
|
|
Expression: "A",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "query name is required",
|
|
},
|
|
{
|
|
desc: "invalid data source for builder query",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: "invalid",
|
|
Expression: "A",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "data source is invalid",
|
|
},
|
|
// {
|
|
// desc: "invalid aggregate operator for builder query",
|
|
// compositeQuery: v3.CompositeQuery{
|
|
// PanelType: v3.PanelTypeGraph,
|
|
// QueryType: v3.QueryTypeBuilder,
|
|
// BuilderQueries: map[string]*v3.BuilderQuery{
|
|
// "A": {
|
|
// QueryName: "A",
|
|
// DataSource: "metrics",
|
|
// AggregateOperator: "invalid",
|
|
// Expression: "A",
|
|
// },
|
|
// },
|
|
// },
|
|
// expectErr: true,
|
|
// errMsg: "aggregate operator is invalid",
|
|
// },
|
|
{
|
|
desc: "invalid aggregate attribute for builder query",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: "metrics",
|
|
AggregateOperator: "sum",
|
|
AggregateAttribute: v3.AttributeKey{},
|
|
Expression: "A",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "aggregate attribute is required",
|
|
},
|
|
{
|
|
desc: "invalid group by attribute for builder query",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: "logs",
|
|
AggregateOperator: "sum",
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute"},
|
|
GroupBy: []v3.AttributeKey{{Key: ""}},
|
|
Expression: "A",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "builder query A is invalid: group by is invalid",
|
|
},
|
|
{
|
|
desc: "builder query with shift by",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: "logs",
|
|
AggregateOperator: "sum",
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute"},
|
|
GroupBy: []v3.AttributeKey{{Key: "group_key"}},
|
|
Expression: "A",
|
|
Functions: []v3.Function{
|
|
{
|
|
Name: v3.FunctionNameTimeShift,
|
|
Args: []interface{}{float64(10)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
hasShiftBy: true,
|
|
shiftBy: 10,
|
|
},
|
|
{
|
|
desc: "builder query with shift by as string",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: "logs",
|
|
AggregateOperator: "sum",
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute"},
|
|
GroupBy: []v3.AttributeKey{{Key: "group_key"}},
|
|
Expression: "A",
|
|
Functions: []v3.Function{
|
|
{
|
|
Name: v3.FunctionNameTimeShift,
|
|
Args: []interface{}{"3600"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
hasShiftBy: true,
|
|
shiftBy: 3600,
|
|
},
|
|
}
|
|
|
|
for _, tc := range reqCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
|
|
queryRangeParams := &v3.QueryRangeParamsV3{
|
|
Start: time.Now().Add(-time.Hour).UnixMilli(),
|
|
End: time.Now().UnixMilli(),
|
|
Step: time.Minute.Microseconds(),
|
|
CompositeQuery: &tc.compositeQuery,
|
|
Variables: map[string]interface{}{},
|
|
}
|
|
|
|
body := &bytes.Buffer{}
|
|
err := json.NewEncoder(body).Encode(queryRangeParams)
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body)
|
|
|
|
params, apiErr := ParseQueryRangeParams(req)
|
|
if tc.expectErr {
|
|
require.Error(t, apiErr)
|
|
require.Contains(t, apiErr.Error(), tc.errMsg)
|
|
} else {
|
|
require.Nil(t, apiErr)
|
|
}
|
|
if tc.hasShiftBy {
|
|
require.Equal(t, tc.shiftBy, params.CompositeQuery.BuilderQueries["A"].ShiftBy)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseQueryRangeParamsExpressions(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
compositeQuery v3.CompositeQuery
|
|
expectErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
desc: "invalid expression",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"},
|
|
Expression: "A +",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "Unexpected end of expression",
|
|
},
|
|
{
|
|
desc: "invalid expression",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceLogs,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute_logs"},
|
|
Expression: "A",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "A + B",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "unknown variable B",
|
|
},
|
|
{
|
|
desc: "invalid expression",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceLogs,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute_logs"},
|
|
Expression: "A",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "A + B + C",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "unknown variable B; unknown variable C",
|
|
},
|
|
}
|
|
|
|
for _, tc := range reqCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
|
|
queryRangeParams := &v3.QueryRangeParamsV3{
|
|
Start: time.Now().Add(-time.Hour).UnixMilli(),
|
|
End: time.Now().UnixMilli(),
|
|
Step: time.Minute.Microseconds(),
|
|
CompositeQuery: &tc.compositeQuery,
|
|
Variables: map[string]interface{}{},
|
|
}
|
|
|
|
body := &bytes.Buffer{}
|
|
err := json.NewEncoder(body).Encode(queryRangeParams)
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body)
|
|
|
|
_, apiErr := ParseQueryRangeParams(req)
|
|
if tc.expectErr {
|
|
if apiErr == nil {
|
|
t.Fatalf("expected error %s, got nil", tc.errMsg)
|
|
}
|
|
require.Error(t, apiErr)
|
|
require.Contains(t, apiErr.Error(), tc.errMsg)
|
|
} else {
|
|
require.Nil(t, apiErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseQueryRangeParamsDashboardVarsSubstitution(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
compositeQuery v3.CompositeQuery
|
|
variables map[string]interface{}
|
|
expectErr bool
|
|
errMsg string
|
|
expectedValue []interface{}
|
|
}{
|
|
{
|
|
desc: "valid builder query with dashboard variables",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"},
|
|
Expression: "A",
|
|
Filters: &v3.FilterSet{
|
|
Operator: "AND",
|
|
Items: []v3.FilterItem{
|
|
{
|
|
Key: v3.AttributeKey{Key: "service_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorEqual,
|
|
Value: "{{.service_name}}",
|
|
},
|
|
{
|
|
Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorIn,
|
|
Value: "{{.operation_name}}",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"operation_name": []interface{}{
|
|
"GET /route",
|
|
"POST /route",
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedValue: []interface{}{"route", []interface{}{"GET /route", "POST /route"}},
|
|
},
|
|
{
|
|
desc: "valid builder query with dashboard variables {{service_name}} and {{operation_name}}",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"},
|
|
Expression: "A",
|
|
Filters: &v3.FilterSet{
|
|
Operator: "AND",
|
|
Items: []v3.FilterItem{
|
|
{
|
|
Key: v3.AttributeKey{Key: "service_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorEqual,
|
|
Value: "{{service_name}}",
|
|
},
|
|
{
|
|
Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorIn,
|
|
Value: "{{operation_name}}",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"operation_name": []interface{}{
|
|
"GET /route",
|
|
"POST /route",
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedValue: []interface{}{"route", []interface{}{"GET /route", "POST /route"}},
|
|
},
|
|
{
|
|
desc: "valid builder query with dashboard variables [[service_name]] and [[operation_name]]",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"},
|
|
Expression: "A",
|
|
Filters: &v3.FilterSet{
|
|
Operator: "AND",
|
|
Items: []v3.FilterItem{
|
|
{
|
|
Key: v3.AttributeKey{Key: "service_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorEqual,
|
|
Value: "[[service_name]]",
|
|
},
|
|
{
|
|
Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorIn,
|
|
Value: "[[operation_name]]",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"operation_name": []interface{}{
|
|
"GET /route",
|
|
"POST /route",
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedValue: []interface{}{"route", []interface{}{"GET /route", "POST /route"}},
|
|
},
|
|
{
|
|
desc: "valid builder query with dashboard variables $service_name and $operation_name",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"},
|
|
Expression: "A",
|
|
Filters: &v3.FilterSet{
|
|
Operator: "AND",
|
|
Items: []v3.FilterItem{
|
|
{
|
|
Key: v3.AttributeKey{Key: "service_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorEqual,
|
|
Value: "$service_name",
|
|
},
|
|
{
|
|
Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorIn,
|
|
Value: "$operation_name",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"operation_name": []interface{}{
|
|
"GET /route",
|
|
"POST /route",
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedValue: []interface{}{"route", []interface{}{"GET /route", "POST /route"}},
|
|
},
|
|
{
|
|
desc: "multiple values for single select operator",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "attribute_metrics"},
|
|
Expression: "A",
|
|
Filters: &v3.FilterSet{
|
|
Operator: "AND",
|
|
Items: []v3.FilterItem{
|
|
{
|
|
Key: v3.AttributeKey{Key: "operation_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag},
|
|
Operator: v3.FilterOperatorEqual,
|
|
Value: "{{.operation_name}}",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"operation_name": []interface{}{
|
|
"GET /route",
|
|
"POST /route",
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "multiple values [GET /route POST /route] are not allowed for operator `=` for key `operation_name`",
|
|
},
|
|
}
|
|
|
|
for _, tc := range reqCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
|
|
queryRangeParams := &v3.QueryRangeParamsV3{
|
|
Start: time.Now().Add(-time.Hour).UnixMilli(),
|
|
End: time.Now().UnixMilli(),
|
|
Step: time.Minute.Microseconds(),
|
|
CompositeQuery: &tc.compositeQuery,
|
|
Variables: tc.variables,
|
|
}
|
|
|
|
body := &bytes.Buffer{}
|
|
err := json.NewEncoder(body).Encode(queryRangeParams)
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body)
|
|
|
|
parsedQueryRangeParams, apiErr := ParseQueryRangeParams(req)
|
|
if tc.expectErr {
|
|
require.Error(t, apiErr)
|
|
require.Contains(t, apiErr.Error(), tc.errMsg)
|
|
} else {
|
|
require.Nil(t, apiErr)
|
|
require.Equal(t, parsedQueryRangeParams.CompositeQuery.BuilderQueries["A"].Filters.Items[0].Value, tc.expectedValue[0])
|
|
require.Equal(t, parsedQueryRangeParams.CompositeQuery.BuilderQueries["A"].Filters.Items[1].Value, tc.expectedValue[1])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseQueryRangeParamsPromQLVars(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
compositeQuery v3.CompositeQuery
|
|
variables map[string]interface{}
|
|
expectErr bool
|
|
errMsg string
|
|
expectedQuery string
|
|
}{
|
|
{
|
|
desc: "valid prom query with dashboard variables",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "http_calls_total{service_name=\"{{.service_name}}\", operation_name=~\"{{.operation_name}}\"}",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"operation_name": []interface{}{
|
|
"GET /route",
|
|
"POST /route",
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedQuery: "http_calls_total{service_name=\"route\", operation_name=~\"GET /route|POST /route\"}",
|
|
},
|
|
{
|
|
desc: "valid prom query with dashboard variables",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "http_calls_total{service_name=\"{{.service_name}}\", status_code=~\"{{.status_code}}\"}",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"status_code": []interface{}{
|
|
200,
|
|
505,
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}",
|
|
},
|
|
{
|
|
desc: "valid prom query with dashboard variables {{service_name}} and {{status_code}}",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "http_calls_total{service_name=\"{{service_name}}\", status_code=~\"{{status_code}}\"}",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"status_code": []interface{}{
|
|
200,
|
|
505,
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}",
|
|
},
|
|
{
|
|
desc: "valid prom query with dashboard variables [[service_name]] and [[status_code]]",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "http_calls_total{service_name=\"[[service_name]]\", status_code=~\"[[status_code]]\"}",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"status_code": []interface{}{
|
|
200,
|
|
505,
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}",
|
|
},
|
|
{
|
|
desc: "valid prom query with dashboard variables $service_name and $status_code",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "http_calls_total{service_name=\"$service_name\", status_code=~\"$status_code\"}",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"status_code": []interface{}{
|
|
200,
|
|
505,
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedQuery: "http_calls_total{service_name=\"route\", status_code=~\"200|505\"}",
|
|
},
|
|
{
|
|
desc: "valid prom query with dashboard variables",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypePromQL,
|
|
PromQueries: map[string]*v3.PromQuery{
|
|
"A": {
|
|
Query: "http_calls_total{service_name=\"{{.service_name}}\", quantity=~\"{{.quantity}}\"}",
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
variables: map[string]interface{}{
|
|
"service_name": "route",
|
|
"quantity": []interface{}{
|
|
4.5,
|
|
4.6,
|
|
},
|
|
},
|
|
expectErr: false,
|
|
expectedQuery: "http_calls_total{service_name=\"route\", quantity=~\"4.5|4.6\"}",
|
|
},
|
|
}
|
|
|
|
for _, tc := range reqCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
|
|
queryRangeParams := &v3.QueryRangeParamsV3{
|
|
Start: time.Now().Add(-time.Hour).UnixMilli(),
|
|
End: time.Now().UnixMilli(),
|
|
Step: time.Minute.Microseconds(),
|
|
CompositeQuery: &tc.compositeQuery,
|
|
Variables: tc.variables,
|
|
}
|
|
|
|
body := &bytes.Buffer{}
|
|
err := json.NewEncoder(body).Encode(queryRangeParams)
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body)
|
|
|
|
parsedQueryRangeParams, apiErr := ParseQueryRangeParams(req)
|
|
if tc.expectErr {
|
|
require.Error(t, apiErr)
|
|
require.Contains(t, apiErr.Error(), tc.errMsg)
|
|
} else {
|
|
require.Nil(t, apiErr)
|
|
require.Equal(t, parsedQueryRangeParams.CompositeQuery.PromQueries["A"].Query, tc.expectedQuery)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueryRangeFormula(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
compositeQuery v3.CompositeQuery
|
|
variables map[string]interface{}
|
|
expectErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
desc: "disjoint group by keys",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}},
|
|
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "operation_name"}},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "B/A",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "Group keys must match or be a subset of the other but found left: [operation_name], right: [service_name]",
|
|
},
|
|
{
|
|
desc: "identical single group by key",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "B/A",
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "identical multiple group by keys",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "B/A",
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "identical multiple group by keys with different order",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "operation_name"}, {Key: "service_name"}},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "B/A",
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "subset group by keys",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "A/B",
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "empty keys on one side",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "A/B",
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "empty keys on both sides",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "A/B",
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "multiple group by keys with partial overlap",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "operation_name"}, {Key: "status_code"}},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "A/B",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "Group keys must match or be a subset of the other but found left: [service_name operation_name], right: [operation_name status_code]",
|
|
},
|
|
{
|
|
desc: "Nested Expressions with Matching Keys - Testing expressions that involve operations (e.g., addition, division) with series whose keys match or are subsets.",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "B",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "A + B",
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "Nested Expressions with Matching Keys - Testing expressions that involve operations (e.g., addition, division) with series whose keys match or are subsets.",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "A",
|
|
},
|
|
"B": {
|
|
QueryName: "B",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}},
|
|
Expression: "B",
|
|
},
|
|
"C": {
|
|
QueryName: "C",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}, {Key: "status_code"}},
|
|
Expression: "C",
|
|
},
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "C/(A + B)",
|
|
},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
desc: "Unknow variable in expression",
|
|
compositeQuery: v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"F1": {
|
|
QueryName: "F1",
|
|
Expression: "A + B",
|
|
},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
errMsg: "unknown variable",
|
|
},
|
|
}
|
|
|
|
for _, tc := range reqCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
|
|
queryRangeParams := &v3.QueryRangeParamsV3{
|
|
Start: time.Now().Add(-time.Hour).UnixMilli(),
|
|
End: time.Now().UnixMilli(),
|
|
Step: time.Minute.Microseconds(),
|
|
CompositeQuery: &tc.compositeQuery,
|
|
Variables: tc.variables,
|
|
}
|
|
|
|
body := &bytes.Buffer{}
|
|
err := json.NewEncoder(body).Encode(queryRangeParams)
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v4/query_range", body)
|
|
|
|
_, apiErr := ParseQueryRangeParams(req)
|
|
if tc.expectErr {
|
|
require.Error(t, apiErr)
|
|
require.Contains(t, apiErr.Error(), tc.errMsg)
|
|
} else {
|
|
if apiErr != nil {
|
|
if apiErr.Err != nil {
|
|
t.Fatalf("unexpected error for case: %s - %v", tc.desc, apiErr.Err)
|
|
}
|
|
}
|
|
require.Nil(t, apiErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseQueryRangeParamsStepIntervalAdjustment(t *testing.T) {
|
|
reqCases := []struct {
|
|
desc string
|
|
start int64
|
|
end int64
|
|
step int64
|
|
}{
|
|
{
|
|
desc: "30 minutes and 60 seconds step",
|
|
start: time.Now().Add(-30 * time.Minute).UnixMilli(),
|
|
end: time.Now().UnixMilli(),
|
|
step: 60, // no update
|
|
},
|
|
{
|
|
desc: "1 hour and 1 second step",
|
|
start: time.Now().Add(-time.Hour).UnixMilli(),
|
|
end: time.Now().UnixMilli(),
|
|
step: 1, // gets updated
|
|
},
|
|
{
|
|
desc: "1 day and 1 hour step",
|
|
start: time.Now().Add(-24 * time.Hour).UnixMilli(),
|
|
end: time.Now().UnixMilli(),
|
|
step: 3600, // no update
|
|
},
|
|
{
|
|
desc: "1 day and 1 minute step",
|
|
start: time.Now().Add(-24 * time.Hour).UnixMilli(),
|
|
end: time.Now().UnixMilli(),
|
|
step: 60, // gets updated
|
|
},
|
|
{
|
|
desc: "1 day and 2 minutes step",
|
|
start: time.Now().Add(-24 * time.Hour).UnixMilli(),
|
|
end: time.Now().UnixMilli(),
|
|
step: 120, // gets updated
|
|
},
|
|
{
|
|
desc: "1 day and 5 minutes step",
|
|
start: time.Now().Add(-24 * time.Hour).UnixMilli(),
|
|
end: time.Now().UnixMilli(),
|
|
step: 300, // no update
|
|
},
|
|
{
|
|
desc: "1 week and 45 minutes step",
|
|
start: time.Now().Add(-7 * 24 * time.Hour).UnixMilli(),
|
|
end: time.Now().UnixMilli(),
|
|
step: 2700, // no update
|
|
},
|
|
}
|
|
|
|
for _, tc := range reqCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
|
|
queryRangeParams := &v3.QueryRangeParamsV3{
|
|
Start: tc.start,
|
|
End: tc.end,
|
|
Step: tc.step,
|
|
CompositeQuery: &v3.CompositeQuery{
|
|
PanelType: v3.PanelTypeGraph,
|
|
QueryType: v3.QueryTypeBuilder,
|
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
|
"A": {
|
|
QueryName: "A",
|
|
DataSource: v3.DataSourceMetrics,
|
|
AggregateOperator: v3.AggregateOperatorSum,
|
|
AggregateAttribute: v3.AttributeKey{Key: "o11y_calls_total"},
|
|
GroupBy: []v3.AttributeKey{{Key: "service_name"}, {Key: "operation_name"}},
|
|
Expression: "A",
|
|
StepInterval: tc.step,
|
|
},
|
|
},
|
|
},
|
|
Variables: map[string]interface{}{},
|
|
}
|
|
|
|
body := &bytes.Buffer{}
|
|
err := json.NewEncoder(body).Encode(queryRangeParams)
|
|
require.NoError(t, err)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v3/query_range", body)
|
|
|
|
p, apiErr := ParseQueryRangeParams(req)
|
|
if apiErr != nil && apiErr.Err != nil {
|
|
t.Fatalf("unexpected error %s", apiErr.Err)
|
|
}
|
|
require.True(t, p.CompositeQuery.BuilderQueries["A"].StepInterval >= common.MinAllowedStepInterval(p.Start, p.End))
|
|
})
|
|
}
|
|
}
|