Support per-tenant left-padding of trace IDs (#6489)
* Support per-tenant left-padding of trace IDs * Update docs * chlog
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
* [ENHANCEMENT] Do deep validation for filter policies in user configurable overrides API [#6407](https://github.com/grafana/tempo/pull/6407) (@electron0zero)
|
||||
* [ENHANCEMENT] Allow span_name_sanitization to be set via user-configurable overrides API [#6411](https://github.com/grafana/tempo/pull/6411) (@Logiraptor)
|
||||
* [ENHANCEMENT] Add `fail_on_high_lag` parameter to allow live-store to fail if it is lagged [#6363](https://github.com/grafana/tempo/pull/6363) (@ruslan-mikhailov)
|
||||
* [ENHANCEMENT] Add support for per-tenant left-padding of trace IDs [#6439](https://github.com/grafana/tempo/pull/6489) (@mapno)
|
||||
* [BUGFIX] Force live-store to rehydrate from Kafka lookback period when local data is missing (e.g. PVC wipe, new node) instead of resuming from the committed consumer group offset [#6428](https://github.com/grafana/tempo/pull/6428) (@oleg-kozlyuk-grafana)
|
||||
* [ENHANCEMENT] Add new metric for generator ring size: `tempo_distributor_metrics_generator_tenant_ring_size` [#5686](https://github.com/grafana/tempo/pull/5686) (@zalegrala)
|
||||
* [BUGFIX] fix: reload span_name_sanitization overrides during runtime [#6435](https://github.com/grafana/tempo/pull/6435) (@electron0zero)
|
||||
|
||||
@@ -1910,6 +1910,11 @@ overrides:
|
||||
# in the front-end configuration is used.
|
||||
[max_metrics_duration: <duration> | default = 0s]
|
||||
|
||||
# Per-user option to left-pad trace IDs with zeros to 32 hex characters in search API responses.
|
||||
# When enabled, trace IDs like "8efff798038103d269b633813fc703" will be returned as
|
||||
# "008efff798038103d269b633813fc703" to comply with the OpenTelemetry and W3C Trace Context specifications.
|
||||
[left_pad_trace_ids: <bool> | default = false]
|
||||
|
||||
# Compaction related overrides
|
||||
compaction:
|
||||
# Per-user block retention. If this value is set to 0 (default),
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/grafana/tempo/pkg/search"
|
||||
"github.com/grafana/tempo/pkg/tempopb"
|
||||
"github.com/grafana/tempo/pkg/traceql"
|
||||
"github.com/grafana/tempo/pkg/util"
|
||||
)
|
||||
|
||||
// SearchJobResponse wraps shardtracker.JobMetadata and implements PipelineResponse.
|
||||
@@ -32,7 +33,7 @@ var _ PipelineResponse = (*SearchJobResponse)(nil)
|
||||
var _ GRPCCombiner[*tempopb.SearchResponse] = (*genericCombiner[*tempopb.SearchResponse])(nil)
|
||||
|
||||
// NewSearch returns a search combiner
|
||||
func NewSearch(limit int, keepMostRecent bool, marshalingFormat api.MarshallingFormat) Combiner {
|
||||
func NewSearch(limit int, keepMostRecent bool, marshalingFormat api.MarshallingFormat, padTraceIDs bool) Combiner {
|
||||
metadataCombiner := traceql.NewMetadataCombiner(limit, keepMostRecent)
|
||||
diffTraces := map[string]struct{}{}
|
||||
completedThroughTracker := &shardtracker.CompletionTracker{}
|
||||
@@ -80,6 +81,9 @@ func NewSearch(limit int, keepMostRecent bool, marshalingFormat api.MarshallingF
|
||||
final.Traces = metadataCombiner.Metadata()
|
||||
final.Metrics = metricsCombiner.Metrics
|
||||
addRootSpanNotReceivedText(final.Traces)
|
||||
if padTraceIDs {
|
||||
padTraceIDsInResponse(final.Traces)
|
||||
}
|
||||
return final, nil
|
||||
},
|
||||
diff: func(current *tempopb.SearchResponse) (*tempopb.SearchResponse, error) {
|
||||
@@ -113,6 +117,9 @@ func NewSearch(limit int, keepMostRecent bool, marshalingFormat api.MarshallingF
|
||||
}
|
||||
|
||||
addRootSpanNotReceivedText(diff.Traces)
|
||||
if padTraceIDs {
|
||||
padTraceIDsInResponse(diff.Traces)
|
||||
}
|
||||
|
||||
return diff, nil
|
||||
},
|
||||
@@ -138,6 +145,13 @@ func addRootSpanNotReceivedText(results []*tempopb.TraceSearchMetadata) {
|
||||
}
|
||||
}
|
||||
|
||||
func NewTypedSearch(limit int, keepMostRecent bool, marshalingFormat api.MarshallingFormat) GRPCCombiner[*tempopb.SearchResponse] {
|
||||
return NewSearch(limit, keepMostRecent, marshalingFormat).(GRPCCombiner[*tempopb.SearchResponse])
|
||||
func NewTypedSearch(limit int, keepMostRecent bool, marshalingFormat api.MarshallingFormat, padTraceIDs bool) GRPCCombiner[*tempopb.SearchResponse] {
|
||||
return NewSearch(limit, keepMostRecent, marshalingFormat, padTraceIDs).(GRPCCombiner[*tempopb.SearchResponse])
|
||||
}
|
||||
|
||||
// padTraceIDsInResponse left-pads all trace IDs in the given search metadata to 32 hex characters.
|
||||
func padTraceIDsInResponse(traces []*tempopb.TraceSearchMetadata) {
|
||||
for _, t := range traces {
|
||||
t.TraceID = util.PadTraceIDString(t.TraceID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,33 +26,33 @@ func TestSearchProgressShouldQuitAnyProtobuf(t *testing.T) {
|
||||
|
||||
func testSearchProgressShouldQuitAny(t *testing.T, marshalingFormat api.MarshallingFormat) {
|
||||
// new combiner should not quit
|
||||
c := NewSearch(0, false, marshalingFormat)
|
||||
c := NewSearch(0, false, marshalingFormat, false)
|
||||
should := c.ShouldQuit()
|
||||
require.False(t, should)
|
||||
|
||||
// 500 response should quit
|
||||
c = NewSearch(0, false, marshalingFormat)
|
||||
c = NewSearch(0, false, marshalingFormat, false)
|
||||
err := c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{}, 500, nil, marshalingFormat))
|
||||
require.NoError(t, err)
|
||||
should = c.ShouldQuit()
|
||||
require.True(t, should)
|
||||
|
||||
// 429 response should quit
|
||||
c = NewSearch(0, false, marshalingFormat)
|
||||
c = NewSearch(0, false, marshalingFormat, false)
|
||||
err = c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{}, 429, nil, marshalingFormat))
|
||||
require.NoError(t, err)
|
||||
should = c.ShouldQuit()
|
||||
require.True(t, should)
|
||||
|
||||
// unparseable body should not quit, but should return an error
|
||||
c = NewSearch(0, false, marshalingFormat)
|
||||
c = NewSearch(0, false, marshalingFormat, false)
|
||||
err = c.AddResponse(&testPipelineResponse{r: &http.Response{Body: io.NopCloser(strings.NewReader("foo")), StatusCode: 200}})
|
||||
require.Error(t, err)
|
||||
should = c.ShouldQuit()
|
||||
require.False(t, should)
|
||||
|
||||
// under limit should not quit
|
||||
c = NewSearch(2, false, marshalingFormat)
|
||||
c = NewSearch(2, false, marshalingFormat, false)
|
||||
err = c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{
|
||||
Traces: []*tempopb.TraceSearchMetadata{
|
||||
{
|
||||
@@ -65,7 +65,7 @@ func testSearchProgressShouldQuitAny(t *testing.T, marshalingFormat api.Marshall
|
||||
require.False(t, should)
|
||||
|
||||
// over limit should quit
|
||||
c = NewSearch(1, false, marshalingFormat)
|
||||
c = NewSearch(1, false, marshalingFormat, false)
|
||||
err = c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{
|
||||
Traces: []*tempopb.TraceSearchMetadata{
|
||||
{
|
||||
@@ -91,33 +91,33 @@ func TestSearchProgressShouldQuitMostRecentProtobuf(t *testing.T) {
|
||||
|
||||
func testSearchProgressShouldQuitMostRecent(t *testing.T, marshalingFormat api.MarshallingFormat) {
|
||||
// new combiner should not quit
|
||||
c := NewSearch(0, true, marshalingFormat)
|
||||
c := NewSearch(0, true, marshalingFormat, false)
|
||||
should := c.ShouldQuit()
|
||||
require.False(t, should)
|
||||
|
||||
// 500 response should quit
|
||||
c = NewSearch(0, true, marshalingFormat)
|
||||
c = NewSearch(0, true, marshalingFormat, false)
|
||||
err := c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{}, 500, nil, marshalingFormat))
|
||||
require.NoError(t, err)
|
||||
should = c.ShouldQuit()
|
||||
require.True(t, should)
|
||||
|
||||
// 429 response should quit
|
||||
c = NewSearch(0, true, marshalingFormat)
|
||||
c = NewSearch(0, true, marshalingFormat, false)
|
||||
err = c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{}, 429, nil, marshalingFormat))
|
||||
require.NoError(t, err)
|
||||
should = c.ShouldQuit()
|
||||
require.True(t, should)
|
||||
|
||||
// unparseable body should not quit, but should return an error
|
||||
c = NewSearch(0, true, marshalingFormat)
|
||||
c = NewSearch(0, true, marshalingFormat, false)
|
||||
err = c.AddResponse(&testPipelineResponse{r: &http.Response{Body: io.NopCloser(strings.NewReader("foo")), StatusCode: 200}})
|
||||
require.Error(t, err)
|
||||
should = c.ShouldQuit()
|
||||
require.False(t, should)
|
||||
|
||||
// under limit should not quit
|
||||
c = NewSearch(2, true, marshalingFormat)
|
||||
c = NewSearch(2, true, marshalingFormat, false)
|
||||
err = c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{
|
||||
Traces: []*tempopb.TraceSearchMetadata{
|
||||
{
|
||||
@@ -130,7 +130,7 @@ func testSearchProgressShouldQuitMostRecent(t *testing.T, marshalingFormat api.M
|
||||
require.False(t, should)
|
||||
|
||||
// over limit but no search job response, should not quit
|
||||
c = NewSearch(1, true, marshalingFormat)
|
||||
c = NewSearch(1, true, marshalingFormat, false)
|
||||
err = c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{
|
||||
Traces: []*tempopb.TraceSearchMetadata{
|
||||
{
|
||||
@@ -198,7 +198,7 @@ func testSearchCombinesResults(t *testing.T, marshalingFormat api.MarshallingFor
|
||||
start := time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC)
|
||||
traceID := "traceID"
|
||||
|
||||
c := NewSearch(10, keepMostRecent, marshalingFormat)
|
||||
c := NewSearch(10, keepMostRecent, marshalingFormat, false)
|
||||
sr := toHTTPResponseWithFormat(t, &tempopb.SearchResponse{
|
||||
Traces: []*tempopb.TraceSearchMetadata{
|
||||
{
|
||||
@@ -420,7 +420,7 @@ func testSearchResponseCombiner(t *testing.T, marshalingFormat api.MarshallingFo
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
combiner := NewTypedSearch(20, keepMostRecent, marshalingFormat)
|
||||
combiner := NewTypedSearch(20, keepMostRecent, marshalingFormat, false)
|
||||
|
||||
err := combiner.AddResponse(tc.response1)
|
||||
require.NoError(t, err)
|
||||
@@ -691,7 +691,7 @@ func testCombinerShards(t *testing.T, marshalingFormat api.MarshallingFormat) {
|
||||
|
||||
// apply tests one at a time to the combiner and check expected results
|
||||
|
||||
combiner := NewTypedSearch(5, true, marshalingFormat)
|
||||
combiner := NewTypedSearch(5, true, marshalingFormat, false)
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.pipelineResponse != nil {
|
||||
@@ -705,3 +705,85 @@ func testCombinerShards(t *testing.T, marshalingFormat api.MarshallingFormat) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchCombinerPadTraceIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
padTraceIDs bool
|
||||
marshalingFmt api.MarshallingFormat
|
||||
inputTraceIDs []string
|
||||
expectedIDs []string
|
||||
useDiff bool
|
||||
}{
|
||||
{
|
||||
name: "padding enabled with JSON format",
|
||||
padTraceIDs: true,
|
||||
marshalingFmt: api.MarshallingFormatJSON,
|
||||
inputTraceIDs: []string{"8efff798038103d269b633813fc703"},
|
||||
expectedIDs: []string{"008efff798038103d269b633813fc703"},
|
||||
},
|
||||
{
|
||||
name: "padding enabled with JSON format - 64-bit trace ID",
|
||||
padTraceIDs: true,
|
||||
marshalingFmt: api.MarshallingFormatJSON,
|
||||
inputTraceIDs: []string{"1234567890abcdef"},
|
||||
expectedIDs: []string{"00000000000000001234567890abcdef"},
|
||||
},
|
||||
{
|
||||
name: "padding disabled with JSON format",
|
||||
padTraceIDs: false,
|
||||
marshalingFmt: api.MarshallingFormatJSON,
|
||||
inputTraceIDs: []string{"8efff798038103d269b633813fc703"},
|
||||
expectedIDs: []string{"8efff798038103d269b633813fc703"},
|
||||
},
|
||||
{
|
||||
name: "padding enabled with protobuf format",
|
||||
padTraceIDs: true,
|
||||
marshalingFmt: api.MarshallingFormatProtobuf,
|
||||
inputTraceIDs: []string{"8efff798038103d269b633813fc703"},
|
||||
expectedIDs: []string{"008efff798038103d269b633813fc703"},
|
||||
},
|
||||
{
|
||||
name: "padding enabled - already 32 chars",
|
||||
padTraceIDs: true,
|
||||
marshalingFmt: api.MarshallingFormatJSON,
|
||||
inputTraceIDs: []string{"1234567890abcdef1234567890abcdef"},
|
||||
expectedIDs: []string{"1234567890abcdef1234567890abcdef"},
|
||||
},
|
||||
{
|
||||
name: "padding enabled via diff",
|
||||
padTraceIDs: true,
|
||||
marshalingFmt: api.MarshallingFormatJSON,
|
||||
inputTraceIDs: []string{"8efff798038103d269b633813fc703"},
|
||||
expectedIDs: []string{"008efff798038103d269b633813fc703"},
|
||||
useDiff: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
traces := make([]*tempopb.TraceSearchMetadata, 0, len(tc.inputTraceIDs))
|
||||
for _, id := range tc.inputTraceIDs {
|
||||
traces = append(traces, &tempopb.TraceSearchMetadata{TraceID: id})
|
||||
}
|
||||
|
||||
c := NewTypedSearch(10, false, tc.marshalingFmt, tc.padTraceIDs)
|
||||
err := c.AddResponse(toHTTPResponseWithFormat(t, &tempopb.SearchResponse{Traces: traces}, 200, nil, tc.marshalingFmt))
|
||||
require.NoError(t, err)
|
||||
|
||||
var resp *tempopb.SearchResponse
|
||||
if tc.useDiff {
|
||||
resp, err = c.GRPCDiff()
|
||||
} else {
|
||||
resp, err = c.GRPCFinal()
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Len(t, resp.Traces, len(tc.expectedIDs))
|
||||
|
||||
for i, expectedID := range tc.expectedIDs {
|
||||
require.Equal(t, expectedID, resp.Traces[i].TraceID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ func New(cfg Config, next pipeline.RoundTripper, o overrides.Interface, reader t
|
||||
|
||||
traces := newTraceIDHandler(cfg, tracePipeline, o, combiner.NewTypedTraceByID, logger, dataAccessController)
|
||||
tracesV2 := newTraceIDV2Handler(cfg, tracePipeline, o, combiner.NewTypedTraceByIDV2, logger, dataAccessController)
|
||||
search := newSearchHTTPHandler(cfg, searchPipeline, logger, dataAccessController)
|
||||
search := newSearchHTTPHandler(cfg, searchPipeline, o, logger, dataAccessController)
|
||||
searchTags := newTagsHTTPHandler(cfg, searchTagsPipeline, o, logger, dataAccessController)
|
||||
searchTagsV2 := newTagsV2HTTPHandler(cfg, searchTagsPipeline, o, logger, dataAccessController)
|
||||
searchTagValues := newTagValuesHTTPHandler(cfg, searchTagValuesPipeline, o, logger, dataAccessController)
|
||||
@@ -281,7 +281,7 @@ func New(cfg Config, next pipeline.RoundTripper, o overrides.Interface, reader t
|
||||
MetricsQueryRangeHandler: newHandler(cfg.Config.LogQueryRequestHeaders, queryRange, logger),
|
||||
|
||||
// grpc/streaming
|
||||
streamingSearch: newSearchStreamingGRPCHandler(cfg, searchPipeline, apiPrefix, logger, dataAccessController),
|
||||
streamingSearch: newSearchStreamingGRPCHandler(cfg, searchPipeline, apiPrefix, o, logger, dataAccessController),
|
||||
streamingTags: newTagsStreamingGRPCHandler(cfg, searchTagsPipeline, apiPrefix, o, logger, dataAccessController),
|
||||
streamingTagsV2: newTagsV2StreamingGRPCHandler(cfg, searchTagsPipeline, apiPrefix, o, logger, dataAccessController),
|
||||
streamingTagValues: newTagValuesStreamingGRPCHandler(cfg, searchTagValuesPipeline, apiPrefix, o, logger, dataAccessController),
|
||||
|
||||
@@ -305,7 +305,7 @@ func TestAsyncResponsesDoesNotLeak(t *testing.T) {
|
||||
bridge := &pipelineBridge{
|
||||
next: tc.finalRT(cancel),
|
||||
}
|
||||
httpCollector := NewHTTPCollector(sharder{next: bridge}, 0, combiner.NewSearch(0, false, api.HeaderAcceptJSON))
|
||||
httpCollector := NewHTTPCollector(sharder{next: bridge}, 0, combiner.NewSearch(0, false, api.HeaderAcceptJSON, false))
|
||||
|
||||
_, _ = httpCollector.RoundTrip(req)
|
||||
|
||||
@@ -327,7 +327,7 @@ func TestAsyncResponsesDoesNotLeak(t *testing.T) {
|
||||
bridge := &pipelineBridge{
|
||||
next: tc.finalRT(cancel),
|
||||
}
|
||||
grpcCollector := NewGRPCCollector[*tempopb.SearchResponse](sharder{next: bridge}, 0, combiner.NewTypedSearch(0, false, api.HeaderAcceptJSON), func(_ *tempopb.SearchResponse) error { return nil })
|
||||
grpcCollector := NewGRPCCollector[*tempopb.SearchResponse](sharder{next: bridge}, 0, combiner.NewTypedSearch(0, false, api.HeaderAcceptJSON, false), func(_ *tempopb.SearchResponse) error { return nil })
|
||||
|
||||
_ = grpcCollector.RoundTrip(req)
|
||||
|
||||
@@ -351,7 +351,7 @@ func TestAsyncResponsesDoesNotLeak(t *testing.T) {
|
||||
}
|
||||
|
||||
s := sharder{next: sharder{next: bridge}, funcSharder: true}
|
||||
grpcCollector := NewGRPCCollector[*tempopb.SearchResponse](s, 0, combiner.NewTypedSearch(0, false, api.HeaderAcceptJSON), func(_ *tempopb.SearchResponse) error { return nil })
|
||||
grpcCollector := NewGRPCCollector[*tempopb.SearchResponse](s, 0, combiner.NewTypedSearch(0, false, api.HeaderAcceptJSON, false), func(_ *tempopb.SearchResponse) error { return nil })
|
||||
|
||||
_ = grpcCollector.RoundTrip(req)
|
||||
|
||||
@@ -374,7 +374,7 @@ func TestAsyncResponsesDoesNotLeak(t *testing.T) {
|
||||
}
|
||||
|
||||
s := sharder{next: sharder{next: bridge, funcSharder: true}}
|
||||
grpcCollector := NewGRPCCollector[*tempopb.SearchResponse](s, 0, combiner.NewTypedSearch(0, false, api.HeaderAcceptJSON), func(_ *tempopb.SearchResponse) error { return nil })
|
||||
grpcCollector := NewGRPCCollector[*tempopb.SearchResponse](s, 0, combiner.NewTypedSearch(0, false, api.HeaderAcceptJSON, false), func(_ *tempopb.SearchResponse) error { return nil })
|
||||
|
||||
_ = grpcCollector.RoundTrip(req)
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@ import (
|
||||
"github.com/grafana/tempo/modules/frontend/pipeline"
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
"github.com/grafana/tempo/modules/overrides"
|
||||
"github.com/grafana/tempo/pkg/api"
|
||||
"github.com/grafana/tempo/pkg/tempopb"
|
||||
"github.com/grafana/tempo/pkg/traceql"
|
||||
)
|
||||
|
||||
// newSearchStreamingGRPCHandler returns a handler that streams results from the HTTP handler
|
||||
func newSearchStreamingGRPCHandler(cfg Config, next pipeline.AsyncRoundTripper[combiner.PipelineResponse], apiPrefix string, logger log.Logger, dataAccessController DataAccessController) streamingSearchHandler {
|
||||
func newSearchStreamingGRPCHandler(cfg Config, next pipeline.AsyncRoundTripper[combiner.PipelineResponse], apiPrefix string, o overrides.Interface, logger log.Logger, dataAccessController DataAccessController) streamingSearchHandler {
|
||||
postSLOHook := searchSLOPostHook(cfg.Search.SLO)
|
||||
downstreamPath := path.Join(apiPrefix, api.PathSearch)
|
||||
|
||||
@@ -54,7 +55,7 @@ func newSearchStreamingGRPCHandler(cfg Config, next pipeline.AsyncRoundTripper[c
|
||||
tenant, _ := user.ExtractOrgID(ctx)
|
||||
start := time.Now()
|
||||
|
||||
comb, err := newCombiner(req, cfg.Search.Sharder, api.MarshallingFormatProtobuf)
|
||||
comb, err := newCombiner(req, cfg.Search.Sharder, api.MarshallingFormatProtobuf, o.LeftPadTraceIDs(tenant))
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "search streaming: could not create combiner", "err", err)
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
@@ -82,7 +83,7 @@ func newSearchStreamingGRPCHandler(cfg Config, next pipeline.AsyncRoundTripper[c
|
||||
}
|
||||
|
||||
// newSearchHTTPHandler returns a handler that returns a single response from the HTTP handler
|
||||
func newSearchHTTPHandler(cfg Config, next pipeline.AsyncRoundTripper[combiner.PipelineResponse], logger log.Logger, dataAccessController DataAccessController) http.RoundTripper {
|
||||
func newSearchHTTPHandler(cfg Config, next pipeline.AsyncRoundTripper[combiner.PipelineResponse], o overrides.Interface, logger log.Logger, dataAccessController DataAccessController) http.RoundTripper {
|
||||
postSLOHook := searchSLOPostHook(cfg.Search.SLO)
|
||||
|
||||
return RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
@@ -109,7 +110,7 @@ func newSearchHTTPHandler(cfg Config, next pipeline.AsyncRoundTripper[combiner.P
|
||||
// check marshalling format
|
||||
marshallingFormat := api.MarshalingFormatFromAcceptHeader(req.Header)
|
||||
|
||||
comb, err := newCombiner(searchReq, cfg.Search.Sharder, marshallingFormat)
|
||||
comb, err := newCombiner(searchReq, cfg.Search.Sharder, marshallingFormat, o.LeftPadTraceIDs(tenant))
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "search: could not create combiner", "err", err)
|
||||
return httpInvalidRequest(err), nil
|
||||
@@ -136,7 +137,7 @@ func newSearchHTTPHandler(cfg Config, next pipeline.AsyncRoundTripper[combiner.P
|
||||
})
|
||||
}
|
||||
|
||||
func newCombiner(req *tempopb.SearchRequest, cfg SearchSharderConfig, marshalingFormat api.MarshallingFormat) (combiner.GRPCCombiner[*tempopb.SearchResponse], error) {
|
||||
func newCombiner(req *tempopb.SearchRequest, cfg SearchSharderConfig, marshalingFormat api.MarshallingFormat, padTraceIDs bool) (combiner.GRPCCombiner[*tempopb.SearchResponse], error) {
|
||||
limit, err := adjustLimit(req.Limit, cfg.DefaultLimit, cfg.MaxLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -155,7 +156,7 @@ func newCombiner(req *tempopb.SearchRequest, cfg SearchSharderConfig, marshaling
|
||||
}
|
||||
}
|
||||
|
||||
return combiner.NewTypedSearch(int(limit), mostRecent, marshalingFormat), nil
|
||||
return combiner.NewTypedSearch(int(limit), mostRecent, marshalingFormat, padTraceIDs), nil
|
||||
}
|
||||
|
||||
// adjusts the limit based on provided config
|
||||
|
||||
@@ -173,6 +173,10 @@ type ReadOverrides struct {
|
||||
MaxMetricsDuration model.Duration `yaml:"max_metrics_duration,omitempty" json:"max_metrics_duration,omitempty"`
|
||||
|
||||
UnsafeQueryHints bool `yaml:"unsafe_query_hints,omitempty" json:"unsafe_query_hints,omitempty"`
|
||||
|
||||
// LeftPadTraceIDs left-pads trace IDs in search responses to 32 hex characters with zeros.
|
||||
// This produces W3C/OpenTelemetry compliant trace IDs (32-hex-character lowercase strings).
|
||||
LeftPadTraceIDs bool `yaml:"left_pad_trace_ids,omitempty" json:"left_pad_trace_ids,omitempty"`
|
||||
}
|
||||
|
||||
type CompactionOverrides struct {
|
||||
|
||||
@@ -79,6 +79,7 @@ func (c *Overrides) toLegacy() LegacyOverrides {
|
||||
MaxSearchDuration: c.Read.MaxSearchDuration,
|
||||
MaxMetricsDuration: c.Read.MaxMetricsDuration,
|
||||
UnsafeQueryHints: c.Read.UnsafeQueryHints,
|
||||
LeftPadTraceIDs: c.Read.LeftPadTraceIDs,
|
||||
|
||||
MaxBytesPerTrace: c.Global.MaxBytesPerTrace,
|
||||
|
||||
@@ -166,6 +167,7 @@ type LegacyOverrides struct {
|
||||
MaxSearchDuration model.Duration `yaml:"max_search_duration" json:"max_search_duration"`
|
||||
MaxMetricsDuration model.Duration `yaml:"max_metrics_duration" json:"max_metrics_duration"`
|
||||
UnsafeQueryHints bool `yaml:"unsafe_query_hints" json:"unsafe_query_hints"`
|
||||
LeftPadTraceIDs bool `yaml:"left_pad_trace_ids" json:"left_pad_trace_ids"`
|
||||
|
||||
// MaxBytesPerTrace is enforced in the Ingester, Compactor, Querier (Search). It
|
||||
// is not used when doing a trace by id lookup.
|
||||
@@ -196,6 +198,7 @@ func (l *LegacyOverrides) toNewLimits() Overrides {
|
||||
MaxSearchDuration: l.MaxSearchDuration,
|
||||
MaxMetricsDuration: l.MaxMetricsDuration,
|
||||
UnsafeQueryHints: l.UnsafeQueryHints,
|
||||
LeftPadTraceIDs: l.LeftPadTraceIDs,
|
||||
},
|
||||
Compaction: CompactionOverrides{
|
||||
BlockRetention: l.BlockRetention,
|
||||
|
||||
@@ -92,6 +92,7 @@ type Interface interface {
|
||||
MaxMetricsDuration(userID string) time.Duration
|
||||
DedicatedColumns(userID string) backend.DedicatedColumns
|
||||
UnsafeQueryHints(userID string) bool
|
||||
LeftPadTraceIDs(userID string) bool
|
||||
CostAttributionMaxCardinality(userID string) uint64
|
||||
CostAttributionDimensions(userID string) map[string]string
|
||||
|
||||
|
||||
@@ -374,6 +374,10 @@ func (o *runtimeConfigOverridesManager) UnsafeQueryHints(userID string) bool {
|
||||
return o.getOverridesForUser(userID).Read.UnsafeQueryHints
|
||||
}
|
||||
|
||||
func (o *runtimeConfigOverridesManager) LeftPadTraceIDs(userID string) bool {
|
||||
return o.getOverridesForUser(userID).Read.LeftPadTraceIDs
|
||||
}
|
||||
|
||||
func (o *runtimeConfigOverridesManager) CostAttributionMaxCardinality(userID string) uint64 {
|
||||
return o.getOverridesForUser(userID).CostAttribution.MaxCardinality
|
||||
}
|
||||
|
||||
@@ -13,6 +13,18 @@ func HexStringToTraceID(id string) ([]byte, error) {
|
||||
return hexStringToID(id, false)
|
||||
}
|
||||
|
||||
// traceIDPadding is a string of 32 zeros used for left-padding trace IDs.
|
||||
const traceIDPadding = "00000000000000000000000000000000"
|
||||
|
||||
// PadTraceIDString left-pads a trace ID hex string to 32 characters with zeros.
|
||||
// This produces W3C/OpenTelemetry compliant 32-character lowercase hex trace IDs.
|
||||
func PadTraceIDString(id string) string {
|
||||
if len(id) >= 32 {
|
||||
return id
|
||||
}
|
||||
return traceIDPadding[:32-len(id)] + id
|
||||
}
|
||||
|
||||
// TraceIDToHexString converts a trace ID to its string representation and removes any leading zeros.
|
||||
func TraceIDToHexString(byteID []byte) string {
|
||||
dst := make([]byte, hex.EncodedLen(len(byteID)))
|
||||
|
||||
@@ -246,6 +246,51 @@ func BenchmarkSpanIDAndKindToToken(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPadTraceIDString(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
id string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
id: "",
|
||||
expected: "00000000000000000000000000000000",
|
||||
},
|
||||
{
|
||||
name: "short id - leading zeros stripped",
|
||||
id: "8efff798038103d269b633813fc703",
|
||||
expected: "008efff798038103d269b633813fc703",
|
||||
},
|
||||
{
|
||||
name: "single char",
|
||||
id: "1",
|
||||
expected: "00000000000000000000000000000001",
|
||||
},
|
||||
{
|
||||
name: "already 32 chars",
|
||||
id: "1234567890abcdef1234567890abcdef",
|
||||
expected: "1234567890abcdef1234567890abcdef",
|
||||
},
|
||||
{
|
||||
name: "64-bit id (16 chars)",
|
||||
id: "1234567890abcdef",
|
||||
expected: "00000000000000001234567890abcdef",
|
||||
},
|
||||
{
|
||||
name: "longer than 32 chars is unchanged",
|
||||
id: "1234567890abcdef1234567890abcdef00",
|
||||
expected: "1234567890abcdef1234567890abcdef00",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, PadTraceIDString(tt.id))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualHexStringTraceIDs(t *testing.T) {
|
||||
a := "82f6471b46d25e23418a0a99d4c2cda"
|
||||
b := "082f6471b46d25e23418a0a99d4c2cda"
|
||||
|
||||
Reference in New Issue
Block a user