Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ To make use of filtering, configure `autocomplete_filtering_enabled`.
* [FEATURE] Add a GRPC streaming endpoint for traceql search [#2366](https://github.com/grafana/tempo/pull/2366) (@joe-elliott)
* [FEATURE] Add new API to summarize span metrics from generators [#2481](https://github.com/grafana/tempo/pull/2481) (@zalegrala)
* [FEATURE] Add `select()` to TraceQL [#2494](https://github.com/grafana/tempo/pull/2494) (@joe-elliott)
* [FEATURE] Add `traceDuration`, `rootName` and `rootServiceName` intrinsics to TraceQL [#2503](https://github.com/grafana/tempo/pull/2503) (@joe-elliott)
* [ENHANCEMENT] Add `scope` parameter to `/api/search/tags` [#2282](https://github.com/grafana/tempo/pull/2282) (@joe-elliott)
Create new endpoint `/api/v2/search/tags` that returns all tags organized by scope.
* [ENHANCEMENT] Ability to toggle off latency or count metrics in metrics-generator [#2070](https://github.com/grafana/tempo/pull/2070) (@AlexDHoffer)
Expand Down
21 changes: 15 additions & 6 deletions docs/sources/tempo/traceql/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,21 @@ Intrinsic fields are fundamental to spans. These fields can be referenced when s

The following table shows the current intrinsic fields:

| **Field** | **Type** | **Definition** | **Example** |
|---------------|-------------|-----------------------------------------------------------------|------------------------|
| status | status enum | status: error, ok, or unset | { status = ok } |
| duration | duration | end - start time of the span | { duration > 100ms } |
| name | string | operation or span name | { name = "HTTP POST" } |
| kind | kind enum | kind: server, client, producer, consumer, internal, unspecified | { kind = server } |
| **Field** | **Type** | **Definition** | **Example** |
|-----------------|-------------|-----------------------------------------------------------------|---------------------------------|
| status | status enum | status: error, ok, or unset | { status = ok } |
| duration | duration | end - start time of the span | { duration > 100ms } |
| name | string | operation or span name | { name = "HTTP POST" } |
| kind | kind enum | kind: server, client, producer, consumer, internal, unspecified | { kind = server } |
| traceDuration | duration | max(end) - min(start) time of the spans in the trace | { traceDuration > 100ms } |
| rootName | string | if it exists the name of the root span in the trace | { rootName = "HTTP GET" } |
| rootServiceName | string | if it exists the service name of the root span in the trace | { rootServiceName = "gateway" } |

{{% admonition type="note" %}}
`traceDuration`, `rootName` and `rootServiceName` are trace-level intrinsics and will be the same for all spans in the same trace. Additionally,
these intrinsics are significantly more performant because they have to inspect much less data then a span-level intrinsic. They should be preferred whenever
possible to span-level intrinsics.
{{% /admonition %}}

### Attribute fields

Expand Down
2 changes: 1 addition & 1 deletion modules/ingester/instance_search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func TestInstanceSearchTagsSpecialCases(t *testing.T) {

resp, err = i.SearchTags(userCtx, "intrinsic")
require.NoError(t, err)
require.Equal(t, []string{"duration", "kind", "name", "status"}, resp.TagNames)
require.Equal(t, []string{"duration", "kind", "name", "status", "traceDuration", "rootServiceName", "rootName"}, resp.TagNames)
}

// TestInstanceSearchMaxBytesPerTagValuesQueryReturnsPartial confirms that SearchTagValues returns
Expand Down
5 changes: 3 additions & 2 deletions modules/querier/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,13 +568,14 @@ func (q *Querier) SearchTagValuesV2(ctx context.Context, req *tempopb.SearchTagV
distinctValues := util.NewDistinctValueCollector(limit, func(v tempopb.TagValue) int { return len(v.Type) + len(v.Value) })

// Virtual tags values. Get these first.
for _, v := range search.GetVirtualTagValuesV2(req.TagName) {
virtualVals := search.GetVirtualTagValuesV2(req.TagName)
for _, v := range virtualVals {
distinctValues.Collect(v)
}

// with v2 search we can confidently bail if GetVirtualTagValuesV2 gives us any hits. this doesn't work
// in v1 search b/c intrinsic tags like "status" are conflated with attributes named "status"
if distinctValues.TotalDataSize() > 0 {
if virtualVals != nil {
return valuesToV2Response(distinctValues), nil
}

Expand Down
91 changes: 91 additions & 0 deletions modules/querier/querier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"net/http"
"net/http/httptest"
"sort"
"testing"
"time"

Expand Down Expand Up @@ -79,3 +80,93 @@ func TestQuerierUsesSearchExternalEndpoint(t *testing.T) {
require.Equal(t, tc.externalExpected, numExternalRequests.Load())
}
}

func TestVirtualTagsDoesntHitBackend(t *testing.T) {
o, err := overrides.NewOverrides(overrides.Limits{})
require.NoError(t, err)

q, err := New(Config{}, ingester_client.Config{}, nil, generator_client.Config{}, nil, nil, o)
require.NoError(t, err)

ctx := user.InjectOrgID(context.Background(), "blerg")

// duration should return nothing
resp, err := q.SearchTagValuesV2(ctx, &tempopb.SearchTagValuesRequest{
TagName: "duration",
})
require.NoError(t, err)
require.Equal(t, &tempopb.SearchTagValuesV2Response{}, resp)

// traceDuration should return nothing
resp, err = q.SearchTagValuesV2(ctx, &tempopb.SearchTagValuesRequest{
TagName: "traceDuration",
})
require.NoError(t, err)
require.Equal(t, &tempopb.SearchTagValuesV2Response{}, resp)

// status should return a static list
resp, err = q.SearchTagValuesV2(ctx, &tempopb.SearchTagValuesRequest{
TagName: "status",
})
require.NoError(t, err)
sort.Slice(resp.TagValues, func(i, j int) bool { return resp.TagValues[i].Value < resp.TagValues[j].Value })
require.Equal(t, &tempopb.SearchTagValuesV2Response{
TagValues: []*tempopb.TagValue{
{
Type: "keyword",
Value: "error",
},
{
Type: "keyword",
Value: "ok",
},
{
Type: "keyword",
Value: "unset",
},
},
}, resp)

// kind should return a static list
resp, err = q.SearchTagValuesV2(ctx, &tempopb.SearchTagValuesRequest{
TagName: "kind",
})
require.NoError(t, err)
sort.Slice(resp.TagValues, func(i, j int) bool { return resp.TagValues[i].Value < resp.TagValues[j].Value })
require.Equal(t, &tempopb.SearchTagValuesV2Response{
TagValues: []*tempopb.TagValue{
{
Type: "keyword",
Value: "client",
},
{
Type: "keyword",
Value: "consumer",
},
{
Type: "keyword",
Value: "internal",
},
{
Type: "keyword",
Value: "producer",
},
{
Type: "keyword",
Value: "server",
},
{
Type: "keyword",
Value: "unspecified",
},
},
}, resp)

// this should panic b/c we haven't actually set any of the fields on the querier necessary for it to forward requests
// to the ingesters.
require.Panics(t, func() {
_, _ = q.SearchTagValuesV2(ctx, &tempopb.SearchTagValuesRequest{
TagName: ".foo",
})
})
}
8 changes: 7 additions & 1 deletion pkg/search/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ func GetVirtualTagValues(tagName string) []string {
}

func GetVirtualTagValuesV2(tagName string) []tempopb.TagValue {

switch tagName {
case traceql.IntrinsicStatus.String():
return []tempopb.TagValue{
Expand All @@ -46,6 +45,10 @@ func GetVirtualTagValuesV2(tagName string) []tempopb.TagValue {
{Type: "keyword", Value: traceql.KindInternal.String()},
{Type: "keyword", Value: traceql.KindUnspecified.String()},
}
case traceql.IntrinsicDuration.String():
return []tempopb.TagValue{}
case traceql.IntrinsicTraceDuration.String():
return []tempopb.TagValue{}
}

return nil
Expand All @@ -57,5 +60,8 @@ func GetVirtualIntrinsicValues() []string {
traceql.IntrinsicKind.String(),
traceql.IntrinsicName.String(),
traceql.IntrinsicStatus.String(),
traceql.IntrinsicTraceDuration.String(),
traceql.IntrinsicTraceRootService.String(),
traceql.IntrinsicTraceRootSpan.String(),
}
}
6 changes: 6 additions & 0 deletions pkg/traceql/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,12 @@ func (a Attribute) impliedType() StaticType {
return TypeKind
case IntrinsicParent:
return TypeNil
case IntrinsicTraceDuration:
return TypeDuration
case IntrinsicTraceRootService:
return TypeString
case IntrinsicTraceRootSpan:
return TypeString
}

return TypeAttribute
Expand Down
22 changes: 7 additions & 15 deletions pkg/traceql/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,8 @@ func (e *Engine) ExecuteSearch(ctx context.Context, searchReq *tempopb.SearchReq
span.SetTag("pipeline", rootExpr.Pipeline)
span.SetTag("fetchSpansRequest", fetchSpansRequest)

// calculate search meta conditions. the only choice is whether or not to include duration
// if we request duration as part of the normal span fetch then we can ignore it
durationRequested := false
for _, c := range fetchSpansRequest.Conditions {
if c.Attribute.Intrinsic == IntrinsicDuration {
durationRequested = true
break
}
}

metaConditions := SearchMetaConditions()
if durationRequested {
metaConditions = SearchMetaConditionsWithoutDuration()
}
// calculate search meta conditions.
metaConditions := SearchMetaConditionsWithout(fetchSpansRequest.Conditions)

spansetsEvaluated := 0
// set up the expression evaluation as a filter to reduce data pulled
Expand Down Expand Up @@ -315,7 +303,11 @@ func (e *Engine) asTraceSearchMetadata(spanset *Spanset) *tempopb.TraceSearchMet
}

for attribute, static := range atts {
if attribute.Intrinsic == IntrinsicName || attribute.Intrinsic == IntrinsicDuration {
if attribute.Intrinsic == IntrinsicName ||
attribute.Intrinsic == IntrinsicDuration ||
attribute.Intrinsic == IntrinsicTraceDuration ||
attribute.Intrinsic == IntrinsicTraceRootService ||
attribute.Intrinsic == IntrinsicTraceRootSpan {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Ah ok I missed this before. We could probably simplify the Span interface and put the intrinsics just in the map and not dedicated fields in the future.

continue
}

Expand Down
14 changes: 7 additions & 7 deletions pkg/traceql/enum_attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ const (
IntrinsicStatus
IntrinsicKind
IntrinsicChildCount

// not yet implemented in traceql but will be
IntrinsicParent
IntrinsicTraceRootService
IntrinsicTraceRootSpan
IntrinsicTraceDuration

// not yet implemented in traceql but will be
IntrinsicParent

// not yet implemented in traceql and may never be. these exist so that we can retrieve
// these fields from the fetch layer
IntrinsicTraceID
Expand All @@ -87,9 +87,9 @@ func (i Intrinsic) String() string {
case IntrinsicParent:
return "parent"
case IntrinsicTraceRootService:
return "traceRootService"
return "rootServiceName"
case IntrinsicTraceRootSpan:
return "traceRootSpan"
return "rootName"
case IntrinsicTraceDuration:
return "traceDuration"
case IntrinsicTraceID:
Expand Down Expand Up @@ -121,9 +121,9 @@ func intrinsicFromString(s string) Intrinsic {
// unimplemented
case "parent":
return IntrinsicParent
case "traceRootService":
case "rootServiceName":
return IntrinsicTraceRootService
case "traceRootSpan":
case "rootName":
return IntrinsicTraceRootSpan
case "traceDuration":
return IntrinsicTraceDuration
Expand Down
17 changes: 10 additions & 7 deletions pkg/traceql/expr.y
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import (
%token <val> DOT OPEN_BRACE CLOSE_BRACE OPEN_PARENS CLOSE_PARENS COMMA
NIL TRUE FALSE STATUS_ERROR STATUS_OK STATUS_UNSET
KIND_UNSPECIFIED KIND_INTERNAL KIND_SERVER KIND_CLIENT KIND_PRODUCER KIND_CONSUMER
IDURATION CHILDCOUNT NAME STATUS PARENT KIND
IDURATION CHILDCOUNT NAME STATUS PARENT KIND ROOTNAME ROOTSERVICENAME TRACEDURATION
PARENT_DOT RESOURCE_DOT SPAN_DOT
COUNT AVG MAX MIN SUM
BY COALESCE SELECT
Expand Down Expand Up @@ -276,12 +276,15 @@ static:
;

intrinsicField:
IDURATION { $$ = NewIntrinsic(IntrinsicDuration) }
| CHILDCOUNT { $$ = NewIntrinsic(IntrinsicChildCount) }
| NAME { $$ = NewIntrinsic(IntrinsicName) }
| STATUS { $$ = NewIntrinsic(IntrinsicStatus) }
| KIND { $$ = NewIntrinsic(IntrinsicKind) }
| PARENT { $$ = NewIntrinsic(IntrinsicParent) }
IDURATION { $$ = NewIntrinsic(IntrinsicDuration) }
| CHILDCOUNT { $$ = NewIntrinsic(IntrinsicChildCount) }
| NAME { $$ = NewIntrinsic(IntrinsicName) }
| STATUS { $$ = NewIntrinsic(IntrinsicStatus) }
| KIND { $$ = NewIntrinsic(IntrinsicKind) }
| PARENT { $$ = NewIntrinsic(IntrinsicParent) }
| ROOTNAME { $$ = NewIntrinsic(IntrinsicTraceRootSpan) }
| ROOTSERVICENAME { $$ = NewIntrinsic(IntrinsicTraceRootService) }
| TRACEDURATION { $$ = NewIntrinsic(IntrinsicTraceDuration) }
;

attributeField:
Expand Down
Loading