diff --git a/CHANGELOG.md b/CHANGELOG.md index f03dc28f4ba..ed7c6834a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ * [ENHANCEMENT] Jsonnet: add `$._config.search_enabled`, correctly set `http_api_prefix` in config [#1072](https://github.com/grafana/tempo/pull/1072) (@kvrhdn) * [ENHANCEMENT] Performance: Remove WAL contention between ingest and searches [#1076](https://github.com/grafana/tempo/pull/1076) (@mdisibio) * [ENHANCEMENT] Include tempo-cli in the release [#1086](https://github.com/grafana/tempo/pull/1086) (@zalegrala) +* [ENHANCEMENT] Add search on span status [#1093](https://github.com/grafana/tempo/pull/1093) (@mdisibio) * [BUGFIX] Update port spec for GCS docker-compose example [#869](https://github.com/grafana/tempo/pull/869) (@zalegrala) * [BUGFIX] Fix "magic number" errors and other block mishandling when an ingester forcefully shuts down [#937](https://github.com/grafana/tempo/issues/937) (@mdisibio) * [BUGFIX] Fix compactor memory leak [#806](https://github.com/grafana/tempo/pull/806) (@mdisibio) diff --git a/modules/distributor/search_data.go b/modules/distributor/search_data.go index 8bb956e5c32..6390f1cba65 100644 --- a/modules/distributor/search_data.go +++ b/modules/distributor/search_data.go @@ -66,6 +66,9 @@ func extractSearchData(trace *tempopb.Trace, id []byte, extractTag extractTagFun // Collect for any spans data.AddTag(search.SpanNameTag, s.Name) + if s.Status != nil { + data.AddTag(search.StatusCodeTag, strconv.Itoa(int(s.Status.Code))) + } data.SetStartTimeUnixNano(s.StartTimeUnixNano) data.SetEndTimeUnixNano(s.EndTimeUnixNano) diff --git a/modules/querier/querier.go b/modules/querier/querier.go index 3f2e90a762c..c26b9e7c562 100644 --- a/modules/querier/querier.go +++ b/modules/querier/querier.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/tempo/pkg/model" "github.com/grafana/tempo/pkg/tempopb" "github.com/grafana/tempo/pkg/validation" + "github.com/grafana/tempo/tempodb/search" "github.com/opentracing/opentracing-go" ot_log "github.com/opentracing/opentracing-go/log" "github.com/pkg/errors" @@ -309,6 +310,11 @@ func (q *Querier) SearchTags(ctx context.Context, req *tempopb.SearchTagsRequest } } + // Extra tags + for _, k := range search.GetVirtualTags() { + uniqueMap[k] = struct{}{} + } + // Final response (sorted) resp := &tempopb.SearchTagsResponse{ TagNames: make([]string, 0, len(uniqueMap)), @@ -348,6 +354,11 @@ func (q *Querier) SearchTagValues(ctx context.Context, req *tempopb.SearchTagVal } } + // Extra values + for _, v := range search.GetVirtualTagValues(req.TagName) { + uniqueMap[v] = struct{}{} + } + // Final response (sorted) resp := &tempopb.SearchTagValuesResponse{ TagValues: make([]string, 0, len(uniqueMap)), diff --git a/tempodb/search/pipeline.go b/tempodb/search/pipeline.go index 04d358ea826..55919b245e2 100644 --- a/tempodb/search/pipeline.go +++ b/tempodb/search/pipeline.go @@ -1,11 +1,13 @@ package search import ( + "strconv" "strings" "time" "github.com/grafana/tempo/pkg/tempofb" "github.com/grafana/tempo/pkg/tempopb" + v1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" ) const SecretExhaustiveSearchTag = "x-dbg-exhaustive" @@ -59,14 +61,8 @@ func NewSearchPipeline(req *tempopb.SearchRequest) Pipeline { vb := make([][]byte, 0, len(req.Tags)) for k, v := range req.Tags { - if k == SecretExhaustiveSearchTag { - // Perform an exhaustive search by: - // * no block or page filters means all blocks and pages match - // * substitute this trace filter instead rejects everything. therefore it never - // quits early due to enough results - p.tracefilters = append(p.tracefilters, func(s tempofb.Trace) bool { - return false - }) + skip, k, v := p.rewriteTagLookup(k, v) + if skip { continue } @@ -91,6 +87,45 @@ func NewSearchPipeline(req *tempopb.SearchRequest) Pipeline { return p } +// rewriteTagLookup intercepts certain tag/value lookups and rewrites them. It returns +// true if the tag lookup should be excluded from the remaining tag/value lookups because +// the it was rewritten into a different filter altogether. Otherwise it returns false, +// and a new set of tag/value strings to use, which will either be the original inputs +// or rewritten lookups. +func (p *Pipeline) rewriteTagLookup(k, v string) (skip bool, newk, newv string) { + switch k { + case SecretExhaustiveSearchTag: + // Perform an exhaustive search by: + // * no block or page filters means all blocks and pages match + // * substitute this trace filter instead rejects everything. therefore it never + // quits early due to enough results + p.tracefilters = append(p.tracefilters, func(s tempofb.Trace) bool { + return false + }) + // Skip + return true, "", "" + + case ErrorTag: + if v == "true" { + // Error = true + return false, StatusCodeTag, strconv.Itoa(int(v1.Status_STATUS_CODE_ERROR)) + } + // Else fall-through + + case StatusCodeTag: + // Convert status.code=string into status.code=int + for statusStr, statusID := range statusCodeMapping { + if v == statusStr { + return false, StatusCodeTag, strconv.Itoa(statusID) + } + } + // Unknown mapping = fall-through + } + + // No rewrite + return false, k, v +} + func (p *Pipeline) Matches(e tempofb.Trace) bool { for _, f := range p.tracefilters { diff --git a/tempodb/search/pipeline_test.go b/tempodb/search/pipeline_test.go index 02374162a6d..e40f0580f46 100644 --- a/tempodb/search/pipeline_test.go +++ b/tempodb/search/pipeline_test.go @@ -1,11 +1,13 @@ package search import ( + "strconv" "testing" "time" "github.com/grafana/tempo/pkg/tempofb" "github.com/grafana/tempo/pkg/tempopb" + v1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" "github.com/stretchr/testify/require" ) @@ -46,7 +48,20 @@ func TestPipelineMatchesTags(t *testing.T) { searchData: map[string][]string{"key1": {"value1"}, "key2": {"value2"}}, request: map[string]string{"key1": "value1", "key3": "value3"}, shouldMatch: false, - }} + }, + { + name: "rewriteError", + searchData: map[string][]string{StatusCodeTag: {strconv.Itoa(int(v1.Status_STATUS_CODE_ERROR))}}, + request: map[string]string{"error": "true"}, + shouldMatch: true, + }, + { + name: "rewriteStatusCode", + searchData: map[string][]string{StatusCodeTag: {strconv.Itoa(int(v1.Status_STATUS_CODE_ERROR))}}, + request: map[string]string{StatusCodeTag: StatusCodeError}, + shouldMatch: true, + }, + } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/tempodb/search/util.go b/tempodb/search/util.go index f4ca122417c..46770b11010 100644 --- a/tempodb/search/util.go +++ b/tempodb/search/util.go @@ -3,6 +3,7 @@ package search import ( "github.com/grafana/tempo/pkg/tempofb" "github.com/grafana/tempo/pkg/tempopb" + v1 "github.com/grafana/tempo/pkg/tempopb/trace/v1" "github.com/grafana/tempo/pkg/util" ) @@ -11,8 +12,36 @@ const ( ServiceNameTag = "service.name" RootSpanNameTag = "root.name" SpanNameTag = "name" + ErrorTag = "error" + StatusCodeTag = "status.code" + StatusCodeUnset = "unset" + StatusCodeOK = "ok" + StatusCodeError = "error" ) +var statusCodeMapping = map[string]int{ + StatusCodeUnset: int(v1.Status_STATUS_CODE_UNSET), + StatusCodeOK: int(v1.Status_STATUS_CODE_OK), + StatusCodeError: int(v1.Status_STATUS_CODE_ERROR), +} + +func GetVirtualTags() []string { + return []string{ErrorTag} +} + +func GetVirtualTagValues(tagName string) []string { + switch tagName { + + case StatusCodeTag: + return []string{StatusCodeUnset, StatusCodeOK, StatusCodeError} + + case ErrorTag: + return []string{"true"} + } + + return nil +} + func GetSearchResultFromData(s *tempofb.SearchEntry) *tempopb.TraceSearchMetadata { return &tempopb.TraceSearchMetadata{ TraceID: util.TraceIDToHexString(s.Id()),