mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
862 lines
26 KiB
Go
862 lines
26 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jaegertracing/jaeger-idl/thrift-gen/jaeger"
|
|
|
|
"github.com/grafana/tempo/integration/util"
|
|
"github.com/grafana/tempo/pkg/httpclient"
|
|
"github.com/grafana/tempo/pkg/tempopb"
|
|
v1 "github.com/grafana/tempo/pkg/tempopb/common/v1"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
configQueryRangeMaxSeries = "config-query-range-max-series.yaml"
|
|
configQueryRangeMaxSeriesDisabled = "config-query-range-max-series-disabled.yaml"
|
|
configQueryRangeExemplars = "config-query-range-exemplars.yaml"
|
|
configQueryRangeEndCutoff = "config-query-range-end-cutoff.yaml"
|
|
)
|
|
|
|
type queryRangeRequest struct {
|
|
Query string `json:"query"`
|
|
Start time.Time `json:"start"` // default: now - 5m
|
|
End time.Time `json:"end"` // default: now + 1m
|
|
Step string `json:"step"` // default: 5s
|
|
Exemplars int `json:"exemplars"` // default: 100
|
|
noDefault bool `json:"-"` // if true, SetDefaults() will not set defaults`
|
|
}
|
|
|
|
func (r *queryRangeRequest) SetDefaults() {
|
|
if r.noDefault {
|
|
return
|
|
}
|
|
if r.Start.IsZero() {
|
|
r.Start = time.Now().Add(-5 * time.Minute)
|
|
}
|
|
if r.End.IsZero() {
|
|
r.End = time.Now().Add(time.Minute)
|
|
}
|
|
if r.Step == "" {
|
|
r.Step = "5s"
|
|
}
|
|
if r.Exemplars == 0 {
|
|
r.Exemplars = 100
|
|
}
|
|
}
|
|
|
|
func TestQueryRangeExemplars(t *testing.T) {
|
|
util.RunIntegrationTests(t, util.TestHarnessConfig{
|
|
ConfigOverlay: configQueryRangeExemplars,
|
|
}, func(h *util.TempoHarness) {
|
|
h.WaitTracesWritable(t)
|
|
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
timer := time.NewTimer(10 * time.Second)
|
|
defer timer.Stop()
|
|
|
|
tracesSent := 0
|
|
|
|
// send one batch every 500ms for 10 seconds
|
|
sendLoop:
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
require.NoError(t, h.WriteJaegerBatch(
|
|
util.MakeThriftBatchWithSpanCountAttributeAndName(
|
|
1, "my operation",
|
|
"res_val", "span_val",
|
|
"res_attr", "span_attr",
|
|
), ""),
|
|
)
|
|
require.NoError(t, h.WriteJaegerBatch(
|
|
util.MakeThriftBatchWithSpanCountAttributeAndName(
|
|
1, "my operation",
|
|
"res_val2", "span_val2",
|
|
"res_attr", "span_attr",
|
|
), ""),
|
|
)
|
|
require.NoError(t, h.WriteJaegerBatch(
|
|
util.MakeThriftBatchWithSpanCountAttributeAndName(
|
|
1, "operation with high cardinality",
|
|
uuid.New().String(), uuid.New().String(),
|
|
"res_high_cardinality", "span_high_cardinality",
|
|
), ""),
|
|
)
|
|
tracesSent += 3
|
|
case <-timer.C:
|
|
break sendLoop
|
|
}
|
|
}
|
|
|
|
h.WaitTracesQueryable(t, tracesSent)
|
|
|
|
for _, exemplarsCase := range []struct {
|
|
name string
|
|
exemplars int
|
|
expectedExemplars int
|
|
}{
|
|
{
|
|
name: "default",
|
|
exemplars: 0,
|
|
expectedExemplars: 100, // if set to 0, then limits to 100
|
|
},
|
|
{
|
|
name: "5 exemplar",
|
|
exemplars: 5,
|
|
expectedExemplars: 5,
|
|
},
|
|
{
|
|
name: "25 exemplars",
|
|
exemplars: 25,
|
|
expectedExemplars: 25,
|
|
},
|
|
{
|
|
name: "capped exemplars",
|
|
exemplars: 1000,
|
|
expectedExemplars: 100, // capped to 100
|
|
},
|
|
} {
|
|
for _, query := range []string{
|
|
"{} | rate()",
|
|
"{} | compare({status=error})",
|
|
"{} | count_over_time()",
|
|
"{} | min_over_time(duration)",
|
|
"{} | max_over_time(duration)",
|
|
"{} | avg_over_time(duration)",
|
|
"{} | sum_over_time(duration)",
|
|
"{} | quantile_over_time(duration, .5)",
|
|
"{} | quantile_over_time(duration, .5, 0.9, 0.99)",
|
|
|
|
"{} | count_over_time() by (span.span_attr)",
|
|
"{} | count_over_time() by (resource.res_attr)",
|
|
"{} | count_over_time() by (.span_attr)",
|
|
"{} | count_over_time() by (.res_attr)",
|
|
|
|
"{} | histogram_over_time(duration)",
|
|
"{} | count_over_time() by (status)",
|
|
"{status != error} | count_over_time() by (status)",
|
|
} {
|
|
t.Run(fmt.Sprintf("%s: %s", exemplarsCase.name, query), func(t *testing.T) {
|
|
req := queryRangeRequest{
|
|
Query: query,
|
|
Exemplars: exemplarsCase.exemplars,
|
|
}
|
|
callQueryRange(t, h, req, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
require.GreaterOrEqual(t, len(queryRangeRes.GetSeries()), 1)
|
|
|
|
exemplarCount := 0
|
|
|
|
for _, series := range queryRangeRes.GetSeries() {
|
|
exemplarCount += len(series.GetExemplars())
|
|
}
|
|
assert.LessOrEqual(t, exemplarCount, exemplarsCase.expectedExemplars)
|
|
assert.GreaterOrEqual(t, exemplarCount, 1)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// check exemplars in more detail
|
|
for _, testCase := range []struct {
|
|
query string
|
|
targetAttribute string
|
|
targetExemplarAttribute string
|
|
}{
|
|
{
|
|
query: "{} | quantile_over_time(duration, .9) by (span.span_attr)",
|
|
targetAttribute: "span.span_attr",
|
|
targetExemplarAttribute: "span.span_attr",
|
|
},
|
|
{
|
|
query: "{} | quantile_over_time(duration, .9) by (resource.res_attr)",
|
|
targetAttribute: "resource.res_attr",
|
|
targetExemplarAttribute: "resource.res_attr",
|
|
},
|
|
{
|
|
query: "{} | quantile_over_time(duration, .9) by (.span_attr)",
|
|
targetAttribute: ".span_attr",
|
|
targetExemplarAttribute: "span.span_attr",
|
|
},
|
|
{
|
|
query: "{} | quantile_over_time(duration, .9) by (.res_attr)",
|
|
targetAttribute: ".res_attr",
|
|
targetExemplarAttribute: "resource.res_attr",
|
|
},
|
|
{
|
|
query: "{} | rate() by (span.span_attr)",
|
|
targetAttribute: "span.span_attr",
|
|
targetExemplarAttribute: "span.span_attr",
|
|
},
|
|
{
|
|
query: "{} | count_over_time() by (span.span_attr)",
|
|
targetAttribute: "span.span_attr",
|
|
targetExemplarAttribute: "span.span_attr",
|
|
},
|
|
{
|
|
query: "{} | min_over_time(duration) by (span.span_attr)",
|
|
targetAttribute: "span.span_attr",
|
|
targetExemplarAttribute: "span.span_attr",
|
|
},
|
|
{
|
|
query: "{} | max_over_time(duration) by (span.span_attr)",
|
|
targetAttribute: "span.span_attr",
|
|
targetExemplarAttribute: "span.span_attr",
|
|
},
|
|
{
|
|
query: "{} | avg_over_time(duration) by (span.span_attr)",
|
|
targetAttribute: "span.span_attr",
|
|
targetExemplarAttribute: "span.span_attr",
|
|
},
|
|
{
|
|
query: "{} | sum_over_time(duration) by (span.span_attr)",
|
|
targetAttribute: "span.span_attr",
|
|
targetExemplarAttribute: "span.span_attr",
|
|
},
|
|
} {
|
|
t.Run(testCase.query, func(t *testing.T) {
|
|
req := queryRangeRequest{
|
|
Query: testCase.query,
|
|
Exemplars: 100,
|
|
}
|
|
callQueryRange(t, h, req, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
require.Equal(t, len(queryRangeRes.GetSeries()), 3) // value 1, value 2 and nil (high cardinality's span has no such attribute)
|
|
|
|
// Verify that all exemplars in this series belongs to the right series
|
|
// by matching attribute values
|
|
var skippedForNilAttr bool
|
|
for _, series := range queryRangeRes.Series {
|
|
// search attribute value for the series
|
|
var expectedAttrValue string
|
|
for _, label := range series.Labels {
|
|
if label.Key == testCase.targetAttribute {
|
|
expectedAttrValue = label.Value.GetStringValue()
|
|
break
|
|
}
|
|
}
|
|
if (expectedAttrValue == "" || expectedAttrValue == "nil") && !skippedForNilAttr { // one attribute is empty, so we skip it
|
|
skippedForNilAttr = true
|
|
continue
|
|
}
|
|
require.NotEmpty(t, expectedAttrValue)
|
|
|
|
// check attribute value in exemplars
|
|
for _, exemplar := range series.Exemplars {
|
|
var actualAttrValue string
|
|
for _, label := range exemplar.Labels {
|
|
if label.Key == testCase.targetExemplarAttribute {
|
|
actualAttrValue = label.Value.GetStringValue()
|
|
break
|
|
}
|
|
}
|
|
require.Equal(t, expectedAttrValue, actualAttrValue)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// invalid query (both http and grpc)
|
|
for name, req := range map[string]queryRangeRequest{
|
|
"invalid query": {
|
|
Query: "{. a}",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
req := req
|
|
callQueryRange(t, h, req, func(_ *tempopb.QueryRangeResponse, err error) {
|
|
require.ErrorContains(t, err, "unexpected END_ATTRIBUTE, expecting IDENTIFIER")
|
|
})
|
|
})
|
|
}
|
|
|
|
// invalid query (http only)
|
|
// grpc accepts 0 step and provides a default
|
|
for name, req := range map[string]queryRangeRequest{
|
|
"step=0 (default step)": {
|
|
Query: "{} | count_over_time()",
|
|
Step: "0",
|
|
},
|
|
"step=0s (default step)": {
|
|
Query: "{} | count_over_time()",
|
|
Step: "0s",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
req := req
|
|
apiClient := h.APIClientHTTP("")
|
|
_, err := apiClient.MetricsQueryRange(req.Query, req.Start.UnixNano(), req.End.UnixNano(), req.Step, req.Exemplars)
|
|
require.ErrorContains(t, err, "response: 400")
|
|
})
|
|
}
|
|
|
|
// small step
|
|
t.Run("small step", func(t *testing.T) {
|
|
req := queryRangeRequest{Query: "{} | count_over_time()"}
|
|
req.SetDefaults()
|
|
req.Step = "35ms"
|
|
|
|
callQueryRange(t, h, req, func(_ *tempopb.QueryRangeResponse, err error) {
|
|
require.ErrorContains(t, err, "step of 35ms is too small, minimum step for given range is 36ms")
|
|
})
|
|
})
|
|
|
|
// query with empty results
|
|
for _, query := range []string{
|
|
// existing attribute, no traces
|
|
"{status=error} | count_over_time()",
|
|
// non-existing attribute, no traces
|
|
`{span.randomattr = "doesnotexist"} | count_over_time()`,
|
|
} {
|
|
t.Run(query, func(t *testing.T) {
|
|
callQueryRange(t, h, queryRangeRequest{Query: query, Exemplars: 100}, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
// it has time series but they are empty and has no exemplars
|
|
require.GreaterOrEqual(t, len(queryRangeRes.GetSeries()), 1)
|
|
exemplarCount := 0
|
|
for _, series := range queryRangeRes.GetSeries() {
|
|
exemplarCount += len(series.GetExemplars())
|
|
}
|
|
require.Equal(t, 0, exemplarCount)
|
|
})
|
|
})
|
|
}
|
|
|
|
for _, testCase := range []struct {
|
|
name string
|
|
query string
|
|
step string
|
|
converter func([]tempopb.Sample) float64
|
|
}{
|
|
{
|
|
name: "count_over_time",
|
|
query: "{} | count_over_time()",
|
|
step: "1s",
|
|
converter: sumSamples,
|
|
},
|
|
{
|
|
name: "sum_over_time",
|
|
query: "{} | sum_over_time(duration)",
|
|
step: "1s",
|
|
converter: sumSamples,
|
|
},
|
|
{
|
|
name: "max_over_time",
|
|
query: "{} | max_over_time(duration)",
|
|
step: "1s",
|
|
converter: maxSamples,
|
|
},
|
|
{
|
|
name: "min_over_time",
|
|
query: "{} | min_over_time(duration)",
|
|
step: "1s",
|
|
converter: minSamples,
|
|
},
|
|
{
|
|
name: "1m step",
|
|
query: "{} | count_over_time()",
|
|
step: "1m",
|
|
converter: sumSamples,
|
|
},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
req := queryRangeRequest{
|
|
Query: testCase.query,
|
|
// Query range truncates the start and end to the step, while instant query does not.
|
|
// We need to truncate the start and end to the step to align the interval for query range and instant query.
|
|
Start: time.Now().Add(-5 * time.Minute).Truncate(time.Minute),
|
|
End: time.Now().Add(time.Minute).Truncate(time.Minute),
|
|
Step: testCase.step,
|
|
}
|
|
|
|
var expectedValue float64
|
|
callQueryRange(t, h, req, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
require.Equal(t, 1, len(queryRangeRes.GetSeries()))
|
|
|
|
expectedValue = testCase.converter(queryRangeRes.Series[0].Samples)
|
|
})
|
|
|
|
instantQueryRes, err := callInstantQuery(h.APIClientHTTP(""), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, instantQueryRes)
|
|
require.Equal(t, 1, len(instantQueryRes.GetSeries()))
|
|
require.InDelta(t, expectedValue, instantQueryRes.GetSeries()[0].Value, 0.000001)
|
|
})
|
|
}
|
|
|
|
t.Run("avg_over_time instant query", func(t *testing.T) {
|
|
req := queryRangeRequest{
|
|
Query: "{} | avg_over_time(duration)",
|
|
Start: time.Now().Add(-5 * time.Minute),
|
|
End: time.Now().Add(time.Minute),
|
|
}
|
|
|
|
countReq := req
|
|
countReq.Query = "{} | count_over_time()"
|
|
countRes, err := callInstantQuery(h.APIClientHTTP(""), countReq)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, countRes)
|
|
require.Equal(t, 1, len(countRes.GetSeries()))
|
|
count := countRes.GetSeries()[0].Value
|
|
|
|
sumReq := req
|
|
sumReq.Query = "{} | sum_over_time(duration)"
|
|
sumRes, err := callInstantQuery(h.APIClientHTTP(""), sumReq)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, sumRes)
|
|
require.Equal(t, 1, len(sumRes.GetSeries()))
|
|
sum := sumRes.GetSeries()[0].Value
|
|
|
|
res, err := callInstantQuery(h.APIClientHTTP(""), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.Equal(t, 1, len(res.GetSeries()))
|
|
require.InDelta(t, sum/count, res.GetSeries()[0].Value, 0.000001)
|
|
})
|
|
|
|
for _, testCase := range []struct {
|
|
name string
|
|
query string
|
|
expectedNum int
|
|
}{
|
|
{
|
|
name: "top 1 by span attribute",
|
|
query: "{ } | rate() by (span.span_high_cardinality) | topk(1)",
|
|
expectedNum: 1,
|
|
},
|
|
{
|
|
name: "top 10 by span attribute",
|
|
query: "{ } | rate() by (span.span_high_cardinality) | topk(10)",
|
|
expectedNum: 10,
|
|
},
|
|
{
|
|
name: "top 2 by resource attribute",
|
|
query: "{ } | rate() by (resource.res_high_cardinality) | topk(2)",
|
|
expectedNum: 2,
|
|
},
|
|
{
|
|
name: "bottom 1 by resource attribute",
|
|
query: "{ } | rate() by (resource.res_high_cardinality) | bottomk(1)",
|
|
expectedNum: 1,
|
|
},
|
|
{
|
|
name: "bootom 10 by resource attribute",
|
|
query: "{ } | rate() by (resource.res_high_cardinality) | bottomk(10)",
|
|
expectedNum: 10,
|
|
},
|
|
{
|
|
name: "bottom 2 by span attribute",
|
|
query: "{ } | rate() by (span.span_high_cardinality) | bottomk(2)",
|
|
expectedNum: 2,
|
|
},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
req := queryRangeRequest{
|
|
Query: testCase.query,
|
|
Start: time.Now().Add(-5 * time.Minute),
|
|
End: time.Now().Add(time.Minute),
|
|
Step: "1m",
|
|
}
|
|
|
|
instantQueryRes, err := callInstantQuery(h.APIClientHTTP(""), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, instantQueryRes)
|
|
require.Equal(t, testCase.expectedNum, len(instantQueryRes.GetSeries()))
|
|
})
|
|
}
|
|
|
|
for _, testCase := range []struct {
|
|
name string
|
|
end time.Time
|
|
step string
|
|
expectedIntervals int
|
|
}{
|
|
// |---start|---|---end|
|
|
{name: "aligned", end: time.Now().Truncate(time.Minute), step: "1m", expectedIntervals: 3},
|
|
// |---|---start---|---|---end---|
|
|
{name: "unaligned", end: time.Now(), step: "1m", expectedIntervals: 4},
|
|
{name: "default step", end: time.Now(), step: "", expectedIntervals: 122},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
req := queryRangeRequest{
|
|
Query: "{} | count_over_time()",
|
|
Start: testCase.end.Add(-2 * time.Minute),
|
|
End: testCase.end,
|
|
Step: testCase.step,
|
|
noDefault: true,
|
|
}
|
|
|
|
callQueryRange(t, h, req, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
series := queryRangeRes.GetSeries()
|
|
require.Equal(t, 1, len(series), "Expected 1 series for count_over_time query")
|
|
require.Equal(t, testCase.expectedIntervals, len(series[0].Samples))
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func sumSamples(samples []tempopb.Sample) float64 {
|
|
var sum float64
|
|
for _, sample := range samples {
|
|
sum += sample.Value
|
|
}
|
|
return sum
|
|
}
|
|
|
|
func maxSamples(samples []tempopb.Sample) float64 {
|
|
maxValue := math.Inf(-1)
|
|
for _, sample := range samples {
|
|
if sample.Value > maxValue {
|
|
maxValue = sample.Value
|
|
}
|
|
}
|
|
return maxValue
|
|
}
|
|
|
|
func minSamples(samples []tempopb.Sample) float64 {
|
|
minValue := math.Inf(1)
|
|
for _, sample := range samples {
|
|
if sample.Value < minValue {
|
|
minValue = sample.Value
|
|
}
|
|
}
|
|
return minValue
|
|
}
|
|
|
|
// TestQueryRangeSingleTrace checks count for a single trace
|
|
// Single trace creates a block with startTime == endTime
|
|
// which covers a few edge cases under the hood.
|
|
func TestQueryRangeSingleTrace(t *testing.T) {
|
|
util.RunIntegrationTests(t, util.TestHarnessConfig{
|
|
DeploymentMode: util.DeploymentModeSingleBinary, // for unknown reasons this fails on microservices mode. TODO: figure that crap out
|
|
}, func(h *util.TempoHarness) {
|
|
h.WaitTracesWritable(t)
|
|
// Emit a single trace
|
|
require.NoError(t, h.WriteJaegerBatch(util.MakeThriftBatch(), ""))
|
|
|
|
h.WaitTracesQueryable(t, 1)
|
|
|
|
// Query the trace by count. As we have only one trace, we should get one dot with value 1
|
|
query := "{} | count_over_time()"
|
|
callQueryRange(t, h, queryRangeRequest{Query: query, Exemplars: 100}, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
require.Equal(t, len(queryRangeRes.GetSeries()), 1)
|
|
|
|
series := queryRangeRes.GetSeries()[0]
|
|
assert.Equal(t, len(series.GetExemplars()), 1)
|
|
|
|
var sum float64
|
|
for _, sample := range series.GetSamples() {
|
|
sum += sample.Value
|
|
}
|
|
require.InDelta(t, sum, 1, 0.000001)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestQueryRangeMaxSeries(t *testing.T) {
|
|
util.RunIntegrationTests(t, util.TestHarnessConfig{
|
|
ConfigOverlay: configQueryRangeMaxSeries,
|
|
}, func(h *util.TempoHarness) {
|
|
h.WaitTracesWritable(t)
|
|
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
timer := time.NewTimer(5 * time.Second)
|
|
defer timer.Stop()
|
|
|
|
tracesSent := 0
|
|
sendLoop:
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
require.NoError(t, h.WriteJaegerBatch(util.MakeThriftBatch(), ""))
|
|
tracesSent++
|
|
case <-timer.C:
|
|
break sendLoop
|
|
}
|
|
}
|
|
|
|
require.Greater(t, tracesSent, 3)
|
|
h.WaitTracesQueryable(t, tracesSent)
|
|
|
|
callQueryRange(t, h, queryRangeRequest{
|
|
Query: "{} | rate() by (span:id)",
|
|
Start: time.Now().Add(-5 * time.Minute),
|
|
End: time.Now(),
|
|
Step: "5s",
|
|
Exemplars: 100,
|
|
}, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
require.Equal(t, tempopb.PartialStatus_PARTIAL, queryRangeRes.GetStatus())
|
|
require.Equal(t, "Response exceeds maximum series limit of 3, a partial response is returned. Warning: the accuracy of each individual value is not guaranteed.", queryRangeRes.GetMessage())
|
|
require.Equal(t, 3, len(queryRangeRes.GetSeries()))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestQueryRangeMaxSeriesDisabled(t *testing.T) {
|
|
util.RunIntegrationTests(t, util.TestHarnessConfig{
|
|
ConfigOverlay: configQueryRangeMaxSeriesDisabled,
|
|
}, func(h *util.TempoHarness) {
|
|
h.WaitTracesWritable(t)
|
|
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
timer := time.NewTimer(5 * time.Second)
|
|
defer timer.Stop()
|
|
spanCount := 0
|
|
sendLoop:
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
require.NoError(t, h.WriteJaegerBatch(util.MakeThriftBatch(), ""))
|
|
spanCount++
|
|
case <-timer.C:
|
|
break sendLoop
|
|
}
|
|
}
|
|
|
|
// Wait for traces to be flushed to blocks. spanCount happens to make traces count
|
|
h.WaitTracesQueryable(t, spanCount)
|
|
|
|
callQueryRange(t, h, queryRangeRequest{
|
|
Query: "{} | rate() by (span:id)",
|
|
Start: time.Now().Add(-5 * time.Minute),
|
|
End: time.Now(),
|
|
Step: "5s",
|
|
Exemplars: 100,
|
|
}, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
require.Equal(t, tempopb.PartialStatus_COMPLETE, queryRangeRes.GetStatus())
|
|
require.Equal(t, spanCount, len(queryRangeRes.GetSeries()))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestQueryRangeTypeHandling(t *testing.T) {
|
|
util.RunIntegrationTests(t, util.TestHarnessConfig{}, func(h *util.TempoHarness) {
|
|
h.WaitTracesWritable(t)
|
|
|
|
// Emit spans where the attribute names but the values are
|
|
// string and int with the same textual representation.
|
|
t1 := util.MakeThriftBatch()
|
|
t1.Spans[0].Tags = append(t1.Spans[0].Tags, &jaeger.Tag{
|
|
Key: "foo",
|
|
VType: jaeger.TagType_STRING,
|
|
VStr: strptr("123"),
|
|
})
|
|
require.NoError(t, h.WriteJaegerBatch(t1, ""))
|
|
|
|
t2 := util.MakeThriftBatch()
|
|
t2.Spans[0].Tags = append(t2.Spans[0].Tags, &jaeger.Tag{
|
|
Key: "foo",
|
|
VType: jaeger.TagType_LONG,
|
|
VLong: int64ptr(123),
|
|
})
|
|
require.NoError(t, h.WriteJaegerBatch(t2, ""))
|
|
|
|
// Wait for traces to be flushed to blocks
|
|
h.WaitTracesQueryable(t, 2)
|
|
|
|
callQueryRange(t, h, queryRangeRequest{
|
|
Query: "{} | rate() by (span:id)",
|
|
Start: time.Now().Add(-5 * time.Minute),
|
|
End: time.Now(),
|
|
Step: "5s",
|
|
Exemplars: 100,
|
|
}, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
require.Equal(t, tempopb.PartialStatus_COMPLETE, queryRangeRes.GetStatus())
|
|
require.Equal(t, 2, len(queryRangeRes.GetSeries()))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestQueryRangeEndCutoff(t *testing.T) {
|
|
util.RunIntegrationTests(t, util.TestHarnessConfig{
|
|
ConfigOverlay: configQueryRangeEndCutoff,
|
|
}, func(h *util.TempoHarness) {
|
|
h.WaitTracesWritable(t)
|
|
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
timer := time.NewTimer(10 * time.Second)
|
|
defer timer.Stop()
|
|
|
|
tracesSent := 0
|
|
sendLoop:
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
require.NoError(t, h.WriteJaegerBatch(util.MakeThriftBatch(), ""))
|
|
tracesSent++
|
|
case <-timer.C:
|
|
break sendLoop
|
|
}
|
|
}
|
|
|
|
h.WaitTracesQueryable(t, tracesSent)
|
|
|
|
// Query with end=now, which should be adjusted by query_end_cutoff (5s)
|
|
cutoff := 5 * time.Second
|
|
now := time.Now()
|
|
|
|
req := queryRangeRequest{
|
|
Query: "{} | count_over_time()",
|
|
Start: now.Add(-15 * time.Second),
|
|
End: now,
|
|
Step: time.Second.String(),
|
|
Exemplars: 100,
|
|
}
|
|
|
|
callQueryRange(t, h, req, func(queryRangeRes *tempopb.QueryRangeResponse, err error) {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, queryRangeRes)
|
|
require.Equal(t, 1, len(queryRangeRes.GetSeries()))
|
|
|
|
series := queryRangeRes.GetSeries()[0]
|
|
samples := series.GetSamples()
|
|
require.Greater(t, len(samples), 0, "Expected at least one sample")
|
|
|
|
// The cutoff should be applied, so the last sample should be at least 'cutoff' seconds before now
|
|
maxAllowedTimestamp := now.Add(-cutoff).UnixMilli()
|
|
for _, sample := range samples {
|
|
assert.LessOrEqual(t, sample.TimestampMs, maxAllowedTimestamp,
|
|
"Sample timestamp %d is after cutoff %d (diff: %d ms)",
|
|
sample.TimestampMs, maxAllowedTimestamp, sample.TimestampMs-maxAllowedTimestamp)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func callInstantQuery(apiClient *httpclient.Client, req queryRangeRequest) (*tempopb.QueryInstantResponse, error) {
|
|
req.SetDefaults()
|
|
return apiClient.MetricsQueryInstant(req.Query, req.Start.UnixNano(), req.End.UnixNano(), req.Exemplars)
|
|
}
|
|
|
|
func callQueryRange(t *testing.T, h *util.TempoHarness, req queryRangeRequest, fn func(*tempopb.QueryRangeResponse, error)) {
|
|
req.SetDefaults()
|
|
|
|
apiClient := h.APIClientHTTP("")
|
|
fn(apiClient.MetricsQueryRange(req.Query, req.Start.UnixNano(), req.End.UnixNano(), req.Step, req.Exemplars))
|
|
|
|
step := time.Duration(0)
|
|
if req.Step != "" {
|
|
var err error
|
|
step, err = time.ParseDuration(req.Step)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
grpcClient, ctx, err := h.APIClientGRPC("")
|
|
require.NoError(t, err)
|
|
|
|
stream, err := grpcClient.MetricsQueryRange(ctx, &tempopb.QueryRangeRequest{
|
|
Query: req.Query,
|
|
Start: uint64(req.Start.UnixNano()),
|
|
End: uint64(req.End.UnixNano()),
|
|
Step: uint64(step.Nanoseconds()),
|
|
Exemplars: uint32(req.Exemplars),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
finalResponse := &tempopb.QueryRangeResponse{
|
|
Metrics: &tempopb.SearchMetrics{},
|
|
}
|
|
var finalError error
|
|
for {
|
|
t.Logf("recv")
|
|
resp, err := stream.Recv()
|
|
if resp != nil {
|
|
t.Logf("resp: %+v, count: %d", resp, len(resp.GetSeries()))
|
|
naiveQueryRangeCombine(resp, finalResponse)
|
|
t.Logf("resp: %d, finalResponse: %d", len(resp.GetSeries()), len(finalResponse.GetSeries()))
|
|
}
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
finalError = err
|
|
break
|
|
}
|
|
}
|
|
fn(finalResponse, finalError)
|
|
}
|
|
|
|
func strptr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
func int64ptr(i int64) *int64 {
|
|
return &i
|
|
}
|
|
|
|
// naiveQueryRangeCombine makes assumptions about the data being sent from Tempo. it assumes that labels orders are always
|
|
// the same and that samples and exemplars do not need to be deduped.
|
|
func naiveQueryRangeCombine(rNew, rInto *tempopb.QueryRangeResponse) {
|
|
rIntoSeries := map[string]*tempopb.TimeSeries{}
|
|
for _, series := range rInto.GetSeries() {
|
|
rIntoSeries[keyFromLabels(series.GetLabels())] = series
|
|
}
|
|
|
|
for _, newSeries := range rNew.GetSeries() {
|
|
key := keyFromLabels(newSeries.GetLabels())
|
|
if intoSeries, ok := rIntoSeries[key]; ok {
|
|
intoSeries.Exemplars = append(intoSeries.Exemplars, newSeries.Exemplars...)
|
|
intoSeries.Samples = append(intoSeries.Samples, newSeries.Samples...)
|
|
} else {
|
|
rIntoSeries[key] = newSeries
|
|
}
|
|
}
|
|
|
|
// Rebuild the series slice from the map
|
|
rInto.Series = make([]*tempopb.TimeSeries, 0, len(rIntoSeries))
|
|
for _, series := range rIntoSeries {
|
|
rInto.Series = append(rInto.Series, series)
|
|
}
|
|
|
|
if rInto.Message == "" {
|
|
rInto.Message = rNew.Message
|
|
}
|
|
if rInto.Status == 0 {
|
|
rInto.Status = rNew.Status
|
|
}
|
|
|
|
// metrics?
|
|
rInto.Metrics.CompletedJobs += rNew.Metrics.CompletedJobs
|
|
rInto.Metrics.InspectedBytes += rNew.Metrics.InspectedBytes
|
|
rInto.Metrics.InspectedTraces += rNew.Metrics.InspectedTraces
|
|
rInto.Metrics.InspectedSpans += rNew.Metrics.InspectedSpans
|
|
}
|
|
|
|
func keyFromLabels(labels []v1.KeyValue) string {
|
|
key := ""
|
|
for _, label := range labels {
|
|
key += label.Key + "=" + label.Value.GetStringValue() + ","
|
|
}
|
|
return key
|
|
}
|