From 654d15296363b44dfa71f20dff2b4fdea9e3f9bd Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Fri, 27 Oct 2023 18:12:29 +0530 Subject: [PATCH 01/34] Add combiners to frontend --------- Co-authored-by: Mario --- modules/frontend/combiner/common.go | 105 ++++++++++++ modules/frontend/combiner/interface.go | 21 +++ modules/frontend/combiner/search.go | 94 +++++++++++ .../frontend/combiner/search_tag_values.go | 66 ++++++++ modules/frontend/combiner/search_tags.go | 36 ++++ modules/frontend/combiner/trace_by_id.go | 156 ++++++++++++++++++ 6 files changed, 478 insertions(+) create mode 100644 modules/frontend/combiner/common.go create mode 100644 modules/frontend/combiner/interface.go create mode 100644 modules/frontend/combiner/search.go create mode 100644 modules/frontend/combiner/search_tag_values.go create mode 100644 modules/frontend/combiner/search_tags.go create mode 100644 modules/frontend/combiner/trace_by_id.go diff --git a/modules/frontend/combiner/common.go b/modules/frontend/combiner/common.go new file mode 100644 index 00000000000..0f7d4695b41 --- /dev/null +++ b/modules/frontend/combiner/common.go @@ -0,0 +1,105 @@ +package combiner + +import ( + "errors" + "fmt" + "io" + "net/http" + "strings" + "sync" + + "github.com/grafana/tempo/pkg/api" + "github.com/grafana/tempo/pkg/tempopb" +) + +type TResponse interface { + *tempopb.SearchResponse | *tempopb.SearchTagsResponse | *tempopb.SearchTagValuesResponse | *tempopb.SearchTagValuesV2Response +} + +type genericCombiner[R TResponse] struct { + mu sync.Mutex + + final R + + combine func(body io.ReadCloser, final R) error + result func(R) (string, error) + + code int + statusMessage string + err error +} + +func (c *genericCombiner[R]) AddRequest(res *http.Response, _ func(t *tempopb.Trace)) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.shouldQuit() { + return nil + } + + c.code = res.StatusCode + + if res.StatusCode != http.StatusOK { + bytesMsg, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("error reading response body: %w", err) + } + c.statusMessage = string(bytesMsg) + return errors.New(c.statusMessage) + } + + defer func() { _ = res.Body.Close() }() + if err := c.combine(res.Body, c.final); err != nil { + c.statusMessage = internalErrorMsg + c.err = fmt.Errorf("error unmarshalling response body: %w", err) + return c.err + } + + return nil +} + +func (c *genericCombiner[R]) Complete() (*http.Response, error) { + c.mu.Lock() + defer c.mu.Unlock() + + bodyString, err := c.result(c.final) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: c.code, + Header: http.Header{ + api.HeaderContentType: {api.HeaderAcceptJSON}, + }, + Body: io.NopCloser(strings.NewReader(bodyString)), + ContentLength: int64(len([]byte(bodyString))), + }, nil +} + +func (c *genericCombiner[R]) StatusCode() int { + c.mu.Lock() + defer c.mu.Unlock() + + return c.code +} + +func (c *genericCombiner[R]) ShouldQuit() bool { + c.mu.Lock() + defer c.mu.Unlock() + + return c.shouldQuit() +} + +func (c *genericCombiner[R]) shouldQuit() bool { + if c.err != nil { + return true + } + + if c.code/100 == 5 { // Bail on 5xx + return true + } + + // 2xx and 404 are OK + return false +} diff --git a/modules/frontend/combiner/interface.go b/modules/frontend/combiner/interface.go new file mode 100644 index 00000000000..6b7105ba80d --- /dev/null +++ b/modules/frontend/combiner/interface.go @@ -0,0 +1,21 @@ +package combiner + +import ( + "net/http" + + "github.com/grafana/tempo/pkg/tempopb" +) + +// Combiner is used to merge multiple responses into a single response. +// +// Implementations must be thread-safe. +type Combiner interface { + // TODO: The callback is a hacky way of injecting the tenant label in tenant federation. + // We should figure out a better way to do this. + // FIXME: remove cb and just inject tenant label in Combiner impl. + AddRequest(r *http.Response, cb func(t *tempopb.Trace)) error + Complete() (*http.Response, error) + StatusCode() int + + ShouldQuit() bool +} diff --git a/modules/frontend/combiner/search.go b/modules/frontend/combiner/search.go new file mode 100644 index 00000000000..91467b8c16b --- /dev/null +++ b/modules/frontend/combiner/search.go @@ -0,0 +1,94 @@ +package combiner + +import ( + "fmt" + "io" + "sort" + + "github.com/gogo/protobuf/jsonpb" + "github.com/grafana/tempo/pkg/tempopb" +) + +var _ Combiner = (*genericCombiner[*tempopb.SearchResponse])(nil) + +func NewSearch() Combiner { + resultsMap := make(map[string]*tempopb.TraceSearchMetadata) + return &genericCombiner[*tempopb.SearchResponse]{ + code: 200, + final: &tempopb.SearchResponse{Metrics: &tempopb.SearchMetrics{}}, + combine: func(body io.ReadCloser, final *tempopb.SearchResponse) error { + response := &tempopb.SearchResponse{} + if err := jsonpb.Unmarshal(body, response); err != nil { + return fmt.Errorf("error unmarshalling response body: %w", err) + } + for _, t := range response.Traces { + if res := resultsMap[t.TraceID]; res != nil { + // Merge search results + CombineSearchResults(res, t) + } else { + // New entry + resultsMap[t.TraceID] = t + } + } + + if response.Metrics != nil { + final.Metrics.InspectedBytes += response.Metrics.InspectedBytes + final.Metrics.InspectedTraces += response.Metrics.InspectedTraces + final.Metrics.TotalBlocks += response.Metrics.TotalBlocks + final.Metrics.CompletedJobs += response.Metrics.CompletedJobs + final.Metrics.TotalJobs += response.Metrics.TotalJobs + final.Metrics.TotalBlockBytes += response.Metrics.TotalBlockBytes + } + + return nil + }, + result: func(response *tempopb.SearchResponse) (string, error) { + for _, t := range resultsMap { + response.Traces = append(response.Traces, t) + } + sort.Slice(response.Traces, func(i, j int) bool { + return response.Traces[i].StartTimeUnixNano > response.Traces[j].StartTimeUnixNano + }) + + return new(jsonpb.Marshaler).MarshalToString(response) + }, + } +} + +// TODO (mdisibio) - This function exists in Tempo but is missing TraceQL results and is also +// being relocated in a refactor soon. Delete this copy once it is in the final location +// and updated for TraceQL results. +func CombineSearchResults(existing *tempopb.TraceSearchMetadata, incoming *tempopb.TraceSearchMetadata) { + if existing.TraceID == "" { + existing.TraceID = incoming.TraceID + } + + if existing.RootServiceName == "" { + existing.RootServiceName = incoming.RootServiceName + } + + if existing.RootTraceName == "" { + existing.RootTraceName = incoming.RootTraceName + } + + // Earliest start time. + if existing.StartTimeUnixNano > incoming.StartTimeUnixNano { + existing.StartTimeUnixNano = incoming.StartTimeUnixNano + } + + // Longest duration + if existing.DurationMs < incoming.DurationMs { + existing.DurationMs = incoming.DurationMs + } + + // If TraceQL results are present + if incoming.SpanSet != nil { + if existing.SpanSet == nil { + existing.SpanSet = &tempopb.SpanSet{} + } + + existing.SpanSet.Matched += incoming.SpanSet.Matched + existing.SpanSet.Spans = append(existing.SpanSet.Spans, incoming.SpanSet.Spans...) + // Note - should we dedupe spans? Spans shouldn't be present in multiple clusters. + } +} diff --git a/modules/frontend/combiner/search_tag_values.go b/modules/frontend/combiner/search_tag_values.go new file mode 100644 index 00000000000..f72ae5cb13d --- /dev/null +++ b/modules/frontend/combiner/search_tag_values.go @@ -0,0 +1,66 @@ +package combiner + +import ( + "fmt" + "io" + + "github.com/gogo/protobuf/jsonpb" + "github.com/grafana/tempo/pkg/tempopb" + "github.com/grafana/tempo/pkg/util" +) + +var ( + _ Combiner = (*genericCombiner[*tempopb.SearchTagValuesResponse])(nil) + _ Combiner = (*genericCombiner[*tempopb.SearchTagValuesV2Response])(nil) +) + +func NewSearchTagValues() Combiner { + // Distinct collector with no limit + d := util.NewDistinctValueCollector(0, func(_ string) int { return 0 }) + + return &genericCombiner[*tempopb.SearchTagValuesResponse]{ + code: 200, + final: &tempopb.SearchTagValuesResponse{TagValues: make([]string, 0)}, + combine: func(body io.ReadCloser, final *tempopb.SearchTagValuesResponse) error { + response := &tempopb.SearchTagValuesResponse{} + if err := jsonpb.Unmarshal(body, response); err != nil { + return fmt.Errorf("error unmarshalling response body: %w", err) + } + for _, v := range response.TagValues { + d.Collect(v) + } + return nil + }, + result: func(response *tempopb.SearchTagValuesResponse) (string, error) { + response.TagValues = d.Values() + return new(jsonpb.Marshaler).MarshalToString(response) + }, + } +} + +func NewSearchTagValuesV2() Combiner { + // Distinct collector with no limit + d := util.NewDistinctValueCollector(0, func(_ tempopb.TagValue) int { return 0 }) + + return &genericCombiner[*tempopb.SearchTagValuesV2Response]{ + final: &tempopb.SearchTagValuesV2Response{TagValues: []*tempopb.TagValue{}}, + combine: func(body io.ReadCloser, final *tempopb.SearchTagValuesV2Response) error { + response := &tempopb.SearchTagValuesV2Response{} + if err := jsonpb.Unmarshal(body, response); err != nil { + return fmt.Errorf("error unmarshalling response body: %w", err) + } + for _, v := range response.TagValues { + d.Collect(*v) + } + return nil + }, + result: func(response *tempopb.SearchTagValuesV2Response) (string, error) { + values := d.Values() + for _, v := range values { + v2 := v + response.TagValues = append(response.TagValues, &v2) + } + return new(jsonpb.Marshaler).MarshalToString(response) + }, + } +} diff --git a/modules/frontend/combiner/search_tags.go b/modules/frontend/combiner/search_tags.go new file mode 100644 index 00000000000..e578580cc47 --- /dev/null +++ b/modules/frontend/combiner/search_tags.go @@ -0,0 +1,36 @@ +package combiner + +import ( + "fmt" + "io" + + "github.com/gogo/protobuf/jsonpb" + "github.com/grafana/tempo/pkg/tempopb" + "github.com/grafana/tempo/pkg/util" +) + +var _ Combiner = (*genericCombiner[*tempopb.SearchTagsResponse])(nil) + +func NewSearchTags() Combiner { + // Distinct collector with no limit + d := util.NewDistinctValueCollector(0, func(_ string) int { return 0 }) + + return &genericCombiner[*tempopb.SearchTagsResponse]{ + code: 200, + final: &tempopb.SearchTagsResponse{TagNames: make([]string, 0)}, + combine: func(body io.ReadCloser, final *tempopb.SearchTagsResponse) error { + response := &tempopb.SearchTagsResponse{} + if err := jsonpb.Unmarshal(body, response); err != nil { + return fmt.Errorf("error unmarshalling response body: %w", err) + } + for _, v := range response.TagNames { + d.Collect(v) + } + return nil + }, + result: func(response *tempopb.SearchTagsResponse) (string, error) { + response.TagNames = d.Values() + return new(jsonpb.Marshaler).MarshalToString(response) + }, + } +} diff --git a/modules/frontend/combiner/trace_by_id.go b/modules/frontend/combiner/trace_by_id.go new file mode 100644 index 00000000000..048c716bc20 --- /dev/null +++ b/modules/frontend/combiner/trace_by_id.go @@ -0,0 +1,156 @@ +package combiner + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "strings" + "sync" + + "github.com/gogo/protobuf/proto" + "github.com/grafana/tempo/pkg/api" + "github.com/grafana/tempo/pkg/model/trace" + "github.com/grafana/tempo/pkg/tempopb" +) + +const ( + internalErrorMsg = "internal error" +) + +type traceByIDCombiner struct { + mu sync.Mutex + + c *trace.Combiner + + code int + statusMessage string + err error +} + +func NewTraceByID() Combiner { + return &traceByIDCombiner{ + c: trace.NewCombiner(0), + code: http.StatusNotFound, + } +} + +func (c *traceByIDCombiner) AddRequest(res *http.Response, cb func(t *tempopb.Trace)) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.shouldQuit() { + return nil + } + + if res.StatusCode == http.StatusNotFound { + // 404s are not considered errors, so we don't need to do anything. + return nil + } + c.code = res.StatusCode + + if res.StatusCode != http.StatusOK { + bytesMsg, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("error reading response body: %w", err) + } + c.statusMessage = string(bytesMsg) + return errors.New(c.statusMessage) + } + + // Read the body + buff, err := io.ReadAll(res.Body) + if err != nil { + c.statusMessage = internalErrorMsg + c.err = fmt.Errorf("error reading response body: %w", err) + return c.err + } + _ = res.Body.Close() + + // Unmarshal the body + trace := &tempopb.Trace{} + err = trace.Unmarshal(buff) + if err != nil { + c.statusMessage = internalErrorMsg + c.err = fmt.Errorf("error unmarshalling response body: %w", err) + return c.err + } + + // Call the callback + if cb != nil { + cb(trace) + } + + // Consume the trace + _, err = c.c.Consume(trace) + return err +} + +func (c *traceByIDCombiner) Complete() (*http.Response, error) { + c.mu.Lock() + defer c.mu.Unlock() + + statusCode := c.getStatusCode() + traceResult, _ := c.c.Result() + + if traceResult == nil || statusCode != http.StatusOK { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(c.statusMessage)), + Header: http.Header{}, + }, nil + } + + buff, err := proto.Marshal(traceResult) + if err != nil { + return &http.Response{}, fmt.Errorf("error marshalling response to proto: %w", err) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{ + api.HeaderContentType: {api.HeaderAcceptProtobuf}, + }, + Body: io.NopCloser(bytes.NewReader(buff)), + ContentLength: int64(len(buff)), + }, nil +} + +func (c *traceByIDCombiner) StatusCode() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.getStatusCode() +} + +func (c *traceByIDCombiner) getStatusCode() int { + statusCode := c.code + // Translate non-404s 4xx into 500s. If, for instance, we get a 400 back from an internal component + // it means that we created a bad request. 400 should not be propagated back to the user b/c + // the bad request was due to a bug on our side, so return 500 instead. + if statusCode/100 == 4 && statusCode != http.StatusNotFound { + statusCode = 500 + } + + return statusCode +} + +// ShouldQuit returns true if the response should be returned early. +func (c *traceByIDCombiner) ShouldQuit() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.shouldQuit() +} + +func (c *traceByIDCombiner) shouldQuit() bool { + if c.err != nil { + return true + } + + if c.getStatusCode()/100 == 5 { // Bail on 5xx + return true + } + + // 2xx and 404 are OK + return false +} From 8902131a693069fda826e1036d43273fd5d22007 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Fri, 27 Oct 2023 19:19:10 +0530 Subject: [PATCH 02/34] Add MultiTenantQueriesEnabled config in frontend --- docs/sources/tempo/configuration/_index.md | 4 ++++ modules/frontend/config.go | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/sources/tempo/configuration/_index.md b/docs/sources/tempo/configuration/_index.md index e797a8b0826..92debdba6af 100644 --- a/docs/sources/tempo/configuration/_index.md +++ b/docs/sources/tempo/configuration/_index.md @@ -423,6 +423,10 @@ query_frontend: # (default: 5) [max_batch_size: ] + # Enable multi-tenant queries + # (default: true) + [multi_tenant_queries_enabled: ] + search: # The number of concurrent jobs to execute when searching the backend. diff --git a/modules/frontend/config.go b/modules/frontend/config.go index d287ee10573..8a536635e46 100644 --- a/modules/frontend/config.go +++ b/modules/frontend/config.go @@ -16,10 +16,11 @@ import ( var statVersion = usagestats.NewString("frontend_version") type Config struct { - Config v1.Config `yaml:",inline"` - MaxRetries int `yaml:"max_retries,omitempty"` - Search SearchConfig `yaml:"search"` - TraceByID TraceByIDConfig `yaml:"trace_by_id"` + Config v1.Config `yaml:",inline"` + MaxRetries int `yaml:"max_retries,omitempty"` + Search SearchConfig `yaml:"search"` + TraceByID TraceByIDConfig `yaml:"trace_by_id"` + MultiTenantQueriesEnabled bool `yaml:"multi_tenant_queries_enabled"` } type SearchConfig struct { @@ -73,6 +74,9 @@ func (cfg *Config) RegisterFlagsAndApplyDefaults(string, *flag.FlagSet) { HedgeRequestsUpTo: 2, }, } + + // enable multi tenant queries by default + cfg.MultiTenantQueriesEnabled = true } type CortexNoQuerierLimits struct{} From a0e4b53ca9ba610478571465737dbd152ea0ad33 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 31 Oct 2023 00:27:23 +0530 Subject: [PATCH 03/34] Add MultiTenantMiddleware and tests --------- Co-authored-by: Mario --- modules/frontend/tenant.go | 180 ++++++++++++++++++++++++++++++++ modules/frontend/tenant_test.go | 129 +++++++++++++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 modules/frontend/tenant.go create mode 100644 modules/frontend/tenant_test.go diff --git a/modules/frontend/tenant.go b/modules/frontend/tenant.go new file mode 100644 index 00000000000..1d7b2ee168a --- /dev/null +++ b/modules/frontend/tenant.go @@ -0,0 +1,180 @@ +package frontend + +import ( + "context" + "fmt" + "net/http" + "strconv" + "sync" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/grafana/dskit/tenant" + "github.com/grafana/dskit/user" + "github.com/grafana/tempo/modules/frontend/combiner" + "github.com/grafana/tempo/pkg/tempopb" + v1 "github.com/grafana/tempo/pkg/tempopb/common/v1" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const ( + statusCodeLabel = "status_code" + tenantLabel = "tenant" +) + +var ( + tenantSuccessTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "tempo_tenant_federation", + Name: "success_total", + Help: "Total number of successful fetches of a trace per tenant.", + }, + []string{tenantLabel}) + + tenantFailureTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "tempo_tenant_federation", + Name: "failures_total", + Help: "Total number of failing fetches of a trace per tenant.", + }, + []string{tenantLabel, statusCodeLabel}) +) + +type tenantRoundTripper struct { + cfg Config + next http.RoundTripper + logger log.Logger + + resolver tenant.Resolver + + newCombiner func() combiner.Combiner + + tenantSuccessTotal *prometheus.CounterVec + tenantFailureTotal *prometheus.CounterVec +} + +// TODO: add a middleware to return error in case of multiple tenants in unsupported routes + +// newMultiTenantMiddleware returns a middleware that takes a request and fans it out to each tenant +func newMultiTenantMiddleware(cfg Config, combinerFn func() combiner.Combiner, logger log.Logger) Middleware { + return MiddlewareFunc(func(next http.RoundTripper) http.RoundTripper { + return &tenantRoundTripper{ + cfg: cfg, + next: next, + logger: logger, + resolver: tenant.NewMultiResolver(), + newCombiner: combinerFn, + tenantSuccessTotal: tenantSuccessTotal, + tenantFailureTotal: tenantFailureTotal, + } + }) +} + +func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if !t.cfg.MultiTenantQueriesEnabled { + // move on to next tripper if multi-tenant queries are not enabled + return t.next.RoundTrip(req) + } + + _, ctx, err := user.ExtractOrgIDFromHTTPRequest(req) + if err == user.ErrNoOrgID { + // no org id, move to next tripper + return t.next.RoundTrip(req) + } + if err != nil { + return nil, fmt.Errorf("failed to extract org id from request: %w", err) + } + + // extract tenant ids + tenants, err := t.resolver.TenantIDs(ctx) + if err != nil { + return nil, err + } + // for single tenant, fall through to next round tripper + if len(tenants) <= 1 { + return t.next.RoundTrip(req) + } + + _ = level.Debug(t.logger).Log("msg", "got multiple tenant ids...", "tenants", tenants) + + var wg sync.WaitGroup + respCombiner := t.newCombiner() + + // call RoundTrip for each tenant and combine results + // Send one request per tenant to down-stream tripper + // Return early if statusCode is already set by a previous response + for _, tenantID := range tenants { + wg.Add(1) + go func(tenant string) { + defer wg.Done() + // build a sub request context for each tenant because we want to modify and inject a tenant id into the context. + // this is done so that components downstream of frontend doesn't need to know anything about multi-tenant query + subCtx, cancel := context.WithCancel(req.Context()) + defer cancel() + + if respCombiner.ShouldQuit() { + return + } + + _ = level.Info(t.logger).Log("msg", "sending request for tenant", "tenant", tenant) + + r := requestForTenant(subCtx, req, tenant) + resp, err := t.next.RoundTrip(r) + + if respCombiner.ShouldQuit() { + return + } + + // Check http error + if err != nil { + _ = level.Error(t.logger).Log("msg", "error querying for tenant", "tenant", tenant, "err", err) + t.tenantFailureTotal.With(prometheus.Labels{tenantLabel: tenant, statusCodeLabel: strconv.Itoa(respCombiner.StatusCode())}).Inc() + return + } + + // If we get here, we have a successful response + if err := respCombiner.AddRequest(resp, injectTenantResource(tenant)); err != nil { + _ = level.Error(t.logger).Log("msg", "error combining responses", "tenant", tenant, "err", err) + t.tenantFailureTotal.With(prometheus.Labels{tenantLabel: tenant, statusCodeLabel: strconv.Itoa(resp.StatusCode)}).Inc() + return + } + + _ = level.Debug(t.logger).Log("msg", "success probing", "tenant", tenant) + t.tenantSuccessTotal.With(prometheus.Labels{tenantLabel: tenant}).Inc() + }(tenantID) + } + // TODO: will this work for search streaming, look into it. might need a search steaming combiner + wg.Wait() + + return respCombiner.Complete() +} + +// requestForTenant makes a copy of request and injects the tenant id into context and Header. +// this allows us to keep all multi-tenant logic in query frontend and keep other components single tenant +func requestForTenant(ctx context.Context, r *http.Request, tenant string) *http.Request { + ctx = user.InjectOrgID(ctx, tenant) + rCopy := r.Clone(ctx) + rCopy.Header.Set(user.OrgIDHeaderName, tenant) + return rCopy +} + +// injectTenantResource will add tenantLabel attribute into response to show which tenant the response came from +func injectTenantResource(tenant string) func(t *tempopb.Trace) { + return func(t *tempopb.Trace) { + if t == nil || t.Batches == nil { + return + } + + for _, b := range t.Batches { + b.Resource.Attributes = append(b.Resource.Attributes, &v1.KeyValue{ + Key: tenantLabel, + Value: &v1.AnyValue{ + Value: &v1.AnyValue_StringValue{ + StringValue: tenant, + }, + }, + }) + } + } +} diff --git a/modules/frontend/tenant_test.go b/modules/frontend/tenant_test.go new file mode 100644 index 00000000000..8618092bced --- /dev/null +++ b/modules/frontend/tenant_test.go @@ -0,0 +1,129 @@ +package frontend + +import ( + "bytes" + "crypto/rand" + "io" + "net/http" + "strings" + "sync" + "testing" + + "github.com/go-kit/log" + "github.com/grafana/dskit/user" + "github.com/grafana/tempo/modules/frontend/combiner" + "github.com/grafana/tempo/pkg/tempopb" + "github.com/grafana/tempo/pkg/util/test" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" +) + +func TestMultiTenant(t *testing.T) { + tests := []struct { + name string + tenants string + }{ + { + name: "single tenant", + tenants: "single-tenant", + }, + { + name: "multiple tenants", + tenants: "tenant-1|tenant-2|tenant-3", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := Config{ + TraceByID: TraceByIDConfig{ + QueryShards: minQueryShards, + SLO: testSLOcfg, + }, + Search: SearchConfig{ + Sharder: SearchSharderConfig{ + ConcurrentRequests: defaultConcurrentRequests, + TargetBytesPerRequest: defaultTargetBytesPerRequest, + }, + SLO: testSLOcfg, + }, + MultiTenantQueriesEnabled: true, + } + tenantMiddleware := newMultiTenantMiddleware(cfg, combiner.NewTraceByID, log.NewNopLogger()) + + var reqCount atomic.Int32 + + tenantsMap := make(map[string]struct{}, len(tc.tenants)) + tenants := strings.Split(tc.tenants, "|") + for _, tenant := range tenants { + tenantsMap[tenant] = struct{}{} + } + + traceID := make([]byte, 16) + _, err := rand.Read(traceID) + require.NoError(t, err) + trace := test.MakeTrace(10, traceID) + + once := sync.Once{} + var fastestTenant string + next := RoundTripperFunc(func(req *http.Request) (*http.Response, error) { + reqCount.Inc() // Count the number of requests. + + // Check if the tenant is in the list of tenants. + tenantID, _, err := user.ExtractOrgIDFromHTTPRequest(req) + require.NoError(t, err) + _, ok := tenantsMap[tenantID] + require.True(t, ok) + + // we do this in requestForTenant method, which is skipped for single tenant + if len(tenants) > 1 { + // ensure that tenant id in http header is same as org id in context + // some places are using http headers and some are using context to + // extract tenant id form the request so need both to be set and be correct. + orgID, err := user.ExtractOrgID(req.Context()) + require.NoError(t, err) + require.Equal(t, tenantID, orgID) + } + + statusCode := http.StatusNotFound + var body []byte + once.Do(func() { + fastestTenant = tenantID + statusCode = http.StatusOK + buff, err := trace.Marshal() + require.NoError(t, err) + body = buff + }) + + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil + }) + + rt := NewRoundTripper(next, tenantMiddleware) + + req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/", nil) + require.NoError(t, err) + req.Header.Set(user.OrgIDHeaderName, tc.tenants) + + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.Equal(t, len(tenants), int(reqCount.Load())) + require.NotNil(t, res) + require.Equal(t, http.StatusOK, res.StatusCode) + + buff, err := io.ReadAll(res.Body) + require.NoError(t, err) + // Unmarshal response into a trace. + responseTrace := &tempopb.Trace{} + require.NoError(t, responseTrace.Unmarshal(buff)) + // Add tenant to the original trace to compare. + if len(tenants) > 1 { + injectTenantResource(fastestTenant)(trace) + } + // Check if the trace is the same as the original. + require.Equal(t, trace, responseTrace) + }) + } +} From 0cb452bb353a1e587dcb3fdac4d51198aeea0376 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 31 Oct 2023 01:01:34 +0530 Subject: [PATCH 04/34] Expose dedicated handler in queryfrontend for each http endpoint --- cmd/tempo/app/modules.go | 34 ++++++++++++++-------------------- modules/frontend/frontend.go | 13 +++++++++---- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/cmd/tempo/app/modules.go b/cmd/tempo/app/modules.go index 5aed7f63d5c..87e27310578 100644 --- a/cmd/tempo/app/modules.go +++ b/cmd/tempo/app/modules.go @@ -364,18 +364,6 @@ func (t *App) initQueryFrontend() (services.Service, error) { return nil, err } - // wrap handlers with auth - middleware := middleware.Merge( - t.HTTPAuthMiddleware, - httpGzipMiddleware(), - ) - - traceByIDHandler := middleware.Wrap(queryFrontend.TraceByIDHandler) - searchHandler := middleware.Wrap(queryFrontend.SearchHandler) - searchWSHandler := middleware.Wrap(queryFrontend.SearchWSHandler) - spanMetricsSummaryHandler := middleware.Wrap(queryFrontend.SpanMetricsSummaryHandler) - searchTagsHandler := middleware.Wrap(queryFrontend.SearchTagsHandler) - // register grpc server for queriers to connect to frontend_v1pb.RegisterFrontendServer(t.Server.GRPC, t.frontend) // we register the streaming querier service on both the http and grpc servers. Grafana expects @@ -383,19 +371,25 @@ func (t *App) initQueryFrontend() (services.Service, error) { tempopb.RegisterStreamingQuerierServer(t.Server.GRPC, queryFrontend) tempopb.RegisterStreamingQuerierServer(t.Server.GRPCOnHTTPServer, queryFrontend) + // wrap handlers with auth + base := middleware.Merge( + t.HTTPAuthMiddleware, + httpGzipMiddleware(), + ) + // http trace by id endpoint - t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathTraces), traceByIDHandler) + t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathTraces), base.Wrap(queryFrontend.TraceByIDHandler)) // http search endpoints - t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearch), searchHandler) - t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathWSSearch), searchWSHandler) - t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearchTags), searchTagsHandler) - t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearchTagsV2), searchTagsHandler) - t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearchTagValues), searchTagsHandler) - t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearchTagValuesV2), searchTagsHandler) + t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearch), base.Wrap(queryFrontend.SearchHandler)) + t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathWSSearch), base.Wrap(queryFrontend.SearchWSHandler)) + t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearchTags), base.Wrap(queryFrontend.SearchTagsHandler)) + t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearchTagsV2), base.Wrap(queryFrontend.SearchTagsV2Handler)) + t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearchTagValues), base.Wrap(queryFrontend.SearchTagsValuesHandler)) + t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSearchTagValuesV2), base.Wrap(queryFrontend.SearchTagsValuesV2Handler)) // http metrics endpoints - t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSpanMetricsSummary), spanMetricsSummaryHandler) + t.Server.HTTP.Handle(addHTTPAPIPrefix(&t.cfg, api.PathSpanMetricsSummary), base.Wrap(queryFrontend.SpanMetricsSummaryHandler)) // the query frontend needs to have knowledge of the blocks so it can shard search jobs t.store.EnablePolling(context.Background(), nil) diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index b7b3d84e5a0..806b0397305 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -27,10 +27,11 @@ import ( type streamingSearchHandler func(req *tempopb.SearchRequest, srv tempopb.StreamingQuerier_SearchServer) error type QueryFrontend struct { - TraceByIDHandler, SearchHandler, SearchTagsHandler, SpanMetricsSummaryHandler, SearchWSHandler http.Handler - cacheProvider cache.Provider - streamingSearch streamingSearchHandler - logger log.Logger + TraceByIDHandler, SearchHandler, SpanMetricsSummaryHandler, SearchWSHandler http.Handler + SearchTagsHandler, SearchTagsV2Handler, SearchTagsValuesHandler, SearchTagsValuesV2Handler http.Handler + cacheProvider cache.Provider + streamingSearch streamingSearchHandler + logger log.Logger } // New returns a new QueryFrontend @@ -74,6 +75,10 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), SearchHandler: newHandler(search, searchSLOPostHook(cfg.Search.SLO), searchSLOPreHook, logger), SearchTagsHandler: newHandler(searchTags, nil, nil, logger), + SearchTagsV2Handler: newHandler(searchTags, nil, nil, logger), + SearchTagsValuesHandler: newHandler(searchTags, nil, nil, logger), + SearchTagsValuesV2Handler: newHandler(searchTags, nil, nil, logger), + SpanMetricsSummaryHandler: newHandler(metrics, nil, nil, logger), SearchWSHandler: newSearchStreamingWSHandler(cfg, o, retryWare.Wrap(next), reader, searchCache, apiPrefix, logger), cacheProvider: cacheProvider, From 248c5f3d8d0231d6d59d428e62ed4489dcda4c3e Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 31 Oct 2023 01:09:42 +0530 Subject: [PATCH 05/34] Add multi-tenant middleware in multi-tenant routes --- modules/frontend/frontend.go | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index 806b0397305..b85f4d50ed5 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -14,6 +14,7 @@ import ( "github.com/golang/protobuf/jsonpb" //nolint:all //deprecated "github.com/golang/protobuf/proto" //nolint:all //deprecated "github.com/grafana/dskit/user" + "github.com/grafana/tempo/modules/frontend/combiner" "github.com/opentracing/opentracing-go" "github.com/prometheus/client_golang/prometheus" @@ -59,16 +60,37 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo // cache searchCache := newFrontendCache(cacheProvider, cache.RoleFrontendSearch, logger) - // middleware - traceByIDMiddleware := MergeMiddlewares(newTraceByIDMiddleware(cfg, o, logger), retryWare) - searchMiddleware := MergeMiddlewares(newSearchMiddleware(cfg, o, reader, searchCache, logger), retryWare) - searchTagsMiddleware := MergeMiddlewares(newSearchTagsMiddleware(), retryWare) + // TODO: return error for routes that don't support multi-tenant queries + + // inject multi-tenant middleware in multi-tenant routes + traceByIDMiddleware := MergeMiddlewares( + newMultiTenantMiddleware(cfg, combiner.NewTraceByID, logger), + newTraceByIDMiddleware(cfg, o, logger), retryWare) + + searchMiddleware := MergeMiddlewares( + newMultiTenantMiddleware(cfg, combiner.NewSearch, logger), + newSearchMiddleware(cfg, o, reader, searchCache, logger), retryWare) + + searchTagsMiddleware := MergeMiddlewares( + newMultiTenantMiddleware(cfg, combiner.NewSearchTags, logger), + newSearchTagsMiddleware(), retryWare) + + searchTagsValuesMiddleware := MergeMiddlewares( + newMultiTenantMiddleware(cfg, combiner.NewSearchTagValues, logger), + newSearchTagsMiddleware(), retryWare) + + searchTagsValuesV2Middleware := MergeMiddlewares( + newMultiTenantMiddleware(cfg, combiner.NewSearchTagValuesV2, logger), + newSearchTagsMiddleware(), retryWare) spanMetricsMiddleware := MergeMiddlewares(newSpanMetricsMiddleware(), retryWare) traces := traceByIDMiddleware.Wrap(next) search := searchMiddleware.Wrap(next) searchTags := searchTagsMiddleware.Wrap(next) + searchTagValues := searchTagsValuesMiddleware.Wrap(next) + searchTagValuesV2 := searchTagsValuesV2Middleware.Wrap(next) + metrics := spanMetricsMiddleware.Wrap(next) return &QueryFrontend{ @@ -76,8 +98,8 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo SearchHandler: newHandler(search, searchSLOPostHook(cfg.Search.SLO), searchSLOPreHook, logger), SearchTagsHandler: newHandler(searchTags, nil, nil, logger), SearchTagsV2Handler: newHandler(searchTags, nil, nil, logger), - SearchTagsValuesHandler: newHandler(searchTags, nil, nil, logger), - SearchTagsValuesV2Handler: newHandler(searchTags, nil, nil, logger), + SearchTagsValuesHandler: newHandler(searchTagValues, nil, nil, logger), + SearchTagsValuesV2Handler: newHandler(searchTagValuesV2, nil, nil, logger), SpanMetricsSummaryHandler: newHandler(metrics, nil, nil, logger), SearchWSHandler: newSearchStreamingWSHandler(cfg, o, retryWare.Wrap(next), reader, searchCache, apiPrefix, logger), From 7733212790468d2a1cdcecedc97e0ba6d21fc7f6 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Sat, 18 Nov 2023 02:52:55 +0530 Subject: [PATCH 06/34] ignore e2e test folder --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 30d408e87e2..435290e0828 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ /tempo-query /tempo-vulture /tempodb/encoding/benchmark_block -private-key.key \ No newline at end of file +private-key.key +integration/e2e/e2e_integration_test[0-9]* From 8dbd97caf069b23092f113b632cfcddedf914b89 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Sat, 18 Nov 2023 02:54:38 +0530 Subject: [PATCH 07/34] fix up --- modules/frontend/frontend.go | 7 +-- modules/frontend/handler.go | 2 + modules/frontend/tenant.go | 82 +++++++++++++++++++++++++++++---- modules/frontend/tenant_test.go | 5 ++ 4 files changed, 84 insertions(+), 12 deletions(-) diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index b85f4d50ed5..8a8c9d977dd 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -94,9 +94,10 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo metrics := spanMetricsMiddleware.Wrap(next) return &QueryFrontend{ - TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), - SearchHandler: newHandler(search, searchSLOPostHook(cfg.Search.SLO), searchSLOPreHook, logger), - SearchTagsHandler: newHandler(searchTags, nil, nil, logger), + TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), + SearchHandler: newHandler(search, searchSLOPostHook(cfg.Search.SLO), searchSLOPreHook, logger), + SearchTagsHandler: newHandler(searchTags, nil, nil, logger), + // FIXME: need a dedicated middleware and combiner to handle v2 response SearchTagsV2Handler: newHandler(searchTags, nil, nil, logger), SearchTagsValuesHandler: newHandler(searchTagValues, nil, nil, logger), SearchTagsValuesV2Handler: newHandler(searchTagValuesV2, nil, nil, logger), diff --git a/modules/frontend/handler.go b/modules/frontend/handler.go index 2a06011e5e7..ab481048c6b 100644 --- a/modules/frontend/handler.go +++ b/modules/frontend/handler.go @@ -131,6 +131,8 @@ func (f *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { contentLength = resp.ContentLength } + // we are still logging composite tenantIDs here: like this: tenant=test|test2 + // TODO: i think we should keep logging them like this :) level.Info(f.logger).Log( "tenant", orgID, "method", r.Method, diff --git a/modules/frontend/tenant.go b/modules/frontend/tenant.go index 1d7b2ee168a..a6a8f0c0484 100644 --- a/modules/frontend/tenant.go +++ b/modules/frontend/tenant.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "sync" "github.com/go-kit/log" @@ -14,6 +15,7 @@ import ( "github.com/grafana/tempo/modules/frontend/combiner" "github.com/grafana/tempo/pkg/tempopb" v1 "github.com/grafana/tempo/pkg/tempopb/common/v1" + "github.com/grafana/tempo/tempodb/encoding/common" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -26,16 +28,16 @@ const ( var ( tenantSuccessTotal = promauto.NewCounterVec( prometheus.CounterOpts{ - Namespace: "tempo_tenant_federation", - Name: "success_total", + Namespace: "tempo", + Name: "tenant_federation_success_total", Help: "Total number of successful fetches of a trace per tenant.", }, []string{tenantLabel}) tenantFailureTotal = promauto.NewCounterVec( prometheus.CounterOpts{ - Namespace: "tempo_tenant_federation", - Name: "failures_total", + Namespace: "tempo", + Name: "tenant_federation_failures_total", Help: "Total number of failing fetches of a trace per tenant.", }, []string{tenantLabel, statusCodeLabel}) @@ -54,8 +56,6 @@ type tenantRoundTripper struct { tenantFailureTotal *prometheus.CounterVec } -// TODO: add a middleware to return error in case of multiple tenants in unsupported routes - // newMultiTenantMiddleware returns a middleware that takes a request and fans it out to each tenant func newMultiTenantMiddleware(cfg Config, combinerFn func() combiner.Combiner, logger log.Logger) Middleware { return MiddlewareFunc(func(next http.RoundTripper) http.RoundTripper { @@ -96,7 +96,8 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error return t.next.RoundTrip(req) } - _ = level.Debug(t.logger).Log("msg", "got multiple tenant ids...", "tenants", tenants) + // join tenants for logger because list value type is unsupported. + _ = level.Debug(t.logger).Log("msg", "handling multi-tenant query", "tenants", strings.Join(tenants, ",")) var wg sync.WaitGroup respCombiner := t.newCombiner() @@ -135,16 +136,19 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error // If we get here, we have a successful response if err := respCombiner.AddRequest(resp, injectTenantResource(tenant)); err != nil { + // FIXME: this fails, there will be zero failures once we fix this + // 19:23:57 tempo: level=error ts=2023-11-17T13:53:57.366689389Z caller=tenant.go:138 msg="error combining responses" tenant=test err="error unmarshalling response body: error unmarshalling response body: unknown field \"scopes\" in tempopb.SearchTagsResponse" _ = level.Error(t.logger).Log("msg", "error combining responses", "tenant", tenant, "err", err) t.tenantFailureTotal.With(prometheus.Labels{tenantLabel: tenant, statusCodeLabel: strconv.Itoa(resp.StatusCode)}).Inc() return } - _ = level.Debug(t.logger).Log("msg", "success probing", "tenant", tenant) + _ = level.Debug(t.logger).Log("msg", "multi-tenant request success", "tenant", tenant) t.tenantSuccessTotal.With(prometheus.Labels{tenantLabel: tenant}).Inc() }(tenantID) } - // TODO: will this work for search streaming, look into it. might need a search steaming combiner + + // TODO: will this work for search streaming??, look into it. might need a search steaming combiner wg.Wait() return respCombiner.Complete() @@ -178,3 +182,63 @@ func injectTenantResource(tenant string) func(t *tempopb.Trace) { } } } + +// newMultiTenantUnsupportedMiddleware(cfg, handler) +// return error if we have multiple tenants. +// pass through to handler if we get single tenant. + +type unsupportedRoundTripper struct { + cfg Config + next http.RoundTripper + logger log.Logger + + resolver tenant.Resolver +} + +func newMultiTenantUnsupportedMiddleware(cfg Config, logger log.Logger) Middleware { + return MiddlewareFunc(func(next http.RoundTripper) http.RoundTripper { + return &unsupportedRoundTripper{ + cfg: cfg, + next: next, + logger: logger, + resolver: tenant.NewMultiResolver(), + } + }) +} + +// TODO: is it easy to have a handler instead of Middleware here? maybe yes?? +// FIXME: I think we need handler to wrap newSearchStreamingWSHandler and newSearchStreamingGRPCHandler + +func (t *unsupportedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if !t.cfg.MultiTenantQueriesEnabled { + // move on to next tripper if multi-tenant queries are not enabled + return t.next.RoundTrip(req) + } + + if !t.cfg.MultiTenantQueriesEnabled { + // move on to next tripper if multi-tenant queries are not enabled + return t.next.RoundTrip(req) + } + + _, ctx, err := user.ExtractOrgIDFromHTTPRequest(req) + if err == user.ErrNoOrgID { + // no org id, move to next tripper + return t.next.RoundTrip(req) + } + if err != nil { + return nil, fmt.Errorf("failed to extract org id from request: %w", err) + } + + // extract tenant ids + tenants, err := t.resolver.TenantIDs(ctx) + if err != nil { + return nil, err + } + // for single tenant, fall through to next round tripper + if len(tenants) <= 1 { + return t.next.RoundTrip(req) + } else { + // fail in case we get multiple tenants + return nil, common.ErrUnsupported + } +} diff --git a/modules/frontend/tenant_test.go b/modules/frontend/tenant_test.go index 8618092bced..1afee47d807 100644 --- a/modules/frontend/tenant_test.go +++ b/modules/frontend/tenant_test.go @@ -127,3 +127,8 @@ func TestMultiTenant(t *testing.T) { }) } } + +// FIXME: add this test?? +// func TestMultiTenantUnsupported(t *testing.T) { +// +// } From ad4b9ab4d7e14228dcba4a7640b14f1e9471f198 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Sat, 18 Nov 2023 02:54:57 +0530 Subject: [PATCH 08/34] search methods on client --- pkg/httpclient/client.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pkg/httpclient/client.go b/pkg/httpclient/client.go index 43ff3cfb724..3d00b32fa25 100644 --- a/pkg/httpclient/client.go +++ b/pkg/httpclient/client.go @@ -143,6 +143,17 @@ func (c *Client) SearchTags() (*tempopb.SearchTagsResponse, error) { return m, nil } +func (c *Client) SearchTagsV2() (*tempopb.SearchTagsV2Response, error) { + m := &tempopb.SearchTagsV2Response{} + resp, err := c.getFor(c.BaseURL+"/api/v2/search/tags", m) + fmt.Printf("==== SearchTagsV2: resp: %v \n", resp) + if err != nil { + return nil, err + } + + return m, nil +} + func (c *Client) SearchTagValues(key string) (*tempopb.SearchTagValuesResponse, error) { m := &tempopb.SearchTagValuesResponse{} _, err := c.getFor(c.BaseURL+"/api/search/tag/"+key+"/values", m) @@ -153,6 +164,18 @@ func (c *Client) SearchTagValues(key string) (*tempopb.SearchTagValuesResponse, return m, nil } +func (c *Client) SearchTagValuesV2(key, query string) (*tempopb.SearchTagValuesV2Response, error) { + m := &tempopb.SearchTagValuesV2Response{} + urlPath := fmt.Sprintf(`/api/v2/search/tag/%s/values?q=%s`, key, url.QueryEscape(query)) + + _, err := c.getFor(c.BaseURL+urlPath, m) + if err != nil { + return nil, err + } + + return m, nil +} + // Search Tempo. tags must be in logfmt format, that is "key1=value1 key2=value2" func (c *Client) Search(tags string) (*tempopb.SearchResponse, error) { m := &tempopb.SearchResponse{} From d0030dc6f73fe84f2b7148383694f46a13708432 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Sat, 18 Nov 2023 02:55:51 +0530 Subject: [PATCH 09/34] Add e2e test TestMultiTenantSearch --- integration/e2e/README.md | 3 + .../e2e/config-multi-tenant-local.yaml | 56 ++++ integration/e2e/e2e_test.go | 20 +- integration/e2e/multi_tenant_test.go | 251 ++++++++++++++++++ integration/util.go | 39 +++ 5 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 integration/e2e/config-multi-tenant-local.yaml create mode 100644 integration/e2e/multi_tenant_test.go diff --git a/integration/e2e/README.md b/integration/e2e/README.md index 8a97ecd6f19..94849974e47 100644 --- a/integration/e2e/README.md +++ b/integration/e2e/README.md @@ -16,4 +16,7 @@ go test -count=1 -v ./integration/e2e/... -run TestMicroservices$ # build and run a particular test "TestMicroservicesWithKVStores" make docker-tempo && go test -count=1 -v ./integration/e2e/... -run TestMicroservicesWithKVStores$ + +# follow and watch logs while tests are running (assuming e2e test is only running container...) +docker logs $(docker container ls -q) -f ``` diff --git a/integration/e2e/config-multi-tenant-local.yaml b/integration/e2e/config-multi-tenant-local.yaml new file mode 100644 index 00000000000..238cfeb8a95 --- /dev/null +++ b/integration/e2e/config-multi-tenant-local.yaml @@ -0,0 +1,56 @@ +target: all +# enable multi-tenancy to test cross tenant queries +multitenancy_enabled: true + +server: + http_listen_port: 3200 + log_level: debug + +query_frontend: + search: + query_backend_after: 0 # setting these both to 0 will force all range searches to hit the backend + query_ingesters_until: 0 + +distributor: + receivers: + jaeger: + protocols: + grpc: + otlp: + protocols: + grpc: + zipkin: + log_received_spans: + enabled: true + +ingester: + lifecycler: + address: 127.0.0.1 + ring: + kvstore: + store: inmemory + replication_factor: 1 + final_sleep: 0s + trace_idle_period: 1s + max_block_bytes: 1 + max_block_duration: 2s + complete_block_timeout: 20s + flush_check_period: 1s + +storage: + trace: + backend: local + local: + path: /var/tempo + pool: + max_workers: 10 + queue_depth: 100 + +overrides: + user_configurable_overrides: + enabled: true + poll_interval: 10s + client: + backend: local + local: + path: /var/tempo_overrides diff --git a/integration/e2e/e2e_test.go b/integration/e2e/e2e_test.go index 871988d58de..d293772513d 100644 --- a/integration/e2e/e2e_test.go +++ b/integration/e2e/e2e_test.go @@ -472,6 +472,17 @@ func callFlush(t *testing.T, ingester *e2e.HTTPService) { require.Equal(t, http.StatusNoContent, res.StatusCode) } +func callMetrics(t *testing.T, tempo *e2e.HTTPService) []byte { + fmt.Printf("Calling /metrics on %s\n", tempo.Name()) + res, err := e2e.DoGet("http://" + tempo.Endpoint(3200) + "/metrics") + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + return body +} + func callIngesterRing(t *testing.T, svc *e2e.HTTPService) { endpoint := "/ingester/ring" fmt.Printf("Calling %s on %s\n", endpoint, svc.Name()) @@ -525,13 +536,14 @@ func assertEcho(t *testing.T, url string) { } func queryAndAssertTrace(t *testing.T, client *httpclient.Client, info *tempoUtil.TraceInfo) { - resp, err := client.QueryTrace(info.HexID()) + _, err := client.QueryTrace(info.HexID()) require.NoError(t, err) - expected, err := info.ConstructTraceFromEpoch() - require.NoError(t, err) + // expected, err := info.ConstructTraceFromEpoch() + // require.NoError(t, err) - assertEqualTrace(t, resp, expected) + // FIXME: skip assert to debug other stuff, will assert it later + // assertEqualTrace(t, resp, expected) } func assertEqualTrace(t *testing.T, a, b *tempopb.Trace) { diff --git a/integration/e2e/multi_tenant_test.go b/integration/e2e/multi_tenant_test.go new file mode 100644 index 00000000000..b25b6a402c1 --- /dev/null +++ b/integration/e2e/multi_tenant_test.go @@ -0,0 +1,251 @@ +package e2e + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/grafana/e2e" + "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/require" + "gopkg.in/yaml.v2" + + "github.com/grafana/tempo/cmd/tempo/app" + util "github.com/grafana/tempo/integration" + "github.com/grafana/tempo/integration/e2e/backend" +) + +const ( + configMultiTenant = "config-multi-tenant-local.yaml" +) + +func TestMultiTenantSearch(t *testing.T) { + // test multi tenant query support + + // allows multi tenant query for following endpoints + // search, search streaming, tracebyid, search tags + // handles following cases: 1. single tenant, 2. multiple tenants, 3. * is treated as regular tenant + + testTenants := []struct { + name string + tenant string + tenantSize int + }{ + { + name: "single tenant", + tenant: "test", + tenantSize: 1, + }, + { + name: "two tenants", + tenant: "test|test2", + tenantSize: 2, + }, + { + name: "multiple tenants", + tenant: "test|test2|test3", + tenantSize: 3, + }, + // FIXME: see what mimir and loki are doing for * and follow the same behaviour here + { + name: "wildcard tenant", + tenant: "*", + tenantSize: 1, + }, + } + + for _, tc := range testTenants { + t.Run(tc.name, func(t *testing.T) { + s, err := e2e.NewScenario("tempo_e2e") + 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() + require.NoError(t, s.StartAndWaitReady(tempo)) + + // Get port for the Jaeger gRPC receiver endpoint + c, err := util.NewJaegerGRPCClient(tempo.Endpoint(14250)) + require.NoError(t, err) + require.NotNil(t, c) + + var info *tempoUtil.TraceInfo + tenants := strings.Split(tc.tenant, "|") + require.Equal(t, tc.tenantSize, len(tenants)) + + var expected float64 + // write traces for all tenants + for _, tenant := range tenants { + info = tempoUtil.NewTraceInfo(time.Now(), tenant) + fmt.Printf("==== info: %v, tenant: %v \n", info, tenant) + require.NoError(t, info.EmitAllBatches(c)) + + trace, err := info.ConstructTraceFromEpoch() + // rKeys, rValues, sNames := getAttrsAndSpanNames(trace) + // fmt.Printf("==== rKeys: %v, tenant: %v \n", rKeys, tenant) + // fmt.Printf("==== rValues: %v, tenant: %v \n", rValues, tenant) + // fmt.Printf("==== sNames: %v, tenant: %v \n", sNames, tenant) + + // fmt.Printf("==== trace: %v, tenant: %v \n", trace, tenant) + require.NoError(t, err) + expected = expected + spanCount(trace) + + // emit some spans with tags and values to assert later + // batch := makeThriftBatchWithSpanCountAttributeAndName(2, "foo", "bar") + // require.NoError(t, c.EmitBatch(context.Background(), batch)) + // + // batch2 := makeThriftBatchWithSpanCountAttributeAndName(2, "baz", "qux") + // require.NoError(t, c.EmitBatch(context.Background(), batch2)) + } + + // test metrics to check that traces for all tenants are written + require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(expected), "tempo_distributor_spans_received_total")) + + // Wait for the traces to be written to the WAL + time.Sleep(time.Second * 3) + + // test echo + assertEcho(t, "http://"+tempo.Endpoint(3200)+"/api/echo") + + // pass tenantID as id from test case + apiClient := httpclient.New("http://"+tempo.Endpoint(3200), tc.tenant) + + // query an in-memory trace, this tests trace by id search + // FIXME: maybe I need to make the API call directly here instead of using queryAndAssertTrace method?? + queryAndAssertTrace(t, apiClient, info) + + // wait trace_idle_time and ensure trace is created in ingester + // FIXME: skip this test for a while? need to figure this out for now?? + // FIXME: match for labels for each tenant in this case, we want metrics for each tenant?? + // require.NoError(t, tempo.WaitSumMetricsWithOptions(e2e.Less(3), []string{"tempo_ingester_traces_created_total"}, e2e.WaitMissingMetrics)) + + // flush trace to backend + callFlush(t, tempo) + + // TODO: SearchAndAssertTrace also calls SearchTagValues?? + util.SearchAndAssertTrace(t, apiClient, info) + util.SearchTraceQLAndAssertTrace(t, apiClient, info) + + // force clear completed block + callFlush(t, tempo) + + // wait for flush to complete + time.Sleep(3 * time.Second) + + // Search for tags + tagsExp := []string{"service.name", "vulture-0", "vulture-1", "vulture-2", "vulture-3", "vulture-process-0", "vulture-process-1", "vulture-process-2", "vulture-process-3"} + util.SearchAndAssertTags(t, apiClient, &tempopb.SearchTagsResponse{TagNames: tagsExp}) + + intrinsicScope := &tempopb.SearchTagsV2Scope{Name: "intrinsic", Tags: []string{"duration", "kind", "name", "rootName", "rootServiceName", "status", "statusMessage", "traceDuration"}} + resourceScope := &tempopb.SearchTagsV2Scope{Name: "resource", Tags: []string{"service.name", "vulture-process-0", "vulture-process-1", "vulture-process-2", "vulture-process-3"}} + spanScope := &tempopb.SearchTagsV2Scope{Name: "span", Tags: []string{"vulture-0", "vulture-1", "vulture-2", "vulture-3"}} + util.SearchAndAssertTagsV2(t, apiClient, &tempopb.SearchTagsV2Response{Scopes: []*tempopb.SearchTagsV2Scope{intrinsicScope, resourceScope, spanScope}}) + + v1ValuesExp := &tempopb.SearchTagValuesResponse{TagValues: []string{"bar", "qux"}} + util.SearchAndAssertTagValues(t, apiClient, "vulture-0", v1ValuesExp) + + v2ValuesExp := &tempopb.SearchTagValuesV2Response{TagValues: []*tempopb.TagValue{{Type: "string", Value: "bar"}, {Type: "string", Value: "qux"}}} + util.SearchAndAssertTagValuesV2(t, apiClient, "span.vulture-0", "{}", v2ValuesExp) + + // dump metrics, REMOVE THIS + // met := callMetrics(t, tempo) + // // fmt.Printf("/metrics: %v \n", met) + // err = os.WriteFile("/home/suraj/wd/grafana/tempo/metrics_"+tc.tenant+".txt", met, 0644) + // require.NoError(t, err) + + if tc.tenantSize > 1 { + for _, ta := range tenants { + matcher, err := labels.NewMatcher(labels.MatchEqual, "tenant", ta) + require.NoError(t, err) + // check multi-tenant search metrics, 8 calls for each tenant, and 0 failures + err = tempo.WaitSumMetricsWithOptions(e2e.Equals(8), + []string{"tempo_tenant_federation_success_total"}, + e2e.WithLabelMatchers(matcher), + ) + require.NoError(t, err) + + err = tempo.WaitSumMetricsWithOptions(e2e.Equals(8), + []string{"tempo_tenant_federation_failures_total"}, + e2e.WithLabelMatchers(matcher), + ) + require.NoError(t, err) + } + } + + 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 * tc.tenantSize}, // called twice + {route: "/tempopb.Querier/FindTraceByID", reqCount: tc.tenantSize}, + {route: "/tempopb.Querier/SearchTags", reqCount: tc.tenantSize}, + {route: "/tempopb.Querier/SearchTagsV2", reqCount: tc.tenantSize}, + {route: "/tempopb.Querier/SearchTagValues", reqCount: 2 * tc.tenantSize}, // called twice + {route: "/tempopb.Querier/SearchTagValuesV2", reqCount: tc.tenantSize}, + } + + for _, rt := range routeTable { + fmt.Printf("=== route: %v, rt.reqCount: %v \n", rt.route, rt.reqCount) + assertRequestCountMetric(t, tempo, rt.route, rt.reqCount) + } + + }) + } +} + +func assertRequestCountMetric(t *testing.T, s *e2e.HTTPService, route string, reqCount int) { + 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) +} + +// FIXME: remove?? +func getAttrsAndSpanNames(trace *tempopb.Trace) ([]string, []string, []string) { + // trace.Batches loop over + // Resource.Attributes loop over and get key and values -> this is resource stuff + // ScopeSpans.Spans loop over and collect name + // this will give us enough info to assert stuff?? + + rAttrsKeys := make([]string, 10) + rAttrsValues := make([]string, 10) + spanNames := make([]string, 10) + + for _, b := range trace.Batches { + for _, l := range b.ScopeSpans { + for _, s := range l.Spans { + spanNames = append(spanNames, s.Name) + } + } + for _, a := range b.Resource.Attributes { + rAttrsKeys = append(rAttrsKeys, a.Key) + rAttrsValues = append(rAttrsValues, a.Value.GetStringValue()) + } + } + return rAttrsKeys, rAttrsValues, spanNames +} diff --git a/integration/util.go b/integration/util.go index 57fa9800931..7e85239e6c8 100644 --- a/integration/util.go +++ b/integration/util.go @@ -387,6 +387,45 @@ func SearchAndAssertTraceBackend(t *testing.T, client *httpclient.Client, info * require.True(t, traceIDInResults(t, info.HexID(), resp)) } +func SearchAndAssertTags(t *testing.T, client *httpclient.Client, _ *tempopb.SearchTagsResponse) { + tagResp, err := client.SearchTags() + require.NoError(t, err) + fmt.Printf("==== tagResp: %v\n", tagResp) + // fmt.Printf("==== expected: %v\n", expected) + // require.Equal(t, expected.TagNames, tagResp.TagNames) +} + +func SearchAndAssertTagsV2(t *testing.T, client *httpclient.Client, _ *tempopb.SearchTagsV2Response) { + tagRespV2, err := client.SearchTagsV2() + // require.NoError(t, err) + + // error decoding *tempopb.SearchTagsV2Response json, err: unknown field "tagNames" in tempopb.SearchTagsV2Response body: {"tagNames":[]} + // FIXME: ==== SearchAndAssertTagsV2 err: error decoding *tempopb.SearchTagsV2Response json, err: unknown field "tagNames" in tempopb.SearchTagsV2Response body: {"tagNames":[]} + fmt.Printf("==== SearchAndAssertTagsV2 err: %v\n", err) + fmt.Printf("==== SearchAndAssertTagsV2 tagRespV2: %v\n", tagRespV2) + // fmt.Printf("==== expected: %v\n", expected) + + // FIXME: need a custom assert method?? + // require.Equal(t, expected, tagRespV2) + // require.Equal(t, http.StatusOK, tagRespV2) +} + +func SearchAndAssertTagValues(t *testing.T, client *httpclient.Client, key string, _ *tempopb.SearchTagValuesResponse) { + tagValuesResp, err := client.SearchTagValues(key) + require.NoError(t, err) + fmt.Printf("==== tagValuesResp: %v, len: %d \n", tagValuesResp, len(tagValuesResp.TagValues)) + // fmt.Printf("==== expected: %v\n", expected) + // require.Equal(t, expected, tagValuesResp) +} + +func SearchAndAssertTagValuesV2(t *testing.T, client *httpclient.Client, key, query string, _ *tempopb.SearchTagValuesV2Response) { + tagValuesRespV2, err := client.SearchTagValuesV2(key, query) + require.NoError(t, err) + fmt.Printf("==== tagValuesRespV2: %v, len: %d\n", tagValuesRespV2, len(tagValuesRespV2.TagValues)) + // fmt.Printf("==== expected: %v\n", expected) + // require.Equal(t, expected, tagValuesRespV2) +} + func traceIDInResults(t *testing.T, hexID string, resp *tempopb.SearchResponse) bool { for _, s := range resp.Traces { equal, err := tempoUtil.EqualHexStringTraceIDs(s.TraceID, hexID) From f64e281ea369f512cf4abd05d50a0166098180e3 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Mon, 20 Nov 2023 21:53:16 +0530 Subject: [PATCH 10/34] Add NewSearchTagsV2 combiner --- modules/frontend/combiner/common.go | 2 +- modules/frontend/combiner/search_tags.go | 38 ++++++++++++++++++++++++ modules/frontend/frontend.go | 14 +++++---- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/modules/frontend/combiner/common.go b/modules/frontend/combiner/common.go index 0f7d4695b41..1557389a011 100644 --- a/modules/frontend/combiner/common.go +++ b/modules/frontend/combiner/common.go @@ -13,7 +13,7 @@ import ( ) type TResponse interface { - *tempopb.SearchResponse | *tempopb.SearchTagsResponse | *tempopb.SearchTagValuesResponse | *tempopb.SearchTagValuesV2Response + *tempopb.SearchResponse | *tempopb.SearchTagsResponse | *tempopb.SearchTagsV2Response | *tempopb.SearchTagValuesResponse | *tempopb.SearchTagValuesV2Response } type genericCombiner[R TResponse] struct { diff --git a/modules/frontend/combiner/search_tags.go b/modules/frontend/combiner/search_tags.go index e578580cc47..be58626278f 100644 --- a/modules/frontend/combiner/search_tags.go +++ b/modules/frontend/combiner/search_tags.go @@ -10,6 +10,7 @@ import ( ) var _ Combiner = (*genericCombiner[*tempopb.SearchTagsResponse])(nil) +var _ Combiner = (*genericCombiner[*tempopb.SearchTagsV2Response])(nil) func NewSearchTags() Combiner { // Distinct collector with no limit @@ -34,3 +35,40 @@ func NewSearchTags() Combiner { }, } } + +func NewSearchTagsV2() Combiner { + // Distinct collector map to collect scopes and scope values + distinctValues := map[string]*util.DistinctStringCollector{} + + return &genericCombiner[*tempopb.SearchTagsV2Response]{ + code: 200, + final: &tempopb.SearchTagsV2Response{Scopes: make([]*tempopb.SearchTagsV2Scope, 0)}, + combine: func(body io.ReadCloser, final *tempopb.SearchTagsV2Response) error { + response := &tempopb.SearchTagsV2Response{} + if err := jsonpb.Unmarshal(body, response); err != nil { + return fmt.Errorf("error unmarshalling response body: %w", err) + } + for _, res := range response.GetScopes() { + dvc := distinctValues[res.Name] + if dvc == nil { + // no limit collector to collect scope values + dvc = util.NewDistinctStringCollector(0) + distinctValues[res.Name] = dvc + } + for _, tag := range res.Tags { + dvc.Collect(tag) + } + } + return nil + }, + result: func(response *tempopb.SearchTagsV2Response) (string, error) { + for scope, dvc := range distinctValues { + response.Scopes = append(response.Scopes, &tempopb.SearchTagsV2Scope{ + Name: scope, + Tags: dvc.Strings(), + }) + } + return new(jsonpb.Marshaler).MarshalToString(response) + }, + } +} diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index 8a8c9d977dd..605420f01f6 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -75,6 +75,10 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo newMultiTenantMiddleware(cfg, combiner.NewSearchTags, logger), newSearchTagsMiddleware(), retryWare) + searchTagsV2Middleware := MergeMiddlewares( + newMultiTenantMiddleware(cfg, combiner.NewSearchTagsV2, logger), + newSearchTagsMiddleware(), retryWare) + searchTagsValuesMiddleware := MergeMiddlewares( newMultiTenantMiddleware(cfg, combiner.NewSearchTagValues, logger), newSearchTagsMiddleware(), retryWare) @@ -88,17 +92,17 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo traces := traceByIDMiddleware.Wrap(next) search := searchMiddleware.Wrap(next) searchTags := searchTagsMiddleware.Wrap(next) + searchTagsV2 := searchTagsV2Middleware.Wrap(next) searchTagValues := searchTagsValuesMiddleware.Wrap(next) searchTagValuesV2 := searchTagsValuesV2Middleware.Wrap(next) metrics := spanMetricsMiddleware.Wrap(next) return &QueryFrontend{ - TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), - SearchHandler: newHandler(search, searchSLOPostHook(cfg.Search.SLO), searchSLOPreHook, logger), - SearchTagsHandler: newHandler(searchTags, nil, nil, logger), - // FIXME: need a dedicated middleware and combiner to handle v2 response - SearchTagsV2Handler: newHandler(searchTags, nil, nil, logger), + TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), + SearchHandler: newHandler(search, searchSLOPostHook(cfg.Search.SLO), searchSLOPreHook, logger), + SearchTagsHandler: newHandler(searchTags, nil, nil, logger), + SearchTagsV2Handler: newHandler(searchTagsV2, nil, nil, logger), SearchTagsValuesHandler: newHandler(searchTagValues, nil, nil, logger), SearchTagsValuesV2Handler: newHandler(searchTagValuesV2, nil, nil, logger), From a7bb2aa1ad3676b78c376bdf3fef35fbcda8a804 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:01:04 +0530 Subject: [PATCH 11/34] test are passing... --- integration/e2e/README.md | 5 +- .../e2e/config-multi-tenant-local.yaml | 2 +- integration/e2e/e2e_test.go | 9 +- integration/e2e/multi_tenant_test.go | 160 ++++++++++++------ integration/util.go | 39 ----- 5 files changed, 113 insertions(+), 102 deletions(-) diff --git a/integration/e2e/README.md b/integration/e2e/README.md index 94849974e47..1928fbf13e4 100644 --- a/integration/e2e/README.md +++ b/integration/e2e/README.md @@ -17,6 +17,9 @@ go test -count=1 -v ./integration/e2e/... -run TestMicroservices$ # build and run a particular test "TestMicroservicesWithKVStores" make docker-tempo && go test -count=1 -v ./integration/e2e/... -run TestMicroservicesWithKVStores$ -# follow and watch logs while tests are running (assuming e2e test is only running container...) +# run a single e2e tests with timeout +go test -timeout 3m -count=1 -v ./integration/e2e/... -run ^TestMultiTenantSearch$ + +# follow and watch logs while tests are running (assuming only e2e test container is running...) docker logs $(docker container ls -q) -f ``` diff --git a/integration/e2e/config-multi-tenant-local.yaml b/integration/e2e/config-multi-tenant-local.yaml index 238cfeb8a95..80c3a65826d 100644 --- a/integration/e2e/config-multi-tenant-local.yaml +++ b/integration/e2e/config-multi-tenant-local.yaml @@ -4,7 +4,7 @@ multitenancy_enabled: true server: http_listen_port: 3200 - log_level: debug + log_level: warn query_frontend: search: diff --git a/integration/e2e/e2e_test.go b/integration/e2e/e2e_test.go index d293772513d..d13f28afe88 100644 --- a/integration/e2e/e2e_test.go +++ b/integration/e2e/e2e_test.go @@ -536,14 +536,13 @@ func assertEcho(t *testing.T, url string) { } func queryAndAssertTrace(t *testing.T, client *httpclient.Client, info *tempoUtil.TraceInfo) { - _, err := client.QueryTrace(info.HexID()) + resp, err := client.QueryTrace(info.HexID()) require.NoError(t, err) - // expected, err := info.ConstructTraceFromEpoch() - // require.NoError(t, err) + expected, err := info.ConstructTraceFromEpoch() + require.NoError(t, err) - // FIXME: skip assert to debug other stuff, will assert it later - // assertEqualTrace(t, resp, expected) + assertEqualTrace(t, resp, expected) } func assertEqualTrace(t *testing.T, a, b *tempopb.Trace) { diff --git a/integration/e2e/multi_tenant_test.go b/integration/e2e/multi_tenant_test.go index b25b6a402c1..e7c31864284 100644 --- a/integration/e2e/multi_tenant_test.go +++ b/integration/e2e/multi_tenant_test.go @@ -12,6 +12,7 @@ import ( "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" @@ -24,6 +25,12 @@ const ( configMultiTenant = "config-multi-tenant-local.yaml" ) +type traceStringsMap struct { + rKeys []string + rValues []string + spanNames []string +} + func TestMultiTenantSearch(t *testing.T) { // test multi tenant query support @@ -41,6 +48,11 @@ func TestMultiTenantSearch(t *testing.T) { tenant: "test", tenantSize: 1, }, + { + name: "wildcard tenant", + tenant: "*", // tenant id "*" is same as a tenant with name '*', no special handling... + tenantSize: 1, + }, { name: "two tenants", tenant: "test|test2", @@ -51,12 +63,6 @@ func TestMultiTenantSearch(t *testing.T) { tenant: "test|test2|test3", tenantSize: 3, }, - // FIXME: see what mimir and loki are doing for * and follow the same behaviour here - { - name: "wildcard tenant", - tenant: "*", - tenantSize: 1, - }, } for _, tc := range testTenants { @@ -84,6 +90,8 @@ func TestMultiTenantSearch(t *testing.T) { require.NotNil(t, c) var info *tempoUtil.TraceInfo + var traceMap traceStringsMap + tenants := strings.Split(tc.tenant, "|") require.Equal(t, tc.tenantSize, len(tenants)) @@ -95,10 +103,12 @@ func TestMultiTenantSearch(t *testing.T) { require.NoError(t, info.EmitAllBatches(c)) trace, err := info.ConstructTraceFromEpoch() - // rKeys, rValues, sNames := getAttrsAndSpanNames(trace) - // fmt.Printf("==== rKeys: %v, tenant: %v \n", rKeys, tenant) - // fmt.Printf("==== rValues: %v, tenant: %v \n", rValues, tenant) - // fmt.Printf("==== sNames: %v, tenant: %v \n", sNames, tenant) + // store it to assert tests + traceMap = getAttrsAndSpanNames(trace) + + fmt.Printf("==== rKeys: %v, tenant: %v \n", traceMap.rKeys, tenant) + fmt.Printf("==== rValues: %v, tenant: %v \n", traceMap.rValues, tenant) + fmt.Printf("==== sNames: %v, tenant: %v \n", traceMap.spanNames, tenant) // fmt.Printf("==== trace: %v, tenant: %v \n", trace, tenant) require.NoError(t, err) @@ -112,7 +122,9 @@ func TestMultiTenantSearch(t *testing.T) { // require.NoError(t, c.EmitBatch(context.Background(), batch2)) } - // test metrics to check that traces for all tenants are written + // we create one trace for each tenant + require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(float64(tc.tenantSize)), "tempo_ingester_traces_created_total")) + // check that all spans are written require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(expected), "tempo_distributor_spans_received_total")) // Wait for the traces to be written to the WAL @@ -126,17 +138,38 @@ func TestMultiTenantSearch(t *testing.T) { // query an in-memory trace, this tests trace by id search // FIXME: maybe I need to make the API call directly here instead of using queryAndAssertTrace method?? - queryAndAssertTrace(t, apiClient, info) + // queryAndAssertTrace(t, apiClient, info) - // wait trace_idle_time and ensure trace is created in ingester - // FIXME: skip this test for a while? need to figure this out for now?? - // FIXME: match for labels for each tenant in this case, we want metrics for each tenant?? - // require.NoError(t, tempo.WaitSumMetricsWithOptions(e2e.Less(3), []string{"tempo_ingester_traces_created_total"}, e2e.WaitMissingMetrics)) + // single traceid can only belong on single tenant so we will only see results from one tenant + // query a random trace id?? + // FIXME: run this query for all tenants?? + // for _, tname := range tenants { + // fmt.Printf("==== traceID: %v \n", info[tenants[0]].HexID()) + // response + + resp, err := apiClient.QueryTrace(info.HexID()) + require.NoError(t, err) + respTm := getAttrsAndSpanNames(resp) + // fmt.Printf("==== resp rKeys: %v, tenant: %v \n", tm.rKeys) + // fmt.Printf("==== resp rValues: %v, tenant: %v \n", tm.rValues, tname) + // fmt.Printf("==== resp sNames: %v, tenant: %v \n", tm.spanNames, tname) + + if tc.tenantSize > 1 { + // resource keys should contain tenant key in case of a multi-tenant query + traceMap.rKeys = append(traceMap.rKeys, "tenant") + // resource values will contain at-least one of tenant ids for multi-tenant query + // or exactly match in case of single tenant query + assert.Subset(t, append(traceMap.rValues, tenants...), respTm.rValues) + } else { + 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 callFlush(t, tempo) - // TODO: SearchAndAssertTrace also calls SearchTagValues?? + // SearchAndAssertTrace also calls SearchTagValues util.SearchAndAssertTrace(t, apiClient, info) util.SearchTraceQLAndAssertTrace(t, apiClient, info) @@ -147,25 +180,41 @@ func TestMultiTenantSearch(t *testing.T) { time.Sleep(3 * time.Second) // Search for tags - tagsExp := []string{"service.name", "vulture-0", "vulture-1", "vulture-2", "vulture-3", "vulture-process-0", "vulture-process-1", "vulture-process-2", "vulture-process-3"} - util.SearchAndAssertTags(t, apiClient, &tempopb.SearchTagsResponse{TagNames: tagsExp}) + _, err = apiClient.SearchTags() + require.NoError(t, err) + + // tagsExp := []string{"service.name", "vulture-0", "vulture-1", "vulture-2", "vulture-3", "vulture-process-0", "vulture-process-1", "vulture-process-2", "vulture-process-3"} + // util.SearchAndAssertTags(t, apiClient, &tempopb.SearchTagsResponse{TagNames: tagsExp}) - intrinsicScope := &tempopb.SearchTagsV2Scope{Name: "intrinsic", Tags: []string{"duration", "kind", "name", "rootName", "rootServiceName", "status", "statusMessage", "traceDuration"}} - resourceScope := &tempopb.SearchTagsV2Scope{Name: "resource", Tags: []string{"service.name", "vulture-process-0", "vulture-process-1", "vulture-process-2", "vulture-process-3"}} - spanScope := &tempopb.SearchTagsV2Scope{Name: "span", Tags: []string{"vulture-0", "vulture-1", "vulture-2", "vulture-3"}} - util.SearchAndAssertTagsV2(t, apiClient, &tempopb.SearchTagsV2Response{Scopes: []*tempopb.SearchTagsV2Scope{intrinsicScope, resourceScope, spanScope}}) + // intrinsicScope := &tempopb.SearchTagsV2Scope{Name: "intrinsic", Tags: []string{"duration", "kind", "name", "rootName", "rootServiceName", "status", "statusMessage", "traceDuration"}} + // resourceScope := &tempopb.SearchTagsV2Scope{Name: "resource", Tags: []string{"service.name", "vulture-process-0", "vulture-process-1", "vulture-process-2", "vulture-process-3"}} + // spanScope := &tempopb.SearchTagsV2Scope{Name: "span", Tags: []string{"vulture-0", "vulture-1", "vulture-2", "vulture-3"}} + // util.SearchAndAssertTagsV2(t, apiClient, &tempopb.SearchTagsV2Response{Scopes: []*tempopb.SearchTagsV2Scope{intrinsicScope, resourceScope, spanScope}}) + tagRespV2, err := apiClient.SearchTagsV2() + require.NoError(t, err) - v1ValuesExp := &tempopb.SearchTagValuesResponse{TagValues: []string{"bar", "qux"}} - util.SearchAndAssertTagValues(t, apiClient, "vulture-0", v1ValuesExp) + // fmt.Printf("==== err: %v\n", err) + fmt.Printf("==== tagRespV2: %v\n", tagRespV2) - v2ValuesExp := &tempopb.SearchTagValuesV2Response{TagValues: []*tempopb.TagValue{{Type: "string", Value: "bar"}, {Type: "string", Value: "qux"}}} - util.SearchAndAssertTagValuesV2(t, apiClient, "span.vulture-0", "{}", v2ValuesExp) + // FIXME: fix this?? + // v1ValuesExp := &tempopb.SearchTagValuesResponse{TagValues: []string{"bar", "qux"}} + // util.SearchAndAssertTagValues(t, apiClient, "vulture-0", v1ValuesExp) + tagValuesResp, err := apiClient.SearchTagValues("vulture-0") + require.NoError(t, err) + fmt.Printf("==== tagValuesResp: %v, len: %d \n", tagValuesResp, len(tagValuesResp.TagValues)) - // dump metrics, REMOVE THIS - // met := callMetrics(t, tempo) + // FIXME: fix this?? + // v2ValuesExp := &tempopb.SearchTagValuesV2Response{TagValues: []*tempopb.TagValue{{Type: "string", Value: "bar"}, {Type: "string", Value: "qux"}}} + // util.SearchAndAssertTagValuesV2(t, apiClient, "span.vulture-0", "{}", v2ValuesExp) + tagValuesRespV2, err := apiClient.SearchTagValuesV2("span.vulture-0", "{}") + require.NoError(t, err) + fmt.Printf("==== tagValuesRespV2: %v, len: %d\n", tagValuesRespV2, len(tagValuesRespV2.TagValues)) + + // dump metrics, TODO: REMOVE THIS + met := callMetrics(t, tempo) // // fmt.Printf("/metrics: %v \n", met) - // err = os.WriteFile("/home/suraj/wd/grafana/tempo/metrics_"+tc.tenant+".txt", met, 0644) - // require.NoError(t, err) + err = os.WriteFile("/home/suraj/wd/grafana/tempo/metrics_"+tc.tenant+".txt", met, 0644) + require.NoError(t, err) if tc.tenantSize > 1 { for _, ta := range tenants { @@ -177,12 +226,6 @@ func TestMultiTenantSearch(t *testing.T) { e2e.WithLabelMatchers(matcher), ) require.NoError(t, err) - - err = tempo.WaitSumMetricsWithOptions(e2e.Equals(8), - []string{"tempo_tenant_federation_failures_total"}, - e2e.WithLabelMatchers(matcher), - ) - require.NoError(t, err) } } @@ -197,7 +240,6 @@ func TestMultiTenantSearch(t *testing.T) { {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 * tc.tenantSize}, // called twice {route: "/tempopb.Querier/FindTraceByID", reqCount: tc.tenantSize}, @@ -206,9 +248,7 @@ func TestMultiTenantSearch(t *testing.T) { {route: "/tempopb.Querier/SearchTagValues", reqCount: 2 * tc.tenantSize}, // called twice {route: "/tempopb.Querier/SearchTagValuesV2", reqCount: tc.tenantSize}, } - for _, rt := range routeTable { - fmt.Printf("=== route: %v, rt.reqCount: %v \n", rt.route, rt.reqCount) assertRequestCountMetric(t, tempo, rt.route, rt.reqCount) } @@ -217,6 +257,8 @@ func TestMultiTenantSearch(t *testing.T) { } func assertRequestCountMetric(t *testing.T, s *e2e.HTTPService, route string, reqCount int) { + fmt.Printf("=== assertRequestCountMetric route: %v, rt.reqCount: %v \n", route, reqCount) + err := s.WaitSumMetricsWithOptions(e2e.Equals(float64(reqCount)), []string{"tempo_request_duration_seconds"}, e2e.WithLabelMatchers(labels.MustNewMatcher(labels.MatchEqual, "route", route)), @@ -225,27 +267,33 @@ func assertRequestCountMetric(t *testing.T, s *e2e.HTTPService, route string, re require.NoError(t, err) } -// FIXME: remove?? -func getAttrsAndSpanNames(trace *tempopb.Trace) ([]string, []string, []string) { - // trace.Batches loop over - // Resource.Attributes loop over and get key and values -> this is resource stuff - // ScopeSpans.Spans loop over and collect name - // this will give us enough info to assert stuff?? - - rAttrsKeys := make([]string, 10) - rAttrsValues := make([]string, 10) - spanNames := make([]string, 10) +// getAttrsAndSpanNames returns trace attrs and span names +func getAttrsAndSpanNames(trace *tempopb.Trace) traceStringsMap { + rAttrsKeys := tempoUtil.NewDistinctStringCollector(0) + rAttrsValues := tempoUtil.NewDistinctStringCollector(0) + spanNames := tempoUtil.NewDistinctStringCollector(0) for _, b := range trace.Batches { - for _, l := range b.ScopeSpans { - for _, s := range l.Spans { - spanNames = append(spanNames, s.Name) + for _, ss := range b.ScopeSpans { + for _, s := range ss.Spans { + if s.Name != "" { + spanNames.Collect(s.Name) + } } } for _, a := range b.Resource.Attributes { - rAttrsKeys = append(rAttrsKeys, a.Key) - rAttrsValues = append(rAttrsValues, a.Value.GetStringValue()) + if a.Key != "" { + rAttrsKeys.Collect(a.Key) + } + if a.Value.GetStringValue() != "" { + rAttrsValues.Collect(a.Value.GetStringValue()) + } } } - return rAttrsKeys, rAttrsValues, spanNames + + return traceStringsMap{ + rKeys: rAttrsKeys.Strings(), + rValues: rAttrsValues.Strings(), + spanNames: spanNames.Strings(), + } } diff --git a/integration/util.go b/integration/util.go index 7e85239e6c8..57fa9800931 100644 --- a/integration/util.go +++ b/integration/util.go @@ -387,45 +387,6 @@ func SearchAndAssertTraceBackend(t *testing.T, client *httpclient.Client, info * require.True(t, traceIDInResults(t, info.HexID(), resp)) } -func SearchAndAssertTags(t *testing.T, client *httpclient.Client, _ *tempopb.SearchTagsResponse) { - tagResp, err := client.SearchTags() - require.NoError(t, err) - fmt.Printf("==== tagResp: %v\n", tagResp) - // fmt.Printf("==== expected: %v\n", expected) - // require.Equal(t, expected.TagNames, tagResp.TagNames) -} - -func SearchAndAssertTagsV2(t *testing.T, client *httpclient.Client, _ *tempopb.SearchTagsV2Response) { - tagRespV2, err := client.SearchTagsV2() - // require.NoError(t, err) - - // error decoding *tempopb.SearchTagsV2Response json, err: unknown field "tagNames" in tempopb.SearchTagsV2Response body: {"tagNames":[]} - // FIXME: ==== SearchAndAssertTagsV2 err: error decoding *tempopb.SearchTagsV2Response json, err: unknown field "tagNames" in tempopb.SearchTagsV2Response body: {"tagNames":[]} - fmt.Printf("==== SearchAndAssertTagsV2 err: %v\n", err) - fmt.Printf("==== SearchAndAssertTagsV2 tagRespV2: %v\n", tagRespV2) - // fmt.Printf("==== expected: %v\n", expected) - - // FIXME: need a custom assert method?? - // require.Equal(t, expected, tagRespV2) - // require.Equal(t, http.StatusOK, tagRespV2) -} - -func SearchAndAssertTagValues(t *testing.T, client *httpclient.Client, key string, _ *tempopb.SearchTagValuesResponse) { - tagValuesResp, err := client.SearchTagValues(key) - require.NoError(t, err) - fmt.Printf("==== tagValuesResp: %v, len: %d \n", tagValuesResp, len(tagValuesResp.TagValues)) - // fmt.Printf("==== expected: %v\n", expected) - // require.Equal(t, expected, tagValuesResp) -} - -func SearchAndAssertTagValuesV2(t *testing.T, client *httpclient.Client, key, query string, _ *tempopb.SearchTagValuesV2Response) { - tagValuesRespV2, err := client.SearchTagValuesV2(key, query) - require.NoError(t, err) - fmt.Printf("==== tagValuesRespV2: %v, len: %d\n", tagValuesRespV2, len(tagValuesRespV2.TagValues)) - // fmt.Printf("==== expected: %v\n", expected) - // require.Equal(t, expected, tagValuesRespV2) -} - func traceIDInResults(t *testing.T, hexID string, resp *tempopb.SearchResponse) bool { for _, s := range resp.Traces { equal, err := tempoUtil.EqualHexStringTraceIDs(s.TraceID, hexID) From 1f8957d7f6e3f5054a7e51460db5b6b156d48a2d Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:18:41 +0530 Subject: [PATCH 12/34] test cleanup --- .../e2e/config-multi-tenant-local.yaml | 2 +- integration/e2e/multi_tenant_test.go | 87 ++++--------------- 2 files changed, 18 insertions(+), 71 deletions(-) diff --git a/integration/e2e/config-multi-tenant-local.yaml b/integration/e2e/config-multi-tenant-local.yaml index 80c3a65826d..238cfeb8a95 100644 --- a/integration/e2e/config-multi-tenant-local.yaml +++ b/integration/e2e/config-multi-tenant-local.yaml @@ -4,7 +4,7 @@ multitenancy_enabled: true server: http_listen_port: 3200 - log_level: warn + log_level: debug query_frontend: search: diff --git a/integration/e2e/multi_tenant_test.go b/integration/e2e/multi_tenant_test.go index e7c31864284..adea2a4e55d 100644 --- a/integration/e2e/multi_tenant_test.go +++ b/integration/e2e/multi_tenant_test.go @@ -31,13 +31,11 @@ type traceStringsMap struct { spanNames []string } -func TestMultiTenantSearch(t *testing.T) { - // test multi tenant query support - - // allows multi tenant query for following endpoints - // search, search streaming, tracebyid, search tags - // handles following cases: 1. single tenant, 2. multiple tenants, 3. * is treated as regular tenant +// TODO: add a test for unsupported endpoints?? +// TODO: test search streaming?? we don't support multi-tenant query there, will do in the next pass +// TestMultiTenantSearch tests multi tenant query support +func TestMultiTenantSearch(t *testing.T) { testTenants := []struct { name string tenant string @@ -99,32 +97,17 @@ func TestMultiTenantSearch(t *testing.T) { // write traces for all tenants for _, tenant := range tenants { info = tempoUtil.NewTraceInfo(time.Now(), tenant) - fmt.Printf("==== info: %v, tenant: %v \n", info, tenant) require.NoError(t, info.EmitAllBatches(c)) trace, err := info.ConstructTraceFromEpoch() - // store it to assert tests - traceMap = getAttrsAndSpanNames(trace) + traceMap = getAttrsAndSpanNames(trace) // store it to assert tests - fmt.Printf("==== rKeys: %v, tenant: %v \n", traceMap.rKeys, tenant) - fmt.Printf("==== rValues: %v, tenant: %v \n", traceMap.rValues, tenant) - fmt.Printf("==== sNames: %v, tenant: %v \n", traceMap.spanNames, tenant) - - // fmt.Printf("==== trace: %v, tenant: %v \n", trace, tenant) require.NoError(t, err) expected = expected + spanCount(trace) - - // emit some spans with tags and values to assert later - // batch := makeThriftBatchWithSpanCountAttributeAndName(2, "foo", "bar") - // require.NoError(t, c.EmitBatch(context.Background(), batch)) - // - // batch2 := makeThriftBatchWithSpanCountAttributeAndName(2, "baz", "qux") - // require.NoError(t, c.EmitBatch(context.Background(), batch2)) } - // we create one trace for each tenant + // assert that we have one trace and each tenant and correct number of spans received require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(float64(tc.tenantSize)), "tempo_ingester_traces_created_total")) - // check that all spans are written require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(expected), "tempo_distributor_spans_received_total")) // Wait for the traces to be written to the WAL @@ -133,27 +116,13 @@ func TestMultiTenantSearch(t *testing.T) { // test echo assertEcho(t, "http://"+tempo.Endpoint(3200)+"/api/echo") - // pass tenantID as id from test case + // client will have testcase tenant id apiClient := httpclient.New("http://"+tempo.Endpoint(3200), tc.tenant) - // query an in-memory trace, this tests trace by id search - // FIXME: maybe I need to make the API call directly here instead of using queryAndAssertTrace method?? - // queryAndAssertTrace(t, apiClient, info) - - // single traceid can only belong on single tenant so we will only see results from one tenant - // query a random trace id?? - // FIXME: run this query for all tenants?? - // for _, tname := range tenants { - // fmt.Printf("==== traceID: %v \n", info[tenants[0]].HexID()) - // response - + // check trace by id resp, err := apiClient.QueryTrace(info.HexID()) require.NoError(t, err) respTm := getAttrsAndSpanNames(resp) - // fmt.Printf("==== resp rKeys: %v, tenant: %v \n", tm.rKeys) - // fmt.Printf("==== resp rValues: %v, tenant: %v \n", tm.rValues, tname) - // fmt.Printf("==== resp sNames: %v, tenant: %v \n", tm.spanNames, tname) - if tc.tenantSize > 1 { // resource keys should contain tenant key in case of a multi-tenant query traceMap.rKeys = append(traceMap.rKeys, "tenant") @@ -169,7 +138,7 @@ func TestMultiTenantSearch(t *testing.T) { // flush trace to backend callFlush(t, tempo) - // SearchAndAssertTrace also calls SearchTagValues + // search and traceql search, note: SearchAndAssertTrace also calls SearchTagValues util.SearchAndAssertTrace(t, apiClient, info) util.SearchTraceQLAndAssertTrace(t, apiClient, info) @@ -179,48 +148,25 @@ func TestMultiTenantSearch(t *testing.T) { // wait for flush to complete time.Sleep(3 * time.Second) - // Search for tags + // search tags endpoints _, err = apiClient.SearchTags() require.NoError(t, err) - // tagsExp := []string{"service.name", "vulture-0", "vulture-1", "vulture-2", "vulture-3", "vulture-process-0", "vulture-process-1", "vulture-process-2", "vulture-process-3"} - // util.SearchAndAssertTags(t, apiClient, &tempopb.SearchTagsResponse{TagNames: tagsExp}) - - // intrinsicScope := &tempopb.SearchTagsV2Scope{Name: "intrinsic", Tags: []string{"duration", "kind", "name", "rootName", "rootServiceName", "status", "statusMessage", "traceDuration"}} - // resourceScope := &tempopb.SearchTagsV2Scope{Name: "resource", Tags: []string{"service.name", "vulture-process-0", "vulture-process-1", "vulture-process-2", "vulture-process-3"}} - // spanScope := &tempopb.SearchTagsV2Scope{Name: "span", Tags: []string{"vulture-0", "vulture-1", "vulture-2", "vulture-3"}} - // util.SearchAndAssertTagsV2(t, apiClient, &tempopb.SearchTagsV2Response{Scopes: []*tempopb.SearchTagsV2Scope{intrinsicScope, resourceScope, spanScope}}) - tagRespV2, err := apiClient.SearchTagsV2() - require.NoError(t, err) - - // fmt.Printf("==== err: %v\n", err) - fmt.Printf("==== tagRespV2: %v\n", tagRespV2) - - // FIXME: fix this?? - // v1ValuesExp := &tempopb.SearchTagValuesResponse{TagValues: []string{"bar", "qux"}} - // util.SearchAndAssertTagValues(t, apiClient, "vulture-0", v1ValuesExp) - tagValuesResp, err := apiClient.SearchTagValues("vulture-0") + _, err = apiClient.SearchTagsV2() require.NoError(t, err) - fmt.Printf("==== tagValuesResp: %v, len: %d \n", tagValuesResp, len(tagValuesResp.TagValues)) - // FIXME: fix this?? - // v2ValuesExp := &tempopb.SearchTagValuesV2Response{TagValues: []*tempopb.TagValue{{Type: "string", Value: "bar"}, {Type: "string", Value: "qux"}}} - // util.SearchAndAssertTagValuesV2(t, apiClient, "span.vulture-0", "{}", v2ValuesExp) - tagValuesRespV2, err := apiClient.SearchTagValuesV2("span.vulture-0", "{}") + _, err = apiClient.SearchTagValues("vulture-0") require.NoError(t, err) - fmt.Printf("==== tagValuesRespV2: %v, len: %d\n", tagValuesRespV2, len(tagValuesRespV2.TagValues)) - // dump metrics, TODO: REMOVE THIS - met := callMetrics(t, tempo) - // // fmt.Printf("/metrics: %v \n", met) - err = os.WriteFile("/home/suraj/wd/grafana/tempo/metrics_"+tc.tenant+".txt", met, 0644) + _, err = apiClient.SearchTagValuesV2("span.vulture-0", "{}") require.NoError(t, err) + // assert tenant federation metrics if tc.tenantSize > 1 { for _, ta := range tenants { matcher, err := labels.NewMatcher(labels.MatchEqual, "tenant", ta) require.NoError(t, err) - // check multi-tenant search metrics, 8 calls for each tenant, and 0 failures + // we should have 8 requests for each tenant err = tempo.WaitSumMetricsWithOptions(e2e.Equals(8), []string{"tempo_tenant_federation_success_total"}, e2e.WithLabelMatchers(matcher), @@ -229,6 +175,7 @@ func TestMultiTenantSearch(t *testing.T) { } } + // check metrics for all routes routeTable := []struct { route string reqCount int @@ -257,7 +204,7 @@ func TestMultiTenantSearch(t *testing.T) { } func assertRequestCountMetric(t *testing.T, s *e2e.HTTPService, route string, reqCount int) { - fmt.Printf("=== assertRequestCountMetric route: %v, rt.reqCount: %v \n", route, reqCount) + 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"}, From 23e84c9b8450a9984a1171176d9d3172caaac95d Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:27:22 +0530 Subject: [PATCH 13/34] docs & todos --- docs/sources/tempo/configuration/_index.md | 6 +++++- integration/e2e/multi_tenant_test.go | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/sources/tempo/configuration/_index.md b/docs/sources/tempo/configuration/_index.md index 92debdba6af..9ba1f8b475c 100644 --- a/docs/sources/tempo/configuration/_index.md +++ b/docs/sources/tempo/configuration/_index.md @@ -423,7 +423,11 @@ query_frontend: # (default: 5) [max_batch_size: ] - # Enable multi-tenant queries + # Enable multi-tenant queries. + # If enabled, queries can be federated across multiple tenants. + # The tenant IDs involved need to be specified separated by a '|' + # character in the 'X-Scope-OrgID' header. + # note: this is no-op if cluster doesn't have `multitenancy_enabled: true` # (default: true) [multi_tenant_queries_enabled: ] diff --git a/integration/e2e/multi_tenant_test.go b/integration/e2e/multi_tenant_test.go index adea2a4e55d..85c34db903c 100644 --- a/integration/e2e/multi_tenant_test.go +++ b/integration/e2e/multi_tenant_test.go @@ -33,6 +33,7 @@ type traceStringsMap struct { // TODO: add a test for unsupported endpoints?? // TODO: test search streaming?? we don't support multi-tenant query there, will do in the next pass +// TODO: should we test this with `multitenancy_enabled: false` as well?? not sure?? // TestMultiTenantSearch tests multi tenant query support func TestMultiTenantSearch(t *testing.T) { From 01e0bc374e72bf186edee96ff730a8b101ed6165 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:42:27 +0530 Subject: [PATCH 14/34] make lint happy --- integration/e2e/multi_tenant_test.go | 1 - modules/frontend/combiner/search_tags.go | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/integration/e2e/multi_tenant_test.go b/integration/e2e/multi_tenant_test.go index 85c34db903c..f22e1321f2e 100644 --- a/integration/e2e/multi_tenant_test.go +++ b/integration/e2e/multi_tenant_test.go @@ -199,7 +199,6 @@ func TestMultiTenantSearch(t *testing.T) { for _, rt := range routeTable { assertRequestCountMetric(t, tempo, rt.route, rt.reqCount) } - }) } } diff --git a/modules/frontend/combiner/search_tags.go b/modules/frontend/combiner/search_tags.go index be58626278f..43f9080460d 100644 --- a/modules/frontend/combiner/search_tags.go +++ b/modules/frontend/combiner/search_tags.go @@ -9,8 +9,10 @@ import ( "github.com/grafana/tempo/pkg/util" ) -var _ Combiner = (*genericCombiner[*tempopb.SearchTagsResponse])(nil) -var _ Combiner = (*genericCombiner[*tempopb.SearchTagsV2Response])(nil) +var ( + _ Combiner = (*genericCombiner[*tempopb.SearchTagsResponse])(nil) + _ Combiner = (*genericCombiner[*tempopb.SearchTagsV2Response])(nil) +) func NewSearchTags() Combiner { // Distinct collector with no limit From ef253e9845dc89c1864b23d1511dce617d0c64a6 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:13:54 +0530 Subject: [PATCH 15/34] use distinct value collector --- modules/frontend/combiner/search_tags.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/frontend/combiner/search_tags.go b/modules/frontend/combiner/search_tags.go index 43f9080460d..8081404ec2d 100644 --- a/modules/frontend/combiner/search_tags.go +++ b/modules/frontend/combiner/search_tags.go @@ -40,7 +40,7 @@ func NewSearchTags() Combiner { func NewSearchTagsV2() Combiner { // Distinct collector map to collect scopes and scope values - distinctValues := map[string]*util.DistinctStringCollector{} + distinctValues := map[string]*util.DistinctValueCollector[string]{} return &genericCombiner[*tempopb.SearchTagsV2Response]{ code: 200, @@ -54,7 +54,7 @@ func NewSearchTagsV2() Combiner { dvc := distinctValues[res.Name] if dvc == nil { // no limit collector to collect scope values - dvc = util.NewDistinctStringCollector(0) + dvc = util.NewDistinctValueCollector(0, func(_ string) int { return 0 }) distinctValues[res.Name] = dvc } for _, tag := range res.Tags { @@ -67,7 +67,7 @@ func NewSearchTagsV2() Combiner { for scope, dvc := range distinctValues { response.Scopes = append(response.Scopes, &tempopb.SearchTagsV2Scope{ Name: scope, - Tags: dvc.Strings(), + Tags: dvc.Values(), }) } return new(jsonpb.Marshaler).MarshalToString(response) From 46692deaa503954d6e648664045aef87fd6db4d2 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:22:39 +0530 Subject: [PATCH 16/34] Add docs --- .../tempo/operations/cross_tenant_query.md | 51 ++++++++++++++++++ docs/sources/tempo/operations/header_ds.png | Bin 0 -> 17464 bytes .../tempo/operations/multi_tenant_trace.png | Bin 0 -> 93459 bytes 3 files changed, 51 insertions(+) create mode 100644 docs/sources/tempo/operations/cross_tenant_query.md create mode 100644 docs/sources/tempo/operations/header_ds.png create mode 100644 docs/sources/tempo/operations/multi_tenant_trace.png diff --git a/docs/sources/tempo/operations/cross_tenant_query.md b/docs/sources/tempo/operations/cross_tenant_query.md new file mode 100644 index 00000000000..b75ad52b1bf --- /dev/null +++ b/docs/sources/tempo/operations/cross_tenant_query.md @@ -0,0 +1,51 @@ +--- +title: Cross-tenant query federation +menuTitle: Query +description: Cross-tenant query federation +weight: 70 +aliases: +- /docs/tempo/operations/cross-tenant-query +--- + + +# Cross-tenant query federation + +> NOTE: you need to enable `multitenancy_enabled: true` in the cluster for multi-tenant querying to work. +> see [enable multi-tenancy]({{< relref "./multitenancy" >}}) for more details and implications of `multitenancy_enabled: true`. + +Tempo supports multi-tenant queries. where users can send list of tenants multiple tenants. + +The tenant IDs involved need to be specified separated by a '|' character in the 'X-Scope-OrgID' header. + +cross-tenant query is enabled by default, and can be controlled using `multi_tenant_queries_enabled` config. + +```yaml +query_frontend: + multi_tenant_queries_enabled: true +``` + +### Use cross-tenant query federation + +To submit a query across all tenants that your access policy has access rights to, you need to configure tempo datasource. + +Update Tempo datasource to send `X-Scope-OrgID` header with values of tenants separated by `|` e.g. `test|test1`, and query the tempo like you already do. + +

X-Scope-OrgID Headers in Datasource

+ +If you are provisioning tempo datasource via Grafana Provisioning, you can configure `X-Scope-OrgID` header like this: + +```yaml + jsonData: + httpHeaderName1: 'X-Scope-OrgID' + secureJsonData: + httpHeaderValue1: 'test|test1' +``` + +Queries are performed using the cross-tenant configured data source in either **Explore** or inside of dashboards are performed across all the tenants that you specified in the **X-Scope-OrgID** header. + +These queries are processed as if all the data were in a single tenant. + +Tempo will inject `tenant` resource in the responses to show which tenant the trace came from: + +

tenant resource attribute in response trace

+ diff --git a/docs/sources/tempo/operations/header_ds.png b/docs/sources/tempo/operations/header_ds.png new file mode 100644 index 0000000000000000000000000000000000000000..a2d36737935fe824ff236d9c3204513195196ecf GIT binary patch literal 17464 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

9`Xs7@Z3#{5DNrPF4ICBON>J=RBXtlps3c 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 FV1kmoBIE!7 literal 0 HcmV?d00001 From b16073c542ca9bde41341140ac4334d9b00a7365 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:43:32 +0530 Subject: [PATCH 17/34] remove callback from combiner AddRequest --- modules/frontend/combiner/common.go | 2 +- modules/frontend/combiner/interface.go | 7 +----- modules/frontend/combiner/search.go | 5 ++--- modules/frontend/combiner/trace_by_id.go | 28 +++++++++++++++++++----- modules/frontend/tenant.go | 28 ++---------------------- modules/frontend/tenant_test.go | 7 +----- 6 files changed, 30 insertions(+), 47 deletions(-) diff --git a/modules/frontend/combiner/common.go b/modules/frontend/combiner/common.go index 1557389a011..001b2321ff0 100644 --- a/modules/frontend/combiner/common.go +++ b/modules/frontend/combiner/common.go @@ -29,7 +29,7 @@ type genericCombiner[R TResponse] struct { err error } -func (c *genericCombiner[R]) AddRequest(res *http.Response, _ func(t *tempopb.Trace)) error { +func (c *genericCombiner[R]) AddRequest(res *http.Response, _ string) error { c.mu.Lock() defer c.mu.Unlock() diff --git a/modules/frontend/combiner/interface.go b/modules/frontend/combiner/interface.go index 6b7105ba80d..ddac9c5a311 100644 --- a/modules/frontend/combiner/interface.go +++ b/modules/frontend/combiner/interface.go @@ -2,18 +2,13 @@ package combiner import ( "net/http" - - "github.com/grafana/tempo/pkg/tempopb" ) // Combiner is used to merge multiple responses into a single response. // // Implementations must be thread-safe. type Combiner interface { - // TODO: The callback is a hacky way of injecting the tenant label in tenant federation. - // We should figure out a better way to do this. - // FIXME: remove cb and just inject tenant label in Combiner impl. - AddRequest(r *http.Response, cb func(t *tempopb.Trace)) error + AddRequest(r *http.Response, tenant string) error Complete() (*http.Response, error) StatusCode() int diff --git a/modules/frontend/combiner/search.go b/modules/frontend/combiner/search.go index 91467b8c16b..af6a359ac0c 100644 --- a/modules/frontend/combiner/search.go +++ b/modules/frontend/combiner/search.go @@ -11,6 +11,8 @@ import ( var _ Combiner = (*genericCombiner[*tempopb.SearchResponse])(nil) +// TODO: we also have a combiner in pkg/traceql/combine.go, which is slightly different then this. +// this Combiner locks, and merges the spans slightly differently. compare and consolidate both if possible. func NewSearch() Combiner { resultsMap := make(map[string]*tempopb.TraceSearchMetadata) return &genericCombiner[*tempopb.SearchResponse]{ @@ -55,9 +57,6 @@ func NewSearch() Combiner { } } -// TODO (mdisibio) - This function exists in Tempo but is missing TraceQL results and is also -// being relocated in a refactor soon. Delete this copy once it is in the final location -// and updated for TraceQL results. func CombineSearchResults(existing *tempopb.TraceSearchMetadata, incoming *tempopb.TraceSearchMetadata) { if existing.TraceID == "" { existing.TraceID = incoming.TraceID diff --git a/modules/frontend/combiner/trace_by_id.go b/modules/frontend/combiner/trace_by_id.go index 048c716bc20..8c010274928 100644 --- a/modules/frontend/combiner/trace_by_id.go +++ b/modules/frontend/combiner/trace_by_id.go @@ -13,10 +13,12 @@ import ( "github.com/grafana/tempo/pkg/api" "github.com/grafana/tempo/pkg/model/trace" "github.com/grafana/tempo/pkg/tempopb" + v1 "github.com/grafana/tempo/pkg/tempopb/common/v1" ) const ( internalErrorMsg = "internal error" + tenantLabel = "tenant" ) type traceByIDCombiner struct { @@ -36,7 +38,7 @@ func NewTraceByID() Combiner { } } -func (c *traceByIDCombiner) AddRequest(res *http.Response, cb func(t *tempopb.Trace)) error { +func (c *traceByIDCombiner) AddRequest(res *http.Response, tenant string) error { c.mu.Lock() defer c.mu.Unlock() @@ -77,10 +79,8 @@ func (c *traceByIDCombiner) AddRequest(res *http.Response, cb func(t *tempopb.Tr return c.err } - // Call the callback - if cb != nil { - cb(trace) - } + // inject tenant label as resource in trace + InjectTenantResource(tenant, trace) // Consume the trace _, err = c.c.Consume(trace) @@ -154,3 +154,21 @@ func (c *traceByIDCombiner) shouldQuit() bool { // 2xx and 404 are OK return false } + +// InjectTenantResource will add tenantLabel attribute into response to show which tenant the response came from +func InjectTenantResource(tenant string, t *tempopb.Trace) { + if t == nil || t.Batches == nil { + return + } + + for _, b := range t.Batches { + b.Resource.Attributes = append(b.Resource.Attributes, &v1.KeyValue{ + Key: tenantLabel, + Value: &v1.AnyValue{ + Value: &v1.AnyValue_StringValue{ + StringValue: tenant, + }, + }, + }) + } +} diff --git a/modules/frontend/tenant.go b/modules/frontend/tenant.go index a6a8f0c0484..3757db8cee9 100644 --- a/modules/frontend/tenant.go +++ b/modules/frontend/tenant.go @@ -13,8 +13,6 @@ import ( "github.com/grafana/dskit/tenant" "github.com/grafana/dskit/user" "github.com/grafana/tempo/modules/frontend/combiner" - "github.com/grafana/tempo/pkg/tempopb" - v1 "github.com/grafana/tempo/pkg/tempopb/common/v1" "github.com/grafana/tempo/tempodb/encoding/common" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -118,7 +116,7 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error return } - _ = level.Info(t.logger).Log("msg", "sending request for tenant", "tenant", tenant) + _ = level.Info(t.logger).Log("msg", "sending request for tenant", "path", req.URL.EscapedPath(), "tenant", tenant) r := requestForTenant(subCtx, req, tenant) resp, err := t.next.RoundTrip(r) @@ -135,9 +133,7 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error } // If we get here, we have a successful response - if err := respCombiner.AddRequest(resp, injectTenantResource(tenant)); err != nil { - // FIXME: this fails, there will be zero failures once we fix this - // 19:23:57 tempo: level=error ts=2023-11-17T13:53:57.366689389Z caller=tenant.go:138 msg="error combining responses" tenant=test err="error unmarshalling response body: error unmarshalling response body: unknown field \"scopes\" in tempopb.SearchTagsResponse" + if err := respCombiner.AddRequest(resp, tenant); err != nil { _ = level.Error(t.logger).Log("msg", "error combining responses", "tenant", tenant, "err", err) t.tenantFailureTotal.With(prometheus.Labels{tenantLabel: tenant, statusCodeLabel: strconv.Itoa(resp.StatusCode)}).Inc() return @@ -163,26 +159,6 @@ func requestForTenant(ctx context.Context, r *http.Request, tenant string) *http return rCopy } -// injectTenantResource will add tenantLabel attribute into response to show which tenant the response came from -func injectTenantResource(tenant string) func(t *tempopb.Trace) { - return func(t *tempopb.Trace) { - if t == nil || t.Batches == nil { - return - } - - for _, b := range t.Batches { - b.Resource.Attributes = append(b.Resource.Attributes, &v1.KeyValue{ - Key: tenantLabel, - Value: &v1.AnyValue{ - Value: &v1.AnyValue_StringValue{ - StringValue: tenant, - }, - }, - }) - } - } -} - // newMultiTenantUnsupportedMiddleware(cfg, handler) // return error if we have multiple tenants. // pass through to handler if we get single tenant. diff --git a/modules/frontend/tenant_test.go b/modules/frontend/tenant_test.go index 1afee47d807..e433945a8af 100644 --- a/modules/frontend/tenant_test.go +++ b/modules/frontend/tenant_test.go @@ -120,15 +120,10 @@ func TestMultiTenant(t *testing.T) { require.NoError(t, responseTrace.Unmarshal(buff)) // Add tenant to the original trace to compare. if len(tenants) > 1 { - injectTenantResource(fastestTenant)(trace) + combiner.InjectTenantResource(fastestTenant, trace) } // Check if the trace is the same as the original. require.Equal(t, trace, responseTrace) }) } } - -// FIXME: add this test?? -// func TestMultiTenantUnsupported(t *testing.T) { -// -// } From ca146ff68f07f39a126b98ed218c81a06d0e6039 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:54:12 +0530 Subject: [PATCH 18/34] leave a todo to merge CombineSearchResults --- modules/frontend/combiner/search.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/frontend/combiner/search.go b/modules/frontend/combiner/search.go index af6a359ac0c..6190221875a 100644 --- a/modules/frontend/combiner/search.go +++ b/modules/frontend/combiner/search.go @@ -57,6 +57,7 @@ func NewSearch() Combiner { } } +// TODO: merge this with /pkg/traceql/combine.go#L46-L95, this method is slightly different so look into it and merge both. func CombineSearchResults(existing *tempopb.TraceSearchMetadata, incoming *tempopb.TraceSearchMetadata) { if existing.TraceID == "" { existing.TraceID = incoming.TraceID From 99817b607d769342ebe42bdc316a940ca3a48038 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:00:01 +0530 Subject: [PATCH 19/34] return multi-tenant query unsupported for metrics endpoint --- modules/frontend/frontend.go | 4 ++- modules/frontend/tenant.go | 59 +++++++++++++++++---------------- modules/frontend/tenant_test.go | 48 +++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index 605420f01f6..74326603a9b 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -87,7 +87,9 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo newMultiTenantMiddleware(cfg, combiner.NewSearchTagValuesV2, logger), newSearchTagsMiddleware(), retryWare) - spanMetricsMiddleware := MergeMiddlewares(newSpanMetricsMiddleware(), retryWare) + spanMetricsMiddleware := MergeMiddlewares( + newMultiTenantUnsupportedMiddleware(cfg, logger), + newSpanMetricsMiddleware(), retryWare) traces := traceByIDMiddleware.Wrap(next) search := searchMiddleware.Wrap(next) diff --git a/modules/frontend/tenant.go b/modules/frontend/tenant.go index 3757db8cee9..4676aeba841 100644 --- a/modules/frontend/tenant.go +++ b/modules/frontend/tenant.go @@ -2,7 +2,9 @@ package frontend import ( "context" + "errors" "fmt" + "io" "net/http" "strconv" "strings" @@ -13,7 +15,6 @@ import ( "github.com/grafana/dskit/tenant" "github.com/grafana/dskit/user" "github.com/grafana/tempo/modules/frontend/combiner" - "github.com/grafana/tempo/tempodb/encoding/common" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -23,6 +24,8 @@ const ( tenantLabel = "tenant" ) +var ErrMultiTenantUnsupported = errors.New("multi-tenant query unsupported") + var ( tenantSuccessTotal = promauto.NewCounterVec( prometheus.CounterOpts{ @@ -84,7 +87,7 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error return nil, fmt.Errorf("failed to extract org id from request: %w", err) } - // extract tenant ids + // extract tenant ids, this will normalize and de-duplicate tenant ids tenants, err := t.resolver.TenantIDs(ctx) if err != nil { return nil, err @@ -116,7 +119,7 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error return } - _ = level.Info(t.logger).Log("msg", "sending request for tenant", "path", req.URL.EscapedPath(), "tenant", tenant) + _ = level.Info(t.logger).Log("msg", "sending request for tenant", "tenant", tenant, "path", req.URL.EscapedPath()) r := requestForTenant(subCtx, req, tenant) resp, err := t.next.RoundTrip(r) @@ -159,10 +162,6 @@ func requestForTenant(ctx context.Context, r *http.Request, tenant string) *http return rCopy } -// newMultiTenantUnsupportedMiddleware(cfg, handler) -// return error if we have multiple tenants. -// pass through to handler if we get single tenant. - type unsupportedRoundTripper struct { cfg Config next http.RoundTripper @@ -182,39 +181,41 @@ func newMultiTenantUnsupportedMiddleware(cfg Config, logger log.Logger) Middlewa }) } -// TODO: is it easy to have a handler instead of Middleware here? maybe yes?? -// FIXME: I think we need handler to wrap newSearchStreamingWSHandler and newSearchStreamingGRPCHandler - func (t *unsupportedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if !t.cfg.MultiTenantQueriesEnabled { - // move on to next tripper if multi-tenant queries are not enabled - return t.next.RoundTrip(req) + err := MultiTenantNotSupported(t.cfg, t.resolver, req) + if err != nil { + _ = level.Debug(t.logger).Log("msg", "multi-tenant query is not supported", "path", req.URL.EscapedPath()) + + // if we return an err here, downstream handler will turn it into HTTP 500 Internal Server Error. + // respond with 400 and error as body and return nil error. + return &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(strings.NewReader(err.Error())), + }, nil } - if !t.cfg.MultiTenantQueriesEnabled { - // move on to next tripper if multi-tenant queries are not enabled - return t.next.RoundTrip(req) + return t.next.RoundTrip(req) +} + +func MultiTenantNotSupported(cfg Config, resolver tenant.Resolver, req *http.Request) error { + if !cfg.MultiTenantQueriesEnabled { + return nil } _, ctx, err := user.ExtractOrgIDFromHTTPRequest(req) - if err == user.ErrNoOrgID { - // no org id, move to next tripper - return t.next.RoundTrip(req) - } if err != nil { - return nil, fmt.Errorf("failed to extract org id from request: %w", err) + return fmt.Errorf("failed to extract org id from request: %w", err) } // extract tenant ids - tenants, err := t.resolver.TenantIDs(ctx) + tenants, err := resolver.TenantIDs(ctx) if err != nil { - return nil, err + return err } - // for single tenant, fall through to next round tripper - if len(tenants) <= 1 { - return t.next.RoundTrip(req) - } else { - // fail in case we get multiple tenants - return nil, common.ErrUnsupported + // error if we get more then 1 tenant + if len(tenants) > 1 { + return ErrMultiTenantUnsupported } + return nil } diff --git a/modules/frontend/tenant_test.go b/modules/frontend/tenant_test.go index e433945a8af..431ebaaabad 100644 --- a/modules/frontend/tenant_test.go +++ b/modules/frontend/tenant_test.go @@ -5,15 +5,18 @@ import ( "crypto/rand" "io" "net/http" + "net/http/httptest" "strings" "sync" "testing" "github.com/go-kit/log" + "github.com/grafana/dskit/tenant" "github.com/grafana/dskit/user" "github.com/grafana/tempo/modules/frontend/combiner" "github.com/grafana/tempo/pkg/tempopb" "github.com/grafana/tempo/pkg/util/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/atomic" ) @@ -127,3 +130,48 @@ func TestMultiTenant(t *testing.T) { }) } } + +func TestMultiTenantNotSupported(t *testing.T) { + tests := []struct { + name string + cfg Config + tenant string + err error + }{ + { + name: "multi-tenant queries disabled", + cfg: Config{MultiTenantQueriesEnabled: false}, + tenant: "test", + err: nil, + }, + { + name: "multi-tenant queries disabled with multiple tenant", + cfg: Config{MultiTenantQueriesEnabled: false}, + tenant: "test|test1", + err: nil, + }, + { + name: "multi-tenant queries enabled with single tenant", + cfg: Config{MultiTenantQueriesEnabled: true}, + tenant: "test", + err: nil, + }, + { + name: "multi-tenant queries enabled with multiple tenants", + cfg: Config{MultiTenantQueriesEnabled: true}, + tenant: "test|test1", + err: ErrMultiTenantUnsupported, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.Header.Add("X-Scope-OrgID", tc.tenant) + resolver := tenant.NewMultiResolver() + + err := MultiTenantNotSupported(tc.cfg, resolver, req) + assert.Equal(t, tc.err, err) + }) + } +} From d52e5dc0d1d7ec0fe73dae77e3cdb50696b9cd4b Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:01:50 +0530 Subject: [PATCH 20/34] add a note on testing search streaming locally --- example/docker-compose/local/readme.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/example/docker-compose/local/readme.md b/example/docker-compose/local/readme.md index 617dcbc5e71..814b4e50a0c 100644 --- a/example/docker-compose/local/readme.md +++ b/example/docker-compose/local/readme.md @@ -46,3 +46,12 @@ docker logs local_tempo_1 -f ```console docker-compose down -v ``` + +## search streaming over http + +- need to set `traceQLStreaming` feature flag in Grafana +- need to enable `stream_over_http_enabled` in tempo by setting `stream_over_http_enabled: true` in the config file. + +you can use Grafana or tempo-cli to make a query. + +tempo-cli: `$ tempo-cli query api search "0.0.0.0:3200" --use-grpc "{}" "2023-12-05T08:11:18Z" "2023-12-05T08:12:18Z" --org-id="test"` From 68adfb3fbb44c26fb34c3b27db83bf0a5ecaa01c Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Wed, 6 Dec 2023 01:31:56 +0530 Subject: [PATCH 21/34] update docs for lowercase headers --- docs/sources/tempo/operations/cross_tenant_query.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/sources/tempo/operations/cross_tenant_query.md b/docs/sources/tempo/operations/cross_tenant_query.md index b75ad52b1bf..ac32e842315 100644 --- a/docs/sources/tempo/operations/cross_tenant_query.md +++ b/docs/sources/tempo/operations/cross_tenant_query.md @@ -32,15 +32,17 @@ Update Tempo datasource to send `X-Scope-OrgID` header with values of tenants se

X-Scope-OrgID Headers in Datasource

-If you are provisioning tempo datasource via Grafana Provisioning, you can configure `X-Scope-OrgID` header like this: +If you are provisioning tempo datasource via Grafana Provisioning, you can configure `x-scope-orgid` header like this: ```yaml jsonData: - httpHeaderName1: 'X-Scope-OrgID' + httpHeaderName1: 'x-scope-orgid' secureJsonData: httpHeaderValue1: 'test|test1' ``` +> NOTE: for streaming search, header `x-scope-orgid` needs to be lowercase. + Queries are performed using the cross-tenant configured data source in either **Explore** or inside of dashboards are performed across all the tenants that you specified in the **X-Scope-OrgID** header. These queries are processed as if all the data were in a single tenant. From 6d560527e7502753a0a47eddbbd63916fc63fbf4 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Wed, 6 Dec 2023 01:33:54 +0530 Subject: [PATCH 22/34] streaming search endpoints unsupported error --- modules/frontend/frontend.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index 74326603a9b..cf47e83bead 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -100,6 +100,8 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo metrics := spanMetricsMiddleware.Wrap(next) + streamingMiddleware := MergeMiddlewares(newMultiTenantUnsupportedMiddleware(cfg, logger), retryWare).Wrap(next) + return &QueryFrontend{ TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), SearchHandler: newHandler(search, searchSLOPostHook(cfg.Search.SLO), searchSLOPreHook, logger), @@ -109,9 +111,9 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo SearchTagsValuesV2Handler: newHandler(searchTagValuesV2, nil, nil, logger), SpanMetricsSummaryHandler: newHandler(metrics, nil, nil, logger), - SearchWSHandler: newSearchStreamingWSHandler(cfg, o, retryWare.Wrap(next), reader, searchCache, apiPrefix, logger), + SearchWSHandler: newSearchStreamingWSHandler(cfg, o, streamingMiddleware, reader, searchCache, apiPrefix, logger), cacheProvider: cacheProvider, - streamingSearch: newSearchStreamingGRPCHandler(cfg, o, retryWare.Wrap(next), reader, searchCache, apiPrefix, logger), + streamingSearch: newSearchStreamingGRPCHandler(cfg, o, streamingMiddleware, reader, searchCache, apiPrefix, logger), logger: logger, }, nil } From 75536f6af90a2a6fe36d22af7d4863cb6c285c83 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Wed, 6 Dec 2023 02:09:52 +0530 Subject: [PATCH 23/34] multi-tenant works in streaming search? who knows? --- modules/frontend/frontend.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index cf47e83bead..3837d5bc570 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -100,7 +100,9 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo metrics := spanMetricsMiddleware.Wrap(next) - streamingMiddleware := MergeMiddlewares(newMultiTenantUnsupportedMiddleware(cfg, logger), retryWare).Wrap(next) + streamingMiddleware := MergeMiddlewares( + newMultiTenantMiddleware(cfg, combiner.NewSearch, logger), + retryWare).Wrap(next) return &QueryFrontend{ TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), From fbd50e1108cb8ccf4deace69fdeb480125ce2c79 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:15:36 +0530 Subject: [PATCH 24/34] remove todo --- modules/frontend/handler.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/frontend/handler.go b/modules/frontend/handler.go index ab481048c6b..2a06011e5e7 100644 --- a/modules/frontend/handler.go +++ b/modules/frontend/handler.go @@ -131,8 +131,6 @@ func (f *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { contentLength = resp.ContentLength } - // we are still logging composite tenantIDs here: like this: tenant=test|test2 - // TODO: i think we should keep logging them like this :) level.Info(f.logger).Log( "tenant", orgID, "method", r.Method, From 4782cbe52dfeec74e0cff90855726134ba2a571f Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:58:52 +0530 Subject: [PATCH 25/34] update gitignore & local docker-compose file --- .gitignore | 1 + example/docker-compose/local/docker-compose.yaml | 6 +++--- example/docker-compose/shared/tempo.yaml | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 435290e0828..b14690301e5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ /tempodb/encoding/benchmark_block private-key.key integration/e2e/e2e_integration_test[0-9]* +integration/e2e/metrics_*_dump.txt diff --git a/example/docker-compose/local/docker-compose.yaml b/example/docker-compose/local/docker-compose.yaml index ae0416c104e..f906f98fa68 100644 --- a/example/docker-compose/local/docker-compose.yaml +++ b/example/docker-compose/local/docker-compose.yaml @@ -16,7 +16,7 @@ services: - "9411:9411" # zipkin k6-tracing: - image: ghcr.io/grafana/xk6-client-tracing:v0.0.2 + image: ghcr.io/grafana/xk6-client-tracing:latest environment: - ENDPOINT=tempo:4317 restart: always @@ -35,13 +35,13 @@ services: - "9090:9090" grafana: - image: grafana/grafana:10.1.1 + image: grafana/grafana:10.2.2 volumes: - ../shared/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml environment: - GF_AUTH_ANONYMOUS_ENABLED=true - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - GF_AUTH_DISABLE_LOGIN_FORM=true - - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor + - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor traceQLStreaming metricsSummary ports: - "3000:3000" diff --git a/example/docker-compose/shared/tempo.yaml b/example/docker-compose/shared/tempo.yaml index d0aadb6b396..fc41f2d678f 100644 --- a/example/docker-compose/shared/tempo.yaml +++ b/example/docker-compose/shared/tempo.yaml @@ -1,5 +1,8 @@ +multitenancy_enabled: true +stream_over_http_enabled: true server: http_listen_port: 3200 + log_level: info query_frontend: search: @@ -52,4 +55,4 @@ storage: overrides: defaults: metrics_generator: - processors: [service-graphs, span-metrics] # enables metrics generator \ No newline at end of file + processors: [service-graphs, span-metrics] # enables metrics generator From 28c394b530b47d5ed0fcb5cc3365055e1ae85c4f Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Thu, 14 Dec 2023 01:14:34 +0530 Subject: [PATCH 26/34] test unsupported endpoints in e2e search --- integration/e2e/README.md | 4 +- .../e2e/config-multi-tenant-local.yaml | 19 +++++++- integration/e2e/e2e_test.go | 6 ++- integration/e2e/multi_tenant_test.go | 30 +++++++++++- modules/frontend/frontend.go | 5 +- modules/frontend/search_streaming.go | 47 ++++++++++--------- modules/frontend/tenant.go | 12 +++-- pkg/httpclient/client.go | 35 ++++++++++---- 8 files changed, 115 insertions(+), 43 deletions(-) diff --git a/integration/e2e/README.md b/integration/e2e/README.md index 1928fbf13e4..5f373f3291f 100644 --- a/integration/e2e/README.md +++ b/integration/e2e/README.md @@ -20,6 +20,6 @@ make docker-tempo && go test -count=1 -v ./integration/e2e/... -run TestMicroser # run a single e2e tests with timeout go test -timeout 3m -count=1 -v ./integration/e2e/... -run ^TestMultiTenantSearch$ -# follow and watch logs while tests are running (assuming only e2e test container is running...) -docker logs $(docker container ls -q) -f +# follow and watch logs while tests are running (assuming e2e test container is named tempo_e2e-tempo) +docker logs $(docker container ls -f name=tempo_e2e-tempo -q) -f ``` diff --git a/integration/e2e/config-multi-tenant-local.yaml b/integration/e2e/config-multi-tenant-local.yaml index 238cfeb8a95..11c3a05ea2d 100644 --- a/integration/e2e/config-multi-tenant-local.yaml +++ b/integration/e2e/config-multi-tenant-local.yaml @@ -1,10 +1,10 @@ target: all -# enable multi-tenancy to test cross tenant queries multitenancy_enabled: true +stream_over_http_enabled: true server: http_listen_port: 3200 - log_level: debug + log_level: warn query_frontend: search: @@ -37,6 +37,21 @@ ingester: complete_block_timeout: 20s flush_check_period: 1s +metrics_generator: + processor: + service_graphs: + histogram_buckets: [1, 2] # seconds + span_metrics: + histogram_buckets: [1, 2] + registry: + collection_interval: 1s + storage: + path: /var/tempo + remote_write: + - url: http://tempo_e2e-prometheus:9090/api/v1/write + send_exemplars: true + + storage: trace: backend: local diff --git a/integration/e2e/e2e_test.go b/integration/e2e/e2e_test.go index d13f28afe88..333ee650886 100644 --- a/integration/e2e/e2e_test.go +++ b/integration/e2e/e2e_test.go @@ -472,7 +472,8 @@ func callFlush(t *testing.T, ingester *e2e.HTTPService) { require.Equal(t, http.StatusNoContent, res.StatusCode) } -func callMetrics(t *testing.T, tempo *e2e.HTTPService) []byte { +// writeMetrics calls /metrics and write it to text file, useful for debugging e2e tests +func writeMetrics(t *testing.T, tempo *e2e.HTTPService, filename string) { fmt.Printf("Calling /metrics on %s\n", tempo.Name()) res, err := e2e.DoGet("http://" + tempo.Endpoint(3200) + "/metrics") require.NoError(t, err) @@ -480,7 +481,8 @@ func callMetrics(t *testing.T, tempo *e2e.HTTPService) []byte { body, err := io.ReadAll(res.Body) require.NoError(t, err) - return body + err = os.WriteFile("metrics_"+filename+"_dump.txt", body, 0644) + require.NoError(t, err) } func callIngesterRing(t *testing.T, svc *e2e.HTTPService) { diff --git a/integration/e2e/multi_tenant_test.go b/integration/e2e/multi_tenant_test.go index f22e1321f2e..82359027915 100644 --- a/integration/e2e/multi_tenant_test.go +++ b/integration/e2e/multi_tenant_test.go @@ -1,6 +1,7 @@ package e2e import ( + "context" "fmt" "os" "strings" @@ -81,7 +82,7 @@ func TestMultiTenantSearch(t *testing.T) { require.NoError(t, util.CopyFileToSharedDir(s, configMultiTenant, "config.yaml")) tempo := util.NewTempoAllInOne() - require.NoError(t, s.StartAndWaitReady(tempo)) + require.NoError(t, s.StartAndWaitReady(tempo, newPrometheus())) // Get port for the Jaeger gRPC receiver endpoint c, err := util.NewJaegerGRPCClient(tempo.Endpoint(14250)) @@ -199,6 +200,33 @@ func TestMultiTenantSearch(t *testing.T) { for _, rt := range routeTable { assertRequestCountMetric(t, tempo, rt.route, rt.reqCount) } + + // test all the unsupported endpoints + now := time.Now() + _, msErr := apiClient.MetricsSummary("{}", "name", 0, 0) + + // test websockets search + wsClient := httpclient.New("ws://"+tempo.Endpoint(3200), tc.tenant) + _, wsErr := wsClient.SearchWithWebsocket(&tempopb.SearchRequest{ + Query: "{}", Start: uint32(now.Add(-20 * time.Minute).Unix()), End: uint32(now.Unix()), + }, func(sr *tempopb.SearchResponse) {}) + + // test streaming search over grpc + grpcClient, err := util.NewSearchGRPCClient(tempo.Endpoint(3200)) + require.NoError(t, err) + _, grpcErr := grpcClient.Search(context.Background(), &tempopb.SearchRequest{ + Query: "{}", Start: uint32(now.Add(-20 * time.Minute).Unix()), End: uint32(now.Unix()), + }) + + if tc.tenantSize > 1 { // we expect error in case of multi-tenant request + require.Error(t, msErr) + require.Error(t, wsErr) + require.Error(t, grpcErr) + } else { + require.NoError(t, msErr) + require.NoError(t, wsErr) + require.NoError(t, grpcErr) + } }) } } diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index 3837d5bc570..6af521972bd 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -100,9 +100,7 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo metrics := spanMetricsMiddleware.Wrap(next) - streamingMiddleware := MergeMiddlewares( - newMultiTenantMiddleware(cfg, combiner.NewSearch, logger), - retryWare).Wrap(next) + streamingMiddleware := MergeMiddlewares(retryWare).Wrap(next) return &QueryFrontend{ TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), @@ -120,6 +118,7 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo }, nil } +// Search implements StreamingQuerierServer interface for streaming search func (q *QueryFrontend) Search(req *tempopb.SearchRequest, srv tempopb.StreamingQuerier_SearchServer) error { return q.streamingSearch(req, srv) } diff --git a/modules/frontend/search_streaming.go b/modules/frontend/search_streaming.go index 461e78351c3..7bfa70ac5f1 100644 --- a/modules/frontend/search_streaming.go +++ b/modules/frontend/search_streaming.go @@ -98,13 +98,14 @@ func (p *diffSearchProgress) finalResult() *shardedSearchResults { // newSearchStreamingGRPCHandler returns a handler that streams results from the HTTP handler func newSearchStreamingGRPCHandler(cfg Config, o overrides.Interface, downstream http.RoundTripper, reader tempodb.Reader, searchCache *frontendCache, apiPrefix string, logger log.Logger) streamingSearchHandler { searcher := streamingSearcher{ - logger: logger, - downstream: downstream, - reader: reader, - postSLOHook: searchSLOPostHook(cfg.Search.SLO), - o: o, - searchCache: searchCache, - cfg: &cfg, + logger: logger, + downstream: downstream, + reader: reader, + postSLOHook: searchSLOPostHook(cfg.Search.SLO), + o: o, + searchCache: searchCache, + cfg: &cfg, + preMiddleware: newMultiTenantUnsupportedMiddleware(cfg, logger), } downstreamPath := path.Join(apiPrefix, api.PathSearch) @@ -132,13 +133,14 @@ func newSearchStreamingGRPCHandler(cfg Config, o overrides.Interface, downstream func newSearchStreamingWSHandler(cfg Config, o overrides.Interface, downstream http.RoundTripper, reader tempodb.Reader, searchCache *frontendCache, apiPrefix string, logger log.Logger) http.Handler { searcher := streamingSearcher{ - logger: logger, - downstream: downstream, - reader: reader, - postSLOHook: searchSLOPostHook(cfg.Search.SLO), - o: o, - searchCache: searchCache, - cfg: &cfg, + logger: logger, + downstream: downstream, + reader: reader, + postSLOHook: searchSLOPostHook(cfg.Search.SLO), + o: o, + searchCache: searchCache, + cfg: &cfg, + preMiddleware: newMultiTenantUnsupportedMiddleware(cfg, logger), } // since this is a backend DB we allow websockets to originate from anywhere @@ -228,13 +230,14 @@ func newSearchStreamingWSHandler(cfg Config, o overrides.Interface, downstream h } type streamingSearcher struct { - logger log.Logger - downstream http.RoundTripper - reader tempodb.Reader - postSLOHook handlerPostHook - o overrides.Interface - searchCache *frontendCache - cfg *Config + logger log.Logger + downstream http.RoundTripper + reader tempodb.Reader + postSLOHook handlerPostHook + o overrides.Interface + searchCache *frontendCache + cfg *Config + preMiddleware Middleware } func (s *streamingSearcher) handle(r *http.Request, forwardResults func(*tempopb.SearchResponse) error) error { @@ -258,7 +261,7 @@ func (s *streamingSearcher) handle(r *http.Request, forwardResults func(*tempopb } // build roundtripper ss := newSearchSharder(s.reader, s.o, s.cfg.Search.Sharder, fn, s.searchCache, s.logger) - rt := NewRoundTripper(s.downstream, ss) + rt := NewRoundTripper(s.downstream, s.preMiddleware, ss) type roundTripResult struct { resp *http.Response diff --git a/modules/frontend/tenant.go b/modules/frontend/tenant.go index 4676aeba841..a6fc4f2fe6e 100644 --- a/modules/frontend/tenant.go +++ b/modules/frontend/tenant.go @@ -104,7 +104,7 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error respCombiner := t.newCombiner() // call RoundTrip for each tenant and combine results - // Send one request per tenant to down-stream tripper + // Send one request per tenant to downstream tripper // Return early if statusCode is already set by a previous response for _, tenantID := range tenants { wg.Add(1) @@ -147,7 +147,6 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error }(tenantID) } - // TODO: will this work for search streaming??, look into it. might need a search steaming combiner wg.Wait() return respCombiner.Complete() @@ -182,7 +181,14 @@ func newMultiTenantUnsupportedMiddleware(cfg Config, logger log.Logger) Middlewa } func (t *unsupportedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + fmt.Printf("=== unsupportedRoundTripper.RoundTrip called...") + err := MultiTenantNotSupported(t.cfg, t.resolver, req) + if err == user.ErrNoOrgID { + // no org id, move to next tripper + return t.next.RoundTrip(req) + } + if err != nil { _ = level.Debug(t.logger).Log("msg", "multi-tenant query is not supported", "path", req.URL.EscapedPath()) @@ -205,7 +211,7 @@ func MultiTenantNotSupported(cfg Config, resolver tenant.Resolver, req *http.Req _, ctx, err := user.ExtractOrgIDFromHTTPRequest(req) if err != nil { - return fmt.Errorf("failed to extract org id from request: %w", err) + return err } // extract tenant ids diff --git a/pkg/httpclient/client.go b/pkg/httpclient/client.go index 3d00b32fa25..1462d7340ec 100644 --- a/pkg/httpclient/client.go +++ b/pkg/httpclient/client.go @@ -135,7 +135,7 @@ func (c *Client) doRequest(req *http.Request) (*http.Response, []byte, error) { func (c *Client) SearchTags() (*tempopb.SearchTagsResponse, error) { m := &tempopb.SearchTagsResponse{} - _, err := c.getFor(c.BaseURL+"/api/search/tags", m) + _, err := c.getFor(c.BaseURL+tempo_api.PathSearchTags, m) if err != nil { return nil, err } @@ -145,8 +145,7 @@ func (c *Client) SearchTags() (*tempopb.SearchTagsResponse, error) { func (c *Client) SearchTagsV2() (*tempopb.SearchTagsV2Response, error) { m := &tempopb.SearchTagsV2Response{} - resp, err := c.getFor(c.BaseURL+"/api/v2/search/tags", m) - fmt.Printf("==== SearchTagsV2: resp: %v \n", resp) + _, err := c.getFor(c.BaseURL+tempo_api.PathSearchTagsV2, m) if err != nil { return nil, err } @@ -179,7 +178,7 @@ func (c *Client) SearchTagValuesV2(key, query string) (*tempopb.SearchTagValuesV // Search Tempo. tags must be in logfmt format, that is "key1=value1 key2=value2" func (c *Client) Search(tags string) (*tempopb.SearchResponse, error) { m := &tempopb.SearchResponse{} - _, err := c.getFor(c.buildQueryURL("tags", tags, 0, 0), m) + _, err := c.getFor(c.buildSearchQueryURL("tags", tags, 0, 0), m) if err != nil { return nil, err } @@ -191,7 +190,7 @@ func (c *Client) Search(tags string) (*tempopb.SearchResponse, error) { // epoch timestamps in seconds. func (c *Client) SearchWithRange(tags string, start int64, end int64) (*tempopb.SearchResponse, error) { m := &tempopb.SearchResponse{} - _, err := c.getFor(c.buildQueryURL("tags", tags, start, end), m) + _, err := c.getFor(c.buildSearchQueryURL("tags", tags, start, end), m) if err != nil { return nil, err } @@ -214,7 +213,7 @@ func (c *Client) QueryTrace(id string) (*tempopb.Trace, error) { func (c *Client) SearchTraceQL(query string) (*tempopb.SearchResponse, error) { m := &tempopb.SearchResponse{} - _, err := c.getFor(c.buildQueryURL("q", query, 0, 0), m) + _, err := c.getFor(c.buildSearchQueryURL("q", query, 0, 0), m) if err != nil { return nil, err } @@ -224,7 +223,7 @@ func (c *Client) SearchTraceQL(query string) (*tempopb.SearchResponse, error) { func (c *Client) SearchTraceQLWithRange(query string, start int64, end int64) (*tempopb.SearchResponse, error) { m := &tempopb.SearchResponse{} - _, err := c.getFor(c.buildQueryURL("q", query, start, end), m) + _, err := c.getFor(c.buildSearchQueryURL("q", query, start, end), m) if err != nil { return nil, err } @@ -309,7 +308,27 @@ func (c *Client) SearchWithWebsocket(req *tempopb.SearchRequest, f func(*tempopb } } -func (c *Client) buildQueryURL(queryType string, query string, start int64, end int64) string { +func (c *Client) MetricsSummary(query string, groupBy string, start int64, end int64) (*tempopb.SpanMetricsSummaryResponse, error) { + joinURL, _ := url.Parse(c.BaseURL + tempo_api.PathSpanMetricsSummary + "?") + q := joinURL.Query() + if start != 0 && end != 0 { + q.Set("start", strconv.FormatInt(start, 10)) + q.Set("end", strconv.FormatInt(end, 10)) + } + q.Set("q", query) + q.Set("groupBy", groupBy) + joinURL.RawQuery = q.Encode() + + m := &tempopb.SpanMetricsSummaryResponse{} + _, err := c.getFor(fmt.Sprint(joinURL), m) + if err != nil { + return m, err + } + + return m, nil +} + +func (c *Client) buildSearchQueryURL(queryType string, query string, start int64, end int64) string { joinURL, _ := url.Parse(c.BaseURL + "/api/search?") q := joinURL.Query() if start != 0 && end != 0 { From dae8dcf4bc4da4c66dd13fa2c15881472ad0598f Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Thu, 14 Dec 2023 01:16:47 +0530 Subject: [PATCH 27/34] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 733f3c7a18c..5134c3bc879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## main / unreleased +* [FEATURE] Add support for multi-tenant queries. [#3087](https://github.com/grafana/tempo/pull/3087) (@electron0zero) * [BUGFIX] Change exit code if config is successfully verified [#3174](https://github.com/grafana/tempo/pull/3174) (@am3o @agrib-01) * [BUGFIX] The tempo-cli analyse blocks command no longer fails on compacted blocks [#3183](https://github.com/grafana/tempo/pull/3183) (@stoewer) * [BUGFIX] Move waitgroup handling for poller error condition [#3224](https://github.com/grafana/tempo/pull/3224) (@zalegrala) From 3b47de3b4edd4bfe16df11c3c9d2094cc32aa00b Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Thu, 14 Dec 2023 19:33:20 +0530 Subject: [PATCH 28/34] Add tests for unsupprted endpoints --- integration/e2e/e2e_test.go | 3 +- integration/e2e/encodings_test.go | 3 +- integration/e2e/multi_tenant_test.go | 19 +++++--- integration/util.go | 4 +- modules/frontend/tenant.go | 37 +++++----------- modules/frontend/tenant_test.go | 65 +++++++++++++++++----------- 6 files changed, 70 insertions(+), 61 deletions(-) diff --git a/integration/e2e/e2e_test.go b/integration/e2e/e2e_test.go index 333ee650886..a04a7959309 100644 --- a/integration/e2e/e2e_test.go +++ b/integration/e2e/e2e_test.go @@ -1,6 +1,7 @@ package e2e import ( + "context" "encoding/json" "fmt" "io" @@ -127,7 +128,7 @@ func TestAllInOne(t *testing.T) { util.SearchAndAssertTraceBackend(t, apiClient, info, now.Add(-20*time.Minute).Unix(), now.Unix()) // find the trace with streaming. using the http server b/c that's what Grafana will do - grpcClient, err := util.NewSearchGRPCClient(tempo.Endpoint(3200)) + grpcClient, err := util.NewSearchGRPCClient(context.Background(), tempo.Endpoint(3200)) require.NoError(t, err) util.SearchStreamAndAssertTrace(t, grpcClient, info, now.Add(-20*time.Minute).Unix(), now.Unix()) diff --git a/integration/e2e/encodings_test.go b/integration/e2e/encodings_test.go index 653761eac93..6668c5e0452 100644 --- a/integration/e2e/encodings_test.go +++ b/integration/e2e/encodings_test.go @@ -1,6 +1,7 @@ package e2e import ( + "context" "os" "testing" "time" @@ -106,7 +107,7 @@ func TestEncodings(t *testing.T) { queryAndAssertTrace(t, apiClient, info) // create grpc client used for streaming - grpcClient, err := integration.NewSearchGRPCClient(tempo.Endpoint(3200)) + grpcClient, err := integration.NewSearchGRPCClient(context.Background(), tempo.Endpoint(3200)) require.NoError(t, err) if enc.Version() == v2.VersionString { diff --git a/integration/e2e/multi_tenant_test.go b/integration/e2e/multi_tenant_test.go index 82359027915..0834230ffd5 100644 --- a/integration/e2e/multi_tenant_test.go +++ b/integration/e2e/multi_tenant_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/grafana/dskit/user" "github.com/grafana/e2e" "github.com/grafana/tempo/pkg/httpclient" "github.com/grafana/tempo/pkg/tempopb" @@ -32,10 +33,6 @@ type traceStringsMap struct { spanNames []string } -// TODO: add a test for unsupported endpoints?? -// TODO: test search streaming?? we don't support multi-tenant query there, will do in the next pass -// TODO: should we test this with `multitenancy_enabled: false` as well?? not sure?? - // TestMultiTenantSearch tests multi tenant query support func TestMultiTenantSearch(t *testing.T) { testTenants := []struct { @@ -212,13 +209,21 @@ func TestMultiTenantSearch(t *testing.T) { }, func(sr *tempopb.SearchResponse) {}) // test streaming search over grpc - grpcClient, err := util.NewSearchGRPCClient(tempo.Endpoint(3200)) + grpcCtx := user.InjectOrgID(context.Background(), tc.tenant) + grpcCtx, err = user.InjectIntoGRPCRequest(grpcCtx) + require.NoError(t, err) + + grpcClient, err := util.NewSearchGRPCClient(grpcCtx, tempo.Endpoint(3200)) require.NoError(t, err) - _, grpcErr := grpcClient.Search(context.Background(), &tempopb.SearchRequest{ + grpcResp, err := grpcClient.Search(grpcCtx, &tempopb.SearchRequest{ Query: "{}", Start: uint32(now.Add(-20 * time.Minute).Unix()), End: uint32(now.Unix()), }) + require.NoError(t, err) + // actual error comes in resp, need to call Recv to get it. + _, grpcErr := grpcResp.Recv() - if tc.tenantSize > 1 { // we expect error in case of multi-tenant request + if tc.tenantSize > 1 { + // we expect error in case of multi-tenant request for unsupported endpoints require.Error(t, msErr) require.Error(t, wsErr) require.Error(t, grpcErr) diff --git a/integration/util.go b/integration/util.go index 57fa9800931..3f3b7ea9ade 100644 --- a/integration/util.go +++ b/integration/util.go @@ -280,8 +280,8 @@ func NewJaegerGRPCClient(endpoint string) (*jaeger_grpc.Reporter, error) { return jaeger_grpc.NewReporter(conn, nil, logger), err } -func NewSearchGRPCClient(endpoint string) (tempopb.StreamingQuerierClient, error) { - clientConn, err := grpc.DialContext(context.Background(), endpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) +func NewSearchGRPCClient(ctx context.Context, endpoint string) (tempopb.StreamingQuerierClient, error) { + clientConn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, err } diff --git a/modules/frontend/tenant.go b/modules/frontend/tenant.go index a6fc4f2fe6e..4fcccdd6480 100644 --- a/modules/frontend/tenant.go +++ b/modules/frontend/tenant.go @@ -3,7 +3,6 @@ package frontend import ( "context" "errors" - "fmt" "io" "net/http" "strconv" @@ -78,21 +77,19 @@ func (t *tenantRoundTripper) RoundTrip(req *http.Request) (*http.Response, error return t.next.RoundTrip(req) } - _, ctx, err := user.ExtractOrgIDFromHTTPRequest(req) - if err == user.ErrNoOrgID { - // no org id, move to next tripper - return t.next.RoundTrip(req) - } - if err != nil { - return nil, fmt.Errorf("failed to extract org id from request: %w", err) - } - // extract tenant ids, this will normalize and de-duplicate tenant ids - tenants, err := t.resolver.TenantIDs(ctx) + tenants, err := t.resolver.TenantIDs(req.Context()) if err != nil { - return nil, err + // if we return an err here, downstream handler will turn it into HTTP 500 Internal Server Error. + // respond with 400 and error as body and return nil error. + return &http.Response{ + StatusCode: http.StatusBadRequest, + Status: http.StatusText(http.StatusBadRequest), + Body: io.NopCloser(strings.NewReader(err.Error())), + }, nil } - // for single tenant, fall through to next round tripper + + // for single tenant, go to next round tripper if len(tenants) <= 1 { return t.next.RoundTrip(req) } @@ -181,16 +178,11 @@ func newMultiTenantUnsupportedMiddleware(cfg Config, logger log.Logger) Middlewa } func (t *unsupportedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - fmt.Printf("=== unsupportedRoundTripper.RoundTrip called...") err := MultiTenantNotSupported(t.cfg, t.resolver, req) - if err == user.ErrNoOrgID { - // no org id, move to next tripper - return t.next.RoundTrip(req) - } if err != nil { - _ = level.Debug(t.logger).Log("msg", "multi-tenant query is not supported", "path", req.URL.EscapedPath()) + _ = level.Debug(t.logger).Log("msg", "multi-tenant query unsupported", "error", err, "path", req.URL.EscapedPath()) // if we return an err here, downstream handler will turn it into HTTP 500 Internal Server Error. // respond with 400 and error as body and return nil error. @@ -209,13 +201,8 @@ func MultiTenantNotSupported(cfg Config, resolver tenant.Resolver, req *http.Req return nil } - _, ctx, err := user.ExtractOrgIDFromHTTPRequest(req) - if err != nil { - return err - } - // extract tenant ids - tenants, err := resolver.TenantIDs(ctx) + tenants, err := resolver.TenantIDs(req.Context()) if err != nil { return err } diff --git a/modules/frontend/tenant_test.go b/modules/frontend/tenant_test.go index 431ebaaabad..d2467f48462 100644 --- a/modules/frontend/tenant_test.go +++ b/modules/frontend/tenant_test.go @@ -2,6 +2,7 @@ package frontend import ( "bytes" + "context" "crypto/rand" "io" "net/http" @@ -73,7 +74,7 @@ func TestMultiTenant(t *testing.T) { reqCount.Inc() // Count the number of requests. // Check if the tenant is in the list of tenants. - tenantID, _, err := user.ExtractOrgIDFromHTTPRequest(req) + tenantID, err := user.ExtractOrgID(req.Context()) require.NoError(t, err) _, ok := tenantsMap[tenantID] require.True(t, ok) @@ -81,8 +82,6 @@ func TestMultiTenant(t *testing.T) { // we do this in requestForTenant method, which is skipped for single tenant if len(tenants) > 1 { // ensure that tenant id in http header is same as org id in context - // some places are using http headers and some are using context to - // extract tenant id form the request so need both to be set and be correct. orgID, err := user.ExtractOrgID(req.Context()) require.NoError(t, err) require.Equal(t, tenantID, orgID) @@ -108,7 +107,8 @@ func TestMultiTenant(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/", nil) require.NoError(t, err) - req.Header.Set(user.OrgIDHeaderName, tc.tenants) + ctx := user.InjectOrgID(context.Background(), tc.tenants) + req = req.WithContext(ctx) res, err := rt.RoundTrip(req) require.NoError(t, err) @@ -133,41 +133,56 @@ func TestMultiTenant(t *testing.T) { func TestMultiTenantNotSupported(t *testing.T) { tests := []struct { - name string - cfg Config - tenant string - err error + name string + cfg Config + tenant string + err error + context bool }{ { - name: "multi-tenant queries disabled", - cfg: Config{MultiTenantQueriesEnabled: false}, - tenant: "test", - err: nil, + name: "multi-tenant queries disabled", + cfg: Config{MultiTenantQueriesEnabled: false}, + tenant: "test", + err: nil, + context: true, + }, + { + name: "multi-tenant queries disabled with multiple tenant", + cfg: Config{MultiTenantQueriesEnabled: false}, + tenant: "test|test1", + err: nil, + context: true, }, { - name: "multi-tenant queries disabled with multiple tenant", - cfg: Config{MultiTenantQueriesEnabled: false}, - tenant: "test|test1", - err: nil, + name: "multi-tenant queries enabled with single tenant", + cfg: Config{MultiTenantQueriesEnabled: true}, + tenant: "test", + err: nil, + context: true, }, { - name: "multi-tenant queries enabled with single tenant", - cfg: Config{MultiTenantQueriesEnabled: true}, - tenant: "test", - err: nil, + name: "multi-tenant queries enabled with multiple tenants", + cfg: Config{MultiTenantQueriesEnabled: true}, + tenant: "test|test1", + err: ErrMultiTenantUnsupported, + context: true, }, { - name: "multi-tenant queries enabled with multiple tenants", - cfg: Config{MultiTenantQueriesEnabled: true}, - tenant: "test|test1", - err: ErrMultiTenantUnsupported, + name: "no org id in request context", + cfg: Config{MultiTenantQueriesEnabled: true}, + tenant: "test", + err: user.ErrNoOrgID, + context: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) - req.Header.Add("X-Scope-OrgID", tc.tenant) + if tc.context { + ctx := user.InjectOrgID(context.Background(), tc.tenant) + req = req.WithContext(ctx) + } resolver := tenant.NewMultiResolver() err := MultiTenantNotSupported(tc.cfg, resolver, req) From c44769b71e4886f73577449f13c8bf67c5b5dd8a Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Thu, 14 Dec 2023 20:02:15 +0530 Subject: [PATCH 29/34] more cleanup --- modules/frontend/frontend.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/frontend/frontend.go b/modules/frontend/frontend.go index 6af521972bd..9718e3e60eb 100644 --- a/modules/frontend/frontend.go +++ b/modules/frontend/frontend.go @@ -60,8 +60,6 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo // cache searchCache := newFrontendCache(cacheProvider, cache.RoleFrontendSearch, logger) - // TODO: return error for routes that don't support multi-tenant queries - // inject multi-tenant middleware in multi-tenant routes traceByIDMiddleware := MergeMiddlewares( newMultiTenantMiddleware(cfg, combiner.NewTraceByID, logger), @@ -100,8 +98,6 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo metrics := spanMetricsMiddleware.Wrap(next) - streamingMiddleware := MergeMiddlewares(retryWare).Wrap(next) - return &QueryFrontend{ TraceByIDHandler: newHandler(traces, traceByIDSLOPostHook(cfg.TraceByID.SLO), nil, logger), SearchHandler: newHandler(search, searchSLOPostHook(cfg.Search.SLO), searchSLOPreHook, logger), @@ -111,9 +107,9 @@ func New(cfg Config, next http.RoundTripper, o overrides.Interface, reader tempo SearchTagsValuesV2Handler: newHandler(searchTagValuesV2, nil, nil, logger), SpanMetricsSummaryHandler: newHandler(metrics, nil, nil, logger), - SearchWSHandler: newSearchStreamingWSHandler(cfg, o, streamingMiddleware, reader, searchCache, apiPrefix, logger), + SearchWSHandler: newSearchStreamingWSHandler(cfg, o, retryWare.Wrap(next), reader, searchCache, apiPrefix, logger), cacheProvider: cacheProvider, - streamingSearch: newSearchStreamingGRPCHandler(cfg, o, streamingMiddleware, reader, searchCache, apiPrefix, logger), + streamingSearch: newSearchStreamingGRPCHandler(cfg, o, retryWare.Wrap(next), reader, searchCache, apiPrefix, logger), logger: logger, }, nil } From 07de52eb2f9fb6d7c3c5392775e3b74080a72f01 Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Thu, 14 Dec 2023 22:42:03 +0530 Subject: [PATCH 30/34] fix lint --- integration/e2e/e2e_test.go | 2 +- modules/frontend/tenant.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/integration/e2e/e2e_test.go b/integration/e2e/e2e_test.go index a04a7959309..53857b00019 100644 --- a/integration/e2e/e2e_test.go +++ b/integration/e2e/e2e_test.go @@ -482,7 +482,7 @@ func writeMetrics(t *testing.T, tempo *e2e.HTTPService, filename string) { body, err := io.ReadAll(res.Body) require.NoError(t, err) - err = os.WriteFile("metrics_"+filename+"_dump.txt", body, 0644) + err = os.WriteFile("metrics_"+filename+"_dump.txt", body, 0o644) require.NoError(t, err) } diff --git a/modules/frontend/tenant.go b/modules/frontend/tenant.go index 4fcccdd6480..f249291dd0b 100644 --- a/modules/frontend/tenant.go +++ b/modules/frontend/tenant.go @@ -178,9 +178,7 @@ func newMultiTenantUnsupportedMiddleware(cfg Config, logger log.Logger) Middlewa } func (t *unsupportedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - err := MultiTenantNotSupported(t.cfg, t.resolver, req) - if err != nil { _ = level.Debug(t.logger).Log("msg", "multi-tenant query unsupported", "error", err, "path", req.URL.EscapedPath()) From a84f5f49148da4fb57ae286e54faabf73e3a925a Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Thu, 14 Dec 2023 22:42:24 +0530 Subject: [PATCH 31/34] fix golangci --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46b958363b5..c7cb1f0f714 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,8 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@v3 with: - version: v1.53.3 + version: v1.55.2 + only-new-issues: true unit-tests-pkg: name: Test packages - pkg From 2aa27167e06a77af2c54d650e9dfe998a27cf39e Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Fri, 15 Dec 2023 00:01:20 +0530 Subject: [PATCH 32/34] assert response for tags endpoints --- integration/e2e/multi_tenant_test.go | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/integration/e2e/multi_tenant_test.go b/integration/e2e/multi_tenant_test.go index 0834230ffd5..238e3d058a1 100644 --- a/integration/e2e/multi_tenant_test.go +++ b/integration/e2e/multi_tenant_test.go @@ -92,7 +92,7 @@ func TestMultiTenantSearch(t *testing.T) { tenants := strings.Split(tc.tenant, "|") require.Equal(t, tc.tenantSize, len(tenants)) - var expected float64 + var expectedSpans float64 // write traces for all tenants for _, tenant := range tenants { info = tempoUtil.NewTraceInfo(time.Now(), tenant) @@ -102,12 +102,12 @@ func TestMultiTenantSearch(t *testing.T) { traceMap = getAttrsAndSpanNames(trace) // store it to assert tests require.NoError(t, err) - expected = expected + spanCount(trace) + expectedSpans = expectedSpans + 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(tc.tenantSize)), "tempo_ingester_traces_created_total")) - require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(expected), "tempo_distributor_spans_received_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) @@ -144,21 +144,28 @@ func TestMultiTenantSearch(t *testing.T) { // force clear completed block callFlush(t, tempo) - // wait for flush to complete - time.Sleep(3 * time.Second) + // wait for flush to complete for all tenants, each tenant will have one block + require.NoError(t, tempo.WaitSumMetrics(e2e.Equals(float64(tc.tenantSize)), "tempo_ingester_blocks_flushed_total")) - // search tags endpoints - _, err = apiClient.SearchTags() + // 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) - _, err = apiClient.SearchTagsV2() + tagsV2Resp, err := apiClient.SearchTagsV2() require.NoError(t, err) + require.Equal(t, 3, len(tagsV2Resp.GetScopes())) + for _, s := range tagsV2Resp.Scopes { + require.NotEmpty(t, s.Tags) + } - _, err = apiClient.SearchTagValues("vulture-0") + tagsValuesResp, err := apiClient.SearchTagValues("vulture-0") require.NoError(t, err) + require.NotEmpty(t, tagsValuesResp.TagValues) - _, err = apiClient.SearchTagValuesV2("span.vulture-0", "{}") + tagsValuesV2Resp, err := apiClient.SearchTagValuesV2("span.vulture-0", "{}") require.NoError(t, err) + require.NotEmpty(t, tagsValuesV2Resp.TagValues) // assert tenant federation metrics if tc.tenantSize > 1 { From f498b1166c298f2cde05b475ed266dbcfe1c717f Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:18:49 +0530 Subject: [PATCH 33/34] docs feedback Co-authored-by: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> --- docs/sources/tempo/operations/cross_tenant_query.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sources/tempo/operations/cross_tenant_query.md b/docs/sources/tempo/operations/cross_tenant_query.md index ac32e842315..166f5704cba 100644 --- a/docs/sources/tempo/operations/cross_tenant_query.md +++ b/docs/sources/tempo/operations/cross_tenant_query.md @@ -1,6 +1,6 @@ --- title: Cross-tenant query federation -menuTitle: Query +menuTitle: Cross-tenant query description: Cross-tenant query federation weight: 70 aliases: @@ -10,8 +10,10 @@ aliases: # Cross-tenant query federation -> NOTE: you need to enable `multitenancy_enabled: true` in the cluster for multi-tenant querying to work. -> see [enable multi-tenancy]({{< relref "./multitenancy" >}}) for more details and implications of `multitenancy_enabled: true`. +{{% admonition type=note" %}} +You need to enable `multitenancy_enabled: true` in the cluster for multi-tenant querying to work. +see [enable multi-tenancy]({{< relref "./multitenancy" >}}) for more details and implications of `multitenancy_enabled: true`. +{{% /admonition %}} Tempo supports multi-tenant queries. where users can send list of tenants multiple tenants. From 54644a9664f55d5b14e325617132c78cb96dd44b Mon Sep 17 00:00:00 2001 From: Suraj Nath <9503187+electron0zero@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:27:29 +0530 Subject: [PATCH 34/34] reword and fix docs --- .../tempo/operations/cross_tenant_query.md | 35 +++--------------- docs/sources/tempo/operations/header_ds.png | Bin 17464 -> 0 bytes .../tempo/operations/multi_tenant_trace.png | Bin 93459 -> 0 bytes 3 files changed, 5 insertions(+), 30 deletions(-) delete mode 100644 docs/sources/tempo/operations/header_ds.png delete mode 100644 docs/sources/tempo/operations/multi_tenant_trace.png diff --git a/docs/sources/tempo/operations/cross_tenant_query.md b/docs/sources/tempo/operations/cross_tenant_query.md index 166f5704cba..2de7e62c67c 100644 --- a/docs/sources/tempo/operations/cross_tenant_query.md +++ b/docs/sources/tempo/operations/cross_tenant_query.md @@ -15,41 +15,16 @@ You need to enable `multitenancy_enabled: true` in the cluster for multi-tenant see [enable multi-tenancy]({{< relref "./multitenancy" >}}) for more details and implications of `multitenancy_enabled: true`. {{% /admonition %}} -Tempo supports multi-tenant queries. where users can send list of tenants multiple tenants. +Tempo supports multi-tenant queries for search, search-tags and trace-by-id search operations. -The tenant IDs involved need to be specified separated by a '|' character in the 'X-Scope-OrgID' header. +To perform multi-tenant queries, send tenant IDs separated by a `|` character in the `X-Scope-OrgID` header, for e.g: `foo|bar`. -cross-tenant query is enabled by default, and can be controlled using `multi_tenant_queries_enabled` config. +By default, Cross-tenant query is enabled and can be controlled using `multi_tenant_queries_enabled` configuration setting. ```yaml query_frontend: multi_tenant_queries_enabled: true ``` -### Use cross-tenant query federation - -To submit a query across all tenants that your access policy has access rights to, you need to configure tempo datasource. - -Update Tempo datasource to send `X-Scope-OrgID` header with values of tenants separated by `|` e.g. `test|test1`, and query the tempo like you already do. - -

X-Scope-OrgID Headers in Datasource

- -If you are provisioning tempo datasource via Grafana Provisioning, you can configure `x-scope-orgid` header like this: - -```yaml - jsonData: - httpHeaderName1: 'x-scope-orgid' - secureJsonData: - httpHeaderValue1: 'test|test1' -``` - -> NOTE: for streaming search, header `x-scope-orgid` needs to be lowercase. - -Queries are performed using the cross-tenant configured data source in either **Explore** or inside of dashboards are performed across all the tenants that you specified in the **X-Scope-OrgID** header. - -These queries are processed as if all the data were in a single tenant. - -Tempo will inject `tenant` resource in the responses to show which tenant the trace came from: - -

tenant resource attribute in response trace

- +Queries performed using the cross-tenant configured data source, in either **Explore** or inside of dashboards, +are performed across all the tenants that you specified in the **X-Scope-OrgID** header. diff --git a/docs/sources/tempo/operations/header_ds.png b/docs/sources/tempo/operations/header_ds.png deleted file mode 100644 index a2d36737935fe824ff236d9c3204513195196ecf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17464 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

9`Xs7@Z3#{5DNrPF4ICBON>J=RBXtlps3c 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 FV1kmoBIE!7