Files
Joe ElliottandGitHub 5ee93d3cf8 Tempo 3.0: Integration Tests Rewrite (#6088)
* First pass test harness

Signed-off-by: Joe Elliott <[email protected]>

* limits - passed

Signed-off-by: Joe Elliott <[email protected]>

* metrics generator - passed

Signed-off-by: Joe Elliott <[email protected]>

* harness cleanup/fix target info test

Signed-off-by: Joe Elliott <[email protected]>

* deployments - passed

Signed-off-by: Joe Elliott <[email protected]>

* receivers - passed

Signed-off-by: Joe Elliott <[email protected]>

* harness cleanup

Signed-off-by: Joe Elliott <[email protected]>

* storage - passed

Signed-off-by: Joe Elliott <[email protected]>

* api - passing

Signed-off-by: Joe Elliott <[email protected]>

* cleanup and CI

Signed-off-by: Joe Elliott <[email protected]>

* Additional tests

Signed-off-by: Joe Elliott <[email protected]>

* it's me and you linter. how many times are you going to do this to me?

Signed-off-by: Joe Elliott <[email protected]>

* lint and tests

Signed-off-by: Joe Elliott <[email protected]>

* restored live store partition tests

Signed-off-by: Joe Elliott <[email protected]>

* massive lint

Signed-off-by: Joe Elliott <[email protected]>

* cleanup

Signed-off-by: Joe Elliott <[email protected]>

* lint >.<

Signed-off-by: Joe Elliott <[email protected]>

* wups

Signed-off-by: Joe Elliott <[email protected]>

* cleanup and stabilizing

Signed-off-by: Joe Elliott <[email protected]>

* cleanup

Signed-off-by: Joe Elliott <[email protected]>

* omg

Signed-off-by: Joe Elliott <[email protected]>

* added config file. removed empty file. deleted some debug code

Signed-off-by: Joe Elliott <[email protected]>

* even more fixes

Signed-off-by: Joe Elliott <[email protected]>

* maybe this works? maybe it was all local networking issues

Signed-off-by: Joe Elliott <[email protected]>

* review

Signed-off-by: Joe Elliott <[email protected]>

* backed off huge write timeout. added missing config file

Signed-off-by: Joe Elliott <[email protected]>

* restore disable parallelism setting. restore beefy write timeout

Signed-off-by: Joe Elliott <[email protected]>

* apparently another test relied on the http receiver

Signed-off-by: Joe Elliott <[email protected]>

* livestore avail zone!

Signed-off-by: Joe Elliott <[email protected]>

* add support for v1 traces and tags endpoints

Signed-off-by: Joe Elliott <[email protected]>

* review

Signed-off-by: Joe Elliott <[email protected]>

* changed the location of the changelog entry to avoid conflicts

Signed-off-by: Joe Elliott <[email protected]>

---------

Signed-off-by: Joe Elliott <[email protected]>
2026-01-07 08:31:11 -05:00

398 lines
11 KiB
Go

package util
import (
"context"
"fmt"
"math/rand"
"time"
"github.com/grafana/dskit/user"
jaeger "github.com/jaegertracing/jaeger-idl/thrift-gen/jaeger"
jaegerTrans "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger"
"go.opentelemetry.io/collector/pdata/ptrace"
"github.com/grafana/tempo/pkg/tempopb"
v1common "github.com/grafana/tempo/pkg/tempopb/common/v1"
)
var (
// maxBatchesPerWrite is used when writing and reading, and needs to match so
// that we get the expected number of batches on a trace. A value larger
// than 10 here results in vulture writing traces that exceed the maximum
// trace size.
maxBatchesPerWrite int64 = 10
// maxBatchesPerWrite is the maximum number of time-delayed writes for a trace.
maxLongWritesPerTrace int64 = 3
)
// TraceInfo is used to construct synthetic traces and manage the expectations.
type TraceInfo struct {
timestamp time.Time
r *rand.Rand
traceIDHigh int64
traceIDLow int64
longWritesRemaining int64
tempoOrgID string
}
// JaegerClient is an interface used to mock the underlying client in tests.
type JaegerClient interface {
EmitBatch(ctx context.Context, b *jaeger.Batch) error
}
// NewTraceInfo is used to produce a new TraceInfo.
func NewTraceInfo(timestamp time.Time, tempoOrgID string) *TraceInfo {
r := newRand(timestamp)
return &TraceInfo{
timestamp: timestamp,
r: r,
traceIDHigh: r.Int63(),
traceIDLow: r.Int63(),
longWritesRemaining: r.Int63n(maxLongWritesPerTrace),
tempoOrgID: tempoOrgID,
}
}
// NewTraceInfos creates multiple trace infos with slightly different timestamps to ensure
// different trace seeds are used.
func NewTraceInfos(timestamp time.Time, count int, tempoOrgID string) []*TraceInfo {
infos := make([]*TraceInfo, 0, count)
for i := 0; i < count; i++ {
ts := timestamp.Add(time.Duration(i) * time.Nanosecond)
infos = append(infos, NewTraceInfo(ts, tempoOrgID))
}
return infos
}
func NewTraceInfoWithMaxLongWrites(timestamp time.Time, maxLongWrites int64, tempoOrgID string) *TraceInfo {
r := newRand(timestamp)
return &TraceInfo{
timestamp: timestamp,
r: r,
traceIDHigh: r.Int63(),
traceIDLow: r.Int63(),
longWritesRemaining: maxLongWrites,
tempoOrgID: tempoOrgID,
}
}
func (t *TraceInfo) Ready(now time.Time, writeBackoff, longWriteBackoff time.Duration) bool {
// Don't use the last time interval to allow the write loop to finish before
// we try to read it.
if t.timestamp.After(now.Add(-writeBackoff)) {
return false
}
// Compare a new instance with the same timestamp to know how many longWritesRemaining.
totalWrites := NewTraceInfo(t.timestamp, t.tempoOrgID).longWritesRemaining
// We are not ready if not all writes have had a chance to send.
lastWrite := t.timestamp.Add(time.Duration(totalWrites) * longWriteBackoff)
return !now.Before(lastWrite.Add(longWriteBackoff))
}
func (t *TraceInfo) Timestamp() time.Time {
return t.timestamp
}
func (t *TraceInfo) TraceID() ([]byte, error) {
return HexStringToTraceID(t.HexID())
}
func (t *TraceInfo) HexID() string {
return fmt.Sprintf("%016x%016x", t.traceIDHigh, t.traceIDLow)
}
func (t *TraceInfo) LongWritesRemaining() int64 {
return t.longWritesRemaining
}
func (t *TraceInfo) Done() {
t.longWritesRemaining--
}
func (t *TraceInfo) EmitBatches(ctx context.Context, c JaegerClient) error {
for i := int64(0); i < t.generateRandomInt(1, maxBatchesPerWrite); i++ {
ctx := user.InjectOrgID(ctx, t.tempoOrgID)
ctx, err := user.InjectIntoGRPCRequest(ctx)
if err != nil {
return fmt.Errorf("error injecting org id: %w", err)
}
err = c.EmitBatch(ctx, t.makeThriftBatch(t.traceIDHigh, t.traceIDLow))
if err != nil {
return fmt.Errorf("error pushing batch to Tempo: %w", err)
}
}
return nil
}
// EmitAllBatches sends all the batches that would normally be sent at some
// interval when using EmitBatches.
func (t *TraceInfo) EmitAllBatches(c JaegerClient) error {
err := t.EmitBatches(context.Background(), c)
if err != nil {
return err
}
for t.LongWritesRemaining() > 0 {
t.Done()
err := t.EmitBatches(context.Background(), c)
if err != nil {
return err
}
}
return nil
}
func (t *TraceInfo) generateRandomInt(min, max int64) int64 {
min++
number := min + t.r.Int63n(max-min)
return number
}
func (t *TraceInfo) makeThriftBatch(traceIDHigh, traceIDLow int64) *jaeger.Batch {
var spans []*jaeger.Span
count := t.generateRandomInt(1, 5)
lastSpanID, nextSpanID := int64(0), int64(0)
// Each span has the previous span as parent, creating a tree with a single branch per batch.
for i := int64(0); i < count; i++ {
nextSpanID = t.r.Int63()
spans = append(spans, &jaeger.Span{
TraceIdLow: traceIDLow,
TraceIdHigh: traceIDHigh,
SpanId: nextSpanID,
ParentSpanId: lastSpanID,
OperationName: fmt.Sprintf("vulture-%d", t.generateRandomInt(0, 100)),
References: nil,
Flags: 0,
StartTime: t.timestamp.UnixMicro(),
Duration: t.generateRandomInt(0, 100),
Tags: t.generateRandomTags(),
Logs: t.generateRandomLogs(),
})
lastSpanID = nextSpanID
}
process := &jaeger.Process{
ServiceName: "tempo-vulture",
Tags: t.generateRandomTagsWithPrefix("vulture-process"),
}
return &jaeger.Batch{Process: process, Spans: spans}
}
func (t *TraceInfo) makeJaegerBatch(TraceIDHigh, TraceIDLow int64) *jaeger.Batch {
var spans []*jaeger.Span
count := t.generateRandomInt(1, 5)
lastSpanID, nextSpanID := int64(0), int64(0)
// Each span has the previous span as parent, creating a tree with a single branch per batch.
for i := int64(0); i < count; i++ {
nextSpanID = t.r.Int63()
spans = append(spans, &jaeger.Span{
TraceIdLow: TraceIDLow,
TraceIdHigh: TraceIDHigh,
SpanId: nextSpanID,
ParentSpanId: lastSpanID,
OperationName: fmt.Sprintf("vulture-%d", t.generateRandomInt(0, 100)),
References: nil,
Flags: 0,
StartTime: t.timestamp.UnixMicro(),
Duration: t.generateRandomInt(0, 100),
Tags: t.generateRandomJaegerTags(),
Logs: t.generateRandomJaegerLogs(),
})
lastSpanID = nextSpanID
}
process := &jaeger.Process{
ServiceName: "tempo-vulture",
Tags: t.generateRandomJaegerTagsWithPrefix("vulture-process"),
}
return &jaeger.Batch{Process: process, Spans: spans}
}
func (t *TraceInfo) generateRandomString() string {
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
s := make([]rune, t.generateRandomInt(5, 20))
for i := range s {
s[i] = letters[t.r.Intn(len(letters))]
}
return string(s)
}
func (t *TraceInfo) generateRandomTags() []*jaeger.Tag {
return t.generateRandomTagsWithPrefix("vulture")
}
func (t *TraceInfo) generateRandomJaegerTags() []*jaeger.Tag {
return t.generateRandomJaegerTagsWithPrefix("vulture")
}
func (t *TraceInfo) generateRandomTagsWithPrefix(prefix string) []*jaeger.Tag {
var tags []*jaeger.Tag
count := t.generateRandomInt(1, 5)
for i := int64(0); i < count; i++ {
value := t.generateRandomString()
tags = append(tags, &jaeger.Tag{
Key: fmt.Sprintf("%s-%d", prefix, i),
VStr: &value,
})
}
return tags
}
func (t *TraceInfo) generateRandomJaegerTagsWithPrefix(prefix string) []*jaeger.Tag {
var tags []*jaeger.Tag
count := t.generateRandomInt(1, 5)
for i := int64(0); i < count; i++ {
value := t.generateRandomString()
tags = append(tags, &jaeger.Tag{
Key: fmt.Sprintf("%s-%d", prefix, i),
VStr: &value,
})
}
return tags
}
func (t *TraceInfo) generateRandomLogs() []*jaeger.Log {
var logs []*jaeger.Log
count := t.generateRandomInt(1, 5)
for i := int64(0); i < count; i++ {
logs = append(logs, &jaeger.Log{
Timestamp: t.timestamp.UnixMicro(),
Fields: t.generateRandomTags(),
})
}
return logs
}
func (t *TraceInfo) generateRandomJaegerLogs() []*jaeger.Log {
var logs []*jaeger.Log
count := t.generateRandomInt(1, 5)
for i := int64(0); i < count; i++ {
logs = append(logs, &jaeger.Log{
Timestamp: t.timestamp.UnixMicro(),
Fields: t.generateRandomJaegerTags(),
})
}
return logs
}
func (t *TraceInfo) ConstructTraceFromEpoch() (*tempopb.Trace, error) {
trace := &tempopb.Trace{}
// Create a new trace from our timestamp to ensure a fresh rand.Rand is used for consistency.
info := NewTraceInfo(t.timestamp, t.tempoOrgID)
addBatches := func(t *TraceInfo, trace *tempopb.Trace) error {
for i := int64(0); i < t.generateRandomInt(1, maxBatchesPerWrite); i++ {
batch := t.makeJaegerBatch(t.traceIDHigh, t.traceIDLow)
internalTrace, err := jaegerTrans.ThriftToTraces(batch)
if err != nil {
return err
}
conv, err := (&ptrace.ProtoMarshaler{}).MarshalTraces(internalTrace)
if err != nil {
return err
}
t := tempopb.Trace{}
err = t.Unmarshal(conv)
if err != nil {
return err
}
// Due to the several transforms above, some manual mangling is required to
// get the parentSpanID to match. In the case of an empty []byte in place
// for the ParentSpanId, we set to nil here to ensure that the final result
// matches the json.Unmarshal value when tempo is queried.
for _, b := range t.ResourceSpans {
for _, l := range b.ScopeSpans {
for _, s := range l.Spans {
if len(s.GetParentSpanId()) == 0 {
s.ParentSpanId = nil
}
}
}
}
trace.ResourceSpans = append(trace.ResourceSpans, t.ResourceSpans...)
}
return nil
}
err := addBatches(info, trace)
if err != nil {
return nil, err
}
for info.longWritesRemaining > 0 {
info.Done()
err := addBatches(info, trace)
if err != nil {
return nil, err
}
}
return trace, nil
}
func RandomAttrFromTrace(t *tempopb.Trace) *v1common.KeyValue {
r := newRand(time.Now())
if len(t.ResourceSpans) == 0 {
return nil
}
batch := randFrom(r, t.ResourceSpans)
// maybe choose resource attribute
res := batch.Resource
if len(res.Attributes) > 0 && r.Int()%2 == 1 {
attr := randFrom(r, res.Attributes)
// skip service.name because service names have low cardinality and produce queries with
// too many results in tempo-vulture
if attr.Key != "service.name" {
return attr
}
}
if len(batch.ScopeSpans) == 0 {
return nil
}
ss := randFrom(r, batch.ScopeSpans)
if len(ss.Spans) == 0 {
return nil
}
span := randFrom(r, ss.Spans)
if len(span.Attributes) == 0 {
return nil
}
return randFrom(r, span.Attributes)
}
func randFrom[T any](r *rand.Rand, s []T) T {
return s[r.Intn(len(s))]
}
func newRand(t time.Time) *rand.Rand {
return rand.New(rand.NewSource(t.UnixNano())) // nolint:gosec // G404: Use of weak random number generator
}