Files
traces/integration/e2e/api/multi_tenant_test.go
T
21ea5626eb feat(export): Add OTLP export support replacing Jaeger agent (#5058)
* add jaeger oltp

* add jaeger oltp e2e

* add jaeger oltp util

* add jaeger oltp changelog

* Update util.go

* fix merge and make it work

* tidy and vendor

* fix last test and changelog

* rename client

* Update CHANGELOG.md

---------

Co-authored-by: javiermolinar <javiermolinar@live.com>
2025-07-07 08:23:13 -04:00

249 lines
7.6 KiB
Go

package api
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/grafana/dskit/user"
"github.com/grafana/e2e"
"github.com/grafana/tempo/integration/util"
"github.com/grafana/tempo/pkg/collector"
"github.com/grafana/tempo/pkg/httpclient"
"github.com/grafana/tempo/pkg/tempopb"
tempoUtil "github.com/grafana/tempo/pkg/util"
"github.com/prometheus/prometheus/model/labels"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
"github.com/grafana/tempo/cmd/tempo/app"
"github.com/grafana/tempo/integration/e2e/backend"
)
const (
configMultiTenant = "config-multi-tenant-local.yaml"
)
type traceStringsMap struct {
rKeys []string
rValues []string
spanNames []string
}
func TestSingleTenantSearch(t *testing.T) {
t.Parallel()
testSearch(t, "test", 1)
}
func TestWildCardTenantSearch(t *testing.T) {
t.Parallel()
testSearch(t, "*", 1)
}
func TestTwoTenantsSearch(t *testing.T) {
t.Parallel()
testSearch(t, "test|test2", 2)
}
func TestThreeTenantsSearch(t *testing.T) {
t.Parallel()
testSearch(t, "test|test2|test3", 3)
}
func generateNetworkName() string {
uuid := uuid.New()
return uuid.String()
}
func testSearch(t *testing.T, tenant string, tenantSize int) {
s, err := e2e.NewScenario(generateNetworkName())
require.NoError(t, err)
defer s.Close()
// set up the backend
cfg := app.Config{}
buff, err := os.ReadFile(configMultiTenant)
require.NoError(t, err)
err = yaml.UnmarshalStrict(buff, &cfg)
require.NoError(t, err)
_, err = backend.New(s, cfg)
require.NoError(t, err)
require.NoError(t, util.CopyFileToSharedDir(s, configMultiTenant, "config.yaml"))
tempo := util.NewTempoAllInOne()
prometheus := util.NewPrometheus()
require.NoError(t, s.StartAndWaitReady(tempo, prometheus))
// Get port for the Jaeger gRPC receiver endpoint
c, err := util.NewJaegerToOTLPExporter(tempo.Endpoint(4317))
require.NoError(t, err)
require.NotNil(t, c)
var info *tempoUtil.TraceInfo
var traceMap traceStringsMap
tenants := strings.Split(tenant, "|")
require.Equal(t, tenantSize, len(tenants))
var expectedSpans float64
// write traces for all tenants
for _, tenant := range tenants {
info = tempoUtil.NewTraceInfo(time.Now(), tenant)
require.NoError(t, info.EmitAllBatches(c))
trace, err := info.ConstructTraceFromEpoch()
traceMap = getAttrsAndSpanNames(trace) // store it to assert tests
require.NoError(t, err)
expectedSpans = expectedSpans + util.SpanCount(trace)
}
// assert that we have one trace and each tenant and correct number of spans received
require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(float64(tenantSize)), "tempo_ingester_traces_created_total"))
require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(expectedSpans), "tempo_distributor_spans_received_total"))
// Wait for the traces to be written to the WAL
time.Sleep(time.Second * 3)
// test echo
util.AssertEcho(t, "http://"+tempo.Endpoint(tempoPort)+"/api/echo")
// client will have testcase tenant id
apiClient := httpclient.New("http://"+tempo.Endpoint(tempoPort), tenant)
// check trace by id
resp, err := apiClient.QueryTrace(info.HexID())
require.NoError(t, err)
respTm := getAttrsAndSpanNames(resp)
assert.ElementsMatch(t, traceMap.rValues, respTm.rValues)
assert.ElementsMatch(t, respTm.rKeys, traceMap.rKeys)
assert.ElementsMatch(t, traceMap.spanNames, respTm.spanNames)
// flush trace to backend
util.CallFlush(t, tempo)
// search and traceql search, note: SearchAndAssertTrace also calls SearchTagValues
util.SearchAndAssertTrace(t, apiClient, info)
util.SearchTraceQLAndAssertTrace(t, apiClient, info)
// force clear completed block
util.CallFlush(t, tempo)
// wait for flush to complete for all tenants, each tenant will have one block
require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(float64(tenantSize)), "tempo_ingester_blocks_flushed_total"))
// call search tags endpoints, ensure no errors and results are not empty
tagsResp, err := apiClient.SearchTags()
require.NoError(t, err)
require.NotEmpty(t, tagsResp.TagNames)
tagsV2Resp, err := apiClient.SearchTagsV2()
require.NoError(t, err)
require.Equal(t, 4, len(tagsV2Resp.GetScopes())) // resource, span, event, link, instrumentation intrinsics
for _, s := range tagsV2Resp.Scopes {
require.NotEmpty(t, s.Tags)
}
tagsValuesResp, err := apiClient.SearchTagValues("vulture-0")
require.NoError(t, err)
require.NotEmpty(t, tagsValuesResp.TagValues)
tagsValuesV2Resp, err := apiClient.SearchTagValuesV2("span.vulture-0", "{}")
require.NoError(t, err)
require.NotEmpty(t, tagsValuesV2Resp.TagValues)
// check metrics for all routes
routeTable := []struct {
route string
reqCount int
}{
// query frontend routes
{route: "api_search", reqCount: 2}, // called twice
{route: "api_traces_traceid", reqCount: 1},
{route: "api_search_tags", reqCount: 1},
{route: "api_search_tag_tagname_values", reqCount: 2}, // called twice
{route: "api_v2_search_tags", reqCount: 1},
{route: "api_v2_search_tag_tagname_values", reqCount: 1},
// Querier routes, we make one request for each tenant
{route: "/tempopb.Querier/SearchRecent", reqCount: 2 * tenantSize}, // called twice
{route: "/tempopb.Querier/FindTraceByID", reqCount: tenantSize},
{route: "/tempopb.Querier/SearchTags", reqCount: tenantSize},
{route: "/tempopb.Querier/SearchTagsV2", reqCount: tenantSize},
{route: "/tempopb.Querier/SearchTagValues", reqCount: 2 * tenantSize}, // called twice
{route: "/tempopb.Querier/SearchTagValuesV2", reqCount: tenantSize},
}
for _, rt := range routeTable {
assertRequestCountMetric(t, tempo, rt.route, rt.reqCount)
}
// test streaming search over grpc
grpcCtx := user.InjectOrgID(context.Background(), tenant)
grpcCtx, err = user.InjectIntoGRPCRequest(grpcCtx)
require.NoError(t, err)
grpcClient, err := util.NewSearchGRPCClient(grpcCtx, tempo.Endpoint(tempoPort))
require.NoError(t, err)
time.Sleep(2 * time.Second) // ensure that blocklist poller has built the blocklist
now := time.Now()
util.SearchStreamAndAssertTrace(t, grpcCtx, grpcClient, info, now.Add(-10*time.Minute).Unix(), now.Add(10*time.Minute).Unix())
assertRequestCountMetric(t, tempo, "/tempopb.StreamingQuerier/Search", 1)
// test unsupported endpoint
_, msErr := apiClient.MetricsSummary("{}", "name", 0, 0)
if tenantSize > 1 {
// error for multi-tenant request for unsupported endpoints
require.Error(t, msErr)
} else {
require.NoError(t, msErr)
}
}
func assertRequestCountMetric(t *testing.T, s *e2e.HTTPService, route string, reqCount int) {
fmt.Printf("==== %s, assertRequestCountMetric route: %v, rt.reqCount: %v \n", t.Name(), route, reqCount)
err := s.WaitSumMetricsWithOptions(e2e.Equals(float64(reqCount)),
[]string{"tempo_request_duration_seconds"},
e2e.WithLabelMatchers(labels.MustNewMatcher(labels.MatchEqual, "route", route)),
e2e.WithMetricCount, // get count from histogram metric
)
require.NoError(t, err)
}
// getAttrsAndSpanNames returns trace attrs and span names
func getAttrsAndSpanNames(trace *tempopb.Trace) traceStringsMap {
rAttrsKeys := collector.NewDistinctString(0, 0, 0)
rAttrsValues := collector.NewDistinctString(0, 0, 0)
spanNames := collector.NewDistinctString(0, 0, 0)
for _, b := range trace.ResourceSpans {
for _, ss := range b.ScopeSpans {
for _, s := range ss.Spans {
if s.Name != "" {
spanNames.Collect(s.Name)
}
}
}
for _, a := range b.Resource.Attributes {
if a.Key != "" {
rAttrsKeys.Collect(a.Key)
}
if a.Value.GetStringValue() != "" {
rAttrsValues.Collect(a.Value.GetStringValue())
}
}
}
return traceStringsMap{
rKeys: rAttrsKeys.Strings(),
rValues: rAttrsValues.Strings(),
spanNames: spanNames.Strings(),
}
}