diff --git a/_oas/tempo.yml b/_oas/tempo.yml index 24dc7dfd..e4604782 100644 --- a/_oas/tempo.yml +++ b/_oas/tempo.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Grafana Tempo 'query-frontend' API - version: 2.1.0 + version: 2.4.0 externalDocs: description: Tempo API reference url: https://grafana.com/docs/tempo/latest/api_docs @@ -33,12 +33,14 @@ paths: schema: type: string description: TraceID to query. + - name: start in: query schema: type: integer format: unix-seconds description: Along with `end` define a time range from which traces should be returned. + - name: end in: query schema: @@ -116,6 +118,13 @@ paths: Providing both `start` and `end` will change the way that Tempo searches. If the parameters are not provided, then Tempo will search the recent trace data stored in the ingesters. If the parameters are provided, it will search the backend as well. + + - name: spss + in: query + schema: + type: integer + description: | + Limit the number of spans per span-set. Default value is 3. responses: 200: $ref: "#/components/responses/Search" @@ -126,6 +135,30 @@ paths: get: operationId: searchTags description: This endpoint retrieves all discovered tag names that can be used in search. + parameters: + - name: scope + in: query + schema: + $ref: "#/components/schemas/TagScope" + description: | + Specifies the scope of the tags, this is an optional parameter, if not specified it means all scopes. + + - name: start + in: query + schema: + type: integer + format: unix-seconds + description: | + Along with `end` define a time range from which tags should be returned. + + - name: end + in: query + schema: + type: integer + format: unix-seconds + description: | + Along with `start` define a time range from which tags should be returned. + Providing both `start` and `end` includes blocks for the specified time range only. responses: 200: $ref: "#/components/responses/SearchTags" @@ -143,29 +176,123 @@ paths: schema: type: string description: Tag name. + + - name: q + in: query + schema: + type: string + description: | + If provided, the tag values returned by the API are filtered to only return values seen on spans matching your filter parameters. + Queries can be incomplete: for example, `{ .cluster = }`. Tempo extracts only the valid matchers and build a valid query. + + Only queries with a single selector `{}`` and AND `&&` operators are supported. + + - Example supported: `{ .cluster = "us-east-1" && .service = "frontend" }` + - Example unsupported: `{ .cluster = "us-east-1" || .service = "frontend" } && { .cluster = "us-east-2" }` + + - name: start + in: query + schema: + type: integer + format: unix-seconds + description: | + Along with `end` define a time range from which tags should be returned. + + - name: end + in: query + schema: + type: integer + format: unix-seconds + description: | + Along with `start` define a time range from which tags should be returned. + Providing both `start` and `end` includes blocks for the specified time range only. responses: 200: $ref: "#/components/responses/SearchTagValues" default: $ref: "#/components/responses/Error" - /api/v2/search/tag/{tag_name}/values: + /api/v2/search/tag/{attribute_selector}/values: get: operationId: searchTagValuesV2 description: This endpoint retrieves all discovered values and their data types for the given TraceQL identifier. parameters: - - name: tag_name + - name: attribute_selector in: path required: true schema: type: string - description: Tag name. + description: TraceQL attribute selector (`.service.name`, `resource.service.name`, etc.). + + - name: q + in: query + schema: + type: string + description: | + If provided, the tag values returned by the API are filtered to only return values seen on spans matching your filter parameters. + Queries can be incomplete: for example, `{ .cluster = }`. Tempo extracts only the valid matchers and build a valid query. + + Only queries with a single selector `{}`` and AND `&&` operators are supported. + + - Example supported: `{ .cluster = "us-east-1" && .service = "frontend" }` + - Example unsupported: `{ .cluster = "us-east-1" || .service = "frontend" } && { .cluster = "us-east-2" }` + + - name: start + in: query + schema: + type: integer + format: unix-seconds + description: | + Along with `end` define a time range from which tags should be returned. + + - name: end + in: query + schema: + type: integer + format: unix-seconds + description: | + Along with `start` define a time range from which tags should be returned. + Providing both `start` and `end` includes blocks for the specified time range only. responses: 200: $ref: "#/components/responses/SearchTagValuesV2" default: $ref: "#/components/responses/Error" + /api/v2/search/tags: + get: + operationId: searchTagsV2 + description: This endpoint retrieves all discovered tag names that can be used in search. + parameters: + - name: scope + in: query + schema: + $ref: "#/components/schemas/TagScope" + description: | + Specifies the scope of the tags, this is an optional parameter, if not specified it means all scopes. + + - name: start + in: query + schema: + type: integer + format: unix-seconds + description: | + Along with `end` define a time range from which tags should be returned. + + - name: end + in: query + schema: + type: integer + format: unix-seconds + description: | + Along with `start` define a time range from which tags should be returned. + Providing both `start` and `end` includes blocks for the specified time range only. + responses: + 200: + $ref: "#/components/responses/SearchTagsV2" + default: + $ref: "#/components/responses/Error" + components: responses: Error: @@ -201,6 +328,12 @@ components: "application/json": schema: $ref: "#/components/schemas/TagValues" + SearchTagsV2: + description: Search tags result + content: + "application/json": + schema: + $ref: "#/components/schemas/TagNamesV2" SearchTagValuesV2: description: Search tag values result content: @@ -354,6 +487,24 @@ components: items: type: string + TagNamesV2: + type: object + properties: + scopes: + type: array + items: + $ref: "#/components/schemas/ScopeTags" + ScopeTags: + type: object + required: [name] + properties: + name: + $ref: "#/components/schemas/TagScope" + tags: + type: array + items: + type: string + TagValuesV2: type: object properties: @@ -372,5 +523,13 @@ components: value: type: string + TagScope: + type: string + enum: + - "span" + - "resource" + - "intrinsic" + - "none" + Error: type: string diff --git a/cmd/oteldb/app.go b/cmd/oteldb/app.go index 19046a60..384cf704 100644 --- a/cmd/oteldb/app.go +++ b/cmd/oteldb/app.go @@ -154,7 +154,7 @@ func (app *App) trySetupTempo() error { engine := traceqlengine.NewEngine(app.traceQuerier, traceqlengine.Options{ TracerProvider: app.metrics.TracerProvider(), }) - tempo := tempohandler.NewTempoAPI(q, engine) + tempo := tempohandler.NewTempoAPI(q, engine, tempohandler.TempoAPIOptions{}) s, err := tempoapi.NewServer(tempo, tempoapi.WithTracerProvider(app.metrics.TracerProvider()), diff --git a/integration/logger.go b/integration/logger.go new file mode 100644 index 00000000..0615db76 --- /dev/null +++ b/integration/logger.go @@ -0,0 +1,49 @@ +package integration + +import ( + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" +) + +// Logger creates a new [zap.Logger] to use in tests. +func Logger(t *testing.T) *zap.Logger { + return zaptest.NewLogger(t, zaptest.WrapOptions(zap.WrapCore(wrapTestLogger))) +} + +func wrapTestLogger(core zapcore.Core) zapcore.Core { + return &filterCore{core: core} +} + +type filterCore struct { + core zapcore.Core +} + +var _ zapcore.Core = (*filterCore)(nil) + +func (c *filterCore) Enabled(l zapcore.Level) bool { + return c.core.Enabled(l) +} + +func (c *filterCore) With(fields []zapcore.Field) zapcore.Core { + return &filterCore{ + core: c.core.With(fields), + } +} + +func (c *filterCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if ce == nil || ce.LoggerName == "ch" { + return ce + } + return c.core.Check(e, ce) +} + +func (c *filterCore) Write(e zapcore.Entry, fields []zapcore.Field) error { + return c.core.Write(e, fields) +} + +func (c *filterCore) Sync() error { + return c.core.Sync() +} diff --git a/integration/lokie2e/ch_test.go b/integration/lokie2e/ch_test.go index 55548896..10e59240 100644 --- a/integration/lokie2e/ch_test.go +++ b/integration/lokie2e/ch_test.go @@ -14,7 +14,6 @@ import ( "github.com/go-faster/sdk/zctx" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" - "go.uber.org/zap/zaptest" "github.com/go-faster/oteldb/integration" "github.com/go-faster/oteldb/internal/chstorage" @@ -91,6 +90,6 @@ func TestCH(t *testing.T) { }) require.NoError(t, err) - ctx = zctx.Base(ctx, zaptest.NewLogger(t)) + ctx = zctx.Base(ctx, integration.Logger(t)) runTest(ctx, t, provider, inserter, querier, querier) } diff --git a/integration/prome2e/ch_test.go b/integration/prome2e/ch_test.go index 32bfafc7..ae719e4c 100644 --- a/integration/prome2e/ch_test.go +++ b/integration/prome2e/ch_test.go @@ -10,6 +10,7 @@ import ( "github.com/ClickHouse/ch-go/chpool" "github.com/cenkalti/backoff/v4" "github.com/go-faster/errors" + "github.com/go-faster/sdk/zctx" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -76,5 +77,6 @@ func TestCH(t *testing.T) { querier, err := chstorage.NewQuerier(c, chstorage.QuerierOptions{Tables: tables}) require.NoError(t, err) + ctx = zctx.Base(ctx, integration.Logger(t)) runTest(ctx, t, inserter, querier, querier) } diff --git a/integration/tempoe2e/ch_test.go b/integration/tempoe2e/ch_test.go index c989a23e..8186b50b 100644 --- a/integration/tempoe2e/ch_test.go +++ b/integration/tempoe2e/ch_test.go @@ -10,6 +10,7 @@ import ( "github.com/ClickHouse/ch-go/chpool" "github.com/cenkalti/backoff/v4" "github.com/go-faster/errors" + "github.com/go-faster/sdk/zctx" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -76,5 +77,6 @@ func TestCH(t *testing.T) { querier, err := chstorage.NewQuerier(c, chstorage.QuerierOptions{Tables: tables}) require.NoError(t, err) + ctx = zctx.Base(ctx, integration.Logger(t)) runTest(ctx, t, inserter, querier, querier) } diff --git a/integration/tempoe2e/common_test.go b/integration/tempoe2e/common_test.go index 15642f87..4a636d8f 100644 --- a/integration/tempoe2e/common_test.go +++ b/integration/tempoe2e/common_test.go @@ -9,6 +9,7 @@ import ( "os" "strings" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -20,6 +21,7 @@ import ( "github.com/go-faster/oteldb/internal/otelstorage" "github.com/go-faster/oteldb/internal/tempoapi" "github.com/go-faster/oteldb/internal/tempohandler" + "github.com/go-faster/oteldb/internal/traceql" "github.com/go-faster/oteldb/internal/traceql/traceqlengine" "github.com/go-faster/oteldb/internal/tracestorage" ) @@ -54,7 +56,7 @@ func setupDB( if engineQuerier != nil { engine = traceqlengine.NewEngine(engineQuerier, traceqlengine.Options{}) } - api := tempohandler.NewTempoAPI(querier, engine) + api := tempohandler.NewTempoAPI(querier, engine, tempohandler.TempoAPIOptions{}) tempoh, err := tempoapi.NewServer(api) require.NoError(t, err) @@ -80,17 +82,58 @@ func runTest( require.NotEmpty(t, set.Traces) c := setupDB(ctx, t, set, inserter, querier, engineQuerier) + start := tempoapi.NewOptUnixSeconds(set.Start.AsTime().Add(-time.Second)) + end := tempoapi.NewOptUnixSeconds(set.End.AsTime()) t.Run("SearchTags", func(t *testing.T) { a := require.New(t) - r, err := c.SearchTags(ctx) + r, err := c.SearchTags(ctx, tempoapi.SearchTagsParams{ + Start: start, + End: end, + }) a.NoError(err) a.Len(r.TagNames, len(set.Tags)) for _, tagName := range r.TagNames { a.Contains(set.Tags, tagName) } }) + t.Run("SearchTagsV2", func(t *testing.T) { + a := require.New(t) + + r, err := c.SearchTagsV2(ctx, tempoapi.SearchTagsV2Params{ + Start: start, + End: end, + }) + a.NoError(err) + + var spanLen, resourceLen int + for _, scope := range r.Scopes { + switch scope.Name { + case tempoapi.TagScopeSpan: + spanLen = len(scope.Tags) + case tempoapi.TagScopeResource: + resourceLen = len(scope.Tags) + } + + switch scope.Name { + case tempoapi.TagScopeSpan, tempoapi.TagScopeResource: + names := set.Tags + for _, tagName := range scope.Tags { + a.Contains(names, tagName) + } + case tempoapi.TagScopeIntrinsic: + names := traceql.IntrinsicNames() + a.Len(scope.Tags, len(names)) + for _, tagName := range scope.Tags { + a.Contains(names, tagName) + } + default: + t.Fatalf("unexpected scope %q", scope.Name) + } + } + a.Equal(len(set.Tags), spanLen+resourceLen) + }) t.Run("SearchTagValues", func(t *testing.T) { a := require.New(t) @@ -100,7 +143,11 @@ func runTest( tagValues[t.Value] = struct{}{} } - r, err := c.SearchTagValues(ctx, tempoapi.SearchTagValuesParams{TagName: tagName}) + r, err := c.SearchTagValues(ctx, tempoapi.SearchTagValuesParams{ + TagName: tagName, + Start: start, + End: end, + }) a.NoError(err) a.Len(r.TagValues, len(tagValues)) for _, val := range r.TagValues { @@ -109,21 +156,61 @@ func runTest( } }) t.Run("SearchTagValuesV2", func(t *testing.T) { - a := require.New(t) + t.Run("Attribute", func(t *testing.T) { + a := require.New(t) - for tagName, tags := range set.Tags { - tagValues := map[string]struct{}{} - for _, t := range tags { - tagValues[t.Value] = struct{}{} + for tagName, tags := range set.Tags { + tagValues := map[string]struct{}{} + for _, t := range tags { + tagValues[t.Value] = struct{}{} + } + + r, err := c.SearchTagValuesV2(ctx, tempoapi.SearchTagValuesV2Params{ + AttributeSelector: "." + tagName, + Start: start, + End: end, + }) + a.NoError(err) + a.Len(r.TagValues, len(tagValues)) + for _, val := range r.TagValues { + a.Containsf(tagValues, val.Value, "check tag %q", tagName) + } } + }) + t.Run("SpanName", func(t *testing.T) { + a := require.New(t) - r, err := c.SearchTagValuesV2(ctx, tempoapi.SearchTagValuesV2Params{TagName: tagName}) + r, err := c.SearchTagValuesV2(ctx, tempoapi.SearchTagValuesV2Params{ + AttributeSelector: `name`, + Start: start, + End: end, + }) a.NoError(err) - a.Len(r.TagValues, len(tagValues)) - for _, val := range r.TagValues { - a.Containsf(tagValues, val.Value, "check tag %q", tagName) + a.Len(r.TagValues, len(set.SpanNames)) + for _, tag := range r.TagValues { + a.Contains(set.SpanNames, tag.Value) } - } + }) + t.Run("SpanStatus", func(t *testing.T) { + a := require.New(t) + + r, err := c.SearchTagValuesV2(ctx, tempoapi.SearchTagValuesV2Params{ + AttributeSelector: `status`, + Start: start, + End: end, + }) + a.NoError(err) + + statuses := []string{ + "unset", + "ok", + "error", + } + a.Len(r.TagValues, len(statuses)) + for _, tag := range r.TagValues { + a.Contains(statuses, tag.Value) + } + }) }) t.Run("TraceByID", func(t *testing.T) { t.Run("Query", func(t *testing.T) { diff --git a/integration/tempoe2e/tempo_e2e.go b/integration/tempoe2e/tempo_e2e.go index 3a1e6a6e..a6a7d5b7 100644 --- a/integration/tempoe2e/tempo_e2e.go +++ b/integration/tempoe2e/tempo_e2e.go @@ -10,17 +10,24 @@ import ( "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" + "github.com/go-faster/oteldb/internal/otelstorage" + "github.com/go-faster/oteldb/internal/traceql" "github.com/go-faster/oteldb/internal/traceql/traceqlengine" "github.com/go-faster/oteldb/internal/tracestorage" ) // BatchSet is a set of batches. type BatchSet struct { - Batches []ptrace.Traces - Tags map[string][]tracestorage.Tag - Traces map[pcommon.TraceID]Trace - Engine *traceqlengine.Engine - mq traceqlengine.MemoryQuerier + Batches []ptrace.Traces + Tags map[string][]tracestorage.Tag + Traces map[pcommon.TraceID]Trace + SpanNames map[string]struct{} + + Start otelstorage.Timestamp + End otelstorage.Timestamp + + Engine *traceqlengine.Engine + mq traceqlengine.MemoryQuerier } // ParseBatchSet parses JSON batches from given reader. @@ -54,20 +61,19 @@ func (s *BatchSet) addBatch(raw ptrace.Traces) { for i := 0; i < resSpans.Len(); i++ { resSpan := resSpans.At(i) res := resSpan.Resource() - s.addTags(res.Attributes()) + s.addTags(res.Attributes(), traceql.ScopeResource) scopeSpans := resSpan.ScopeSpans() for i := 0; i < scopeSpans.Len(); i++ { scopeSpan := scopeSpans.At(i) scope := scopeSpan.Scope() - s.addTags(scope.Attributes()) + s.addTags(scope.Attributes(), traceql.ScopeResource) spans := scopeSpan.Spans() for i := 0; i < spans.Len(); i++ { span := spans.At(i) // Add span name as well. For some reason, Grafana is looking for it too. - s.addName(span.Name()) - s.addTags(span.Attributes()) + s.addTags(span.Attributes(), traceql.ScopeSpan) s.addSpan(span) s.mq.Add(tracestorage.NewSpanFromOTEL(batchID, res, scope, span)) } @@ -76,11 +82,17 @@ func (s *BatchSet) addBatch(raw ptrace.Traces) { } func (s *BatchSet) addSpan(span ptrace.Span) { - if s.Traces == nil { - s.Traces = map[pcommon.TraceID]Trace{} + if start := span.StartTimestamp(); s.Start == 0 || start < s.Start { + s.Start = start + } + if end := span.EndTimestamp(); s.End == 0 || end > s.End { + s.End = end } traceID := span.TraceID() + if s.Traces == nil { + s.Traces = map[pcommon.TraceID]Trace{} + } t, ok := s.Traces[traceID] if !ok { t = Trace{ @@ -90,6 +102,11 @@ func (s *BatchSet) addSpan(span ptrace.Span) { } t.Spanset[span.SpanID()] = span s.Traces[traceID] = t + + if s.SpanNames == nil { + s.SpanNames = map[string]struct{}{} + } + s.SpanNames[span.Name()] = struct{}{} } // Trace contains spanset fields to check storage behavior. @@ -97,15 +114,7 @@ type Trace struct { Spanset map[pcommon.SpanID]ptrace.Span } -func (s *BatchSet) addName(name string) { - s.addTag(tracestorage.Tag{ - Name: "name", - Value: name, - Type: int32(pcommon.ValueTypeStr), - }) -} - -func (s *BatchSet) addTags(m pcommon.Map) { +func (s *BatchSet) addTags(m pcommon.Map, scope traceql.AttributeScope) { m.Range(func(k string, v pcommon.Value) bool { switch t := v.Type(); t { case pcommon.ValueTypeMap, pcommon.ValueTypeSlice: @@ -114,6 +123,7 @@ func (s *BatchSet) addTags(m pcommon.Map) { Name: k, Value: v.AsString(), Type: int32(t), + Scope: scope, }) } return true diff --git a/internal/chstorage/inserter_traces.go b/internal/chstorage/inserter_traces.go index 6e307a7e..036f30c8 100644 --- a/internal/chstorage/inserter_traces.go +++ b/internal/chstorage/inserter_traces.go @@ -73,18 +73,21 @@ func (i *Inserter) InsertTags(ctx context.Context, tags map[tracestorage.Tag]str name = new(proto.ColStr).LowCardinality() value proto.ColStr valueType proto.ColEnum8 + scopeType proto.ColEnum8 ) for tag := range tags { name.Append(tag.Name) value.Append(tag.Value) valueType.Append(proto.Enum8(tag.Type)) + scopeType.Append(proto.Enum8(tag.Scope)) } input := proto.Input{ {Name: "name", Data: name}, {Name: "value", Data: value}, {Name: "value_type", Data: proto.Wrap(&valueType, valueTypeDDL)}, + {Name: "scope", Data: proto.Wrap(&scopeType, scopeTypeDDL)}, } return i.ch.Do(ctx, ch.Query{ diff --git a/internal/chstorage/querier_traces.go b/internal/chstorage/querier_traces.go index e8853069..f66e699d 100644 --- a/internal/chstorage/querier_traces.go +++ b/internal/chstorage/querier_traces.go @@ -10,6 +10,7 @@ import ( "github.com/ClickHouse/ch-go/proto" "github.com/go-faster/errors" "github.com/go-faster/sdk/zctx" + "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" @@ -88,11 +89,14 @@ func (q *Querier) SearchTags(ctx context.Context, tags map[string]string, opts t } // TagNames returns all available tag names. -func (q *Querier) TagNames(ctx context.Context) (r []string, rerr error) { +func (q *Querier) TagNames(ctx context.Context, opts tracestorage.TagNamesOptions) (r []tracestorage.TagName, rerr error) { table := q.tables.Tags ctx, span := q.tracer.Start(ctx, "TagNames", trace.WithAttributes( + attribute.Int64("chstorage.range.start", int64(opts.Start)), + attribute.Int64("chstorage.range.end", int64(opts.End)), + attribute.Stringer("chstorage.scope", opts.Scope), attribute.String("chstorage.table", table), ), ) @@ -103,16 +107,37 @@ func (q *Querier) TagNames(ctx context.Context) (r []string, rerr error) { span.End() }() - data := new(proto.ColStr).LowCardinality() + var query strings.Builder + fmt.Fprintf(&query, "SELECT DISTINCT name, scope FROM %#q", table) + switch scope := opts.Scope; scope { + case traceql.ScopeNone: + case traceql.ScopeResource: + // Tempo merges scope attributes and resource attributes. + fmt.Fprintf(&query, " WHERE scope IN (%d, %d)", scope, traceql.ScopeInstrumentation) + case traceql.ScopeSpan: + fmt.Fprintf(&query, " WHERE scope = %d", scope) + default: + return r, errors.Errorf("unexpected scope %v", scope) + } + + var ( + name = new(proto.ColStr).LowCardinality() + scope proto.ColEnum8 + ) if err := q.ch.Do(ctx, ch.Query{ Logger: zctx.From(ctx).Named("ch"), - Body: fmt.Sprintf("SELECT DISTINCT name FROM %#q", table), - Result: proto.ResultColumn{ - Name: "name", - Data: data, + Body: query.String(), + Result: proto.Results{ + {Name: "name", Data: name}, + {Name: "scope", Data: proto.Wrap(&scope, scopeTypeDDL)}, }, OnResult: func(ctx context.Context, block proto.Block) error { - r = append(r, data.Values...) + for i := 0; i < name.Rows(); i++ { + r = append(r, tracestorage.TagName{ + Name: name.Row(i), + Scope: traceql.AttributeScope(scope.Row(i)), + }) + } return nil }, }); err != nil { @@ -122,12 +147,119 @@ func (q *Querier) TagNames(ctx context.Context) (r []string, rerr error) { } // TagValues returns all available tag values for given tag. -func (q *Querier) TagValues(ctx context.Context, tagName string) (_ iterators.Iterator[tracestorage.Tag], rerr error) { +func (q *Querier) TagValues(ctx context.Context, tag traceql.Attribute, opts tracestorage.TagValuesOptions) (_ iterators.Iterator[tracestorage.Tag], rerr error) { + ctx, span := q.tracer.Start(ctx, "TagValues", + trace.WithAttributes( + attribute.Int64("chstorage.range.start", int64(opts.Start)), + attribute.Int64("chstorage.range.end", int64(opts.End)), + attribute.Stringer("chstorage.tag", tag), + ), + ) + defer func() { + if rerr != nil { + span.RecordError(rerr) + } + span.End() + }() + + switch tag.Prop { + case traceql.SpanAttribute: + return q.attributeValues(ctx, tag, opts) + case traceql.SpanStatus: + // TODO(tdakkota): probably we should do a proper query. + name := tag.String() + statuses := []tracestorage.Tag{ + {Name: name, Value: "unset", Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: "ok", Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: "error", Type: int32(pcommon.ValueTypeStr)}, + } + return iterators.Slice(statuses), nil + case traceql.SpanKind: + // TODO(tdakkota): probably we should do a proper query. + name := tag.String() + kinds := []tracestorage.Tag{ + {Name: name, Value: "unspecified", Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: "internal", Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: "server", Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: "client", Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: "producer", Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: "consumer", Type: int32(pcommon.ValueTypeStr)}, + } + return iterators.Slice(kinds), nil + case traceql.SpanDuration, traceql.SpanChildCount, traceql.SpanParent, traceql.TraceDuration: + // Too high cardinality to query. + return iterators.Empty[tracestorage.Tag](), nil + case traceql.SpanName, traceql.RootSpanName: + // FIXME(tdakkota): we don't check if span name is actually coming from a root span. + return q.spanNames(ctx, opts) + case traceql.RootServiceName: + // FIXME(tdakkota): we don't check if service.name actually coming from a root span. + // + // Equals to `resource.service.name`. + tag = traceql.Attribute{Name: "service.name", Scope: traceql.ScopeResource} + return q.attributeValues(ctx, tag, opts) + default: + return nil, errors.Errorf("unexpected span property %v (attribute: %q)", tag.Prop, tag) + } +} + +func (q *Querier) spanNames(ctx context.Context, opts tracestorage.TagValuesOptions) (_ iterators.Iterator[tracestorage.Tag], rerr error) { + table := q.tables.Spans + + ctx, span := q.tracer.Start(ctx, "spanNames", + trace.WithAttributes( + attribute.String("chstorage.table", table), + ), + ) + defer func() { + if rerr != nil { + span.RecordError(rerr) + } + span.End() + }() + + var query strings.Builder + fmt.Fprintf(&query, `SELECT DISTINCT name FROM %#q WHERE true`, table) + if s := opts.Start; s != 0 { + fmt.Fprintf(&query, " AND toUnixTimestamp64Nano(start) >= %d", s) + } + if e := opts.End; e != 0 { + fmt.Fprintf(&query, " AND toUnixTimestamp64Nano(end) <= %d", e) + } + + var ( + name = new(proto.ColStr).LowCardinality() + r []tracestorage.Tag + ) + if err := q.ch.Do(ctx, ch.Query{ + Logger: zctx.From(ctx).Named("ch"), + Body: query.String(), + Result: proto.Results{ + {Name: "name", Data: name}, + }, + OnResult: func(ctx context.Context, block proto.Block) error { + for i := 0; i < name.Rows(); i++ { + r = append(r, tracestorage.Tag{ + Name: "name", + Value: name.Row(i), + Type: int32(pcommon.ValueTypeStr), + Scope: traceql.ScopeNone, + }) + } + return nil + }, + }); err != nil { + return nil, errors.Wrap(err, "query") + } + + return iterators.Slice(r), nil +} + +func (q *Querier) attributeValues(ctx context.Context, tag traceql.Attribute, _ tracestorage.TagValuesOptions) (_ iterators.Iterator[tracestorage.Tag], rerr error) { table := q.tables.Tags - ctx, span := q.tracer.Start(ctx, "TagValues", + ctx, span := q.tracer.Start(ctx, "attributeValues", trace.WithAttributes( - attribute.String("chstorage.tag_to_query", tagName), attribute.String("chstorage.table", table), ), ) @@ -138,16 +270,29 @@ func (q *Querier) TagValues(ctx context.Context, tagName string) (_ iterators.It span.End() }() + // FIXME(tdakkota): respect time range parameters. + var query strings.Builder + fmt.Fprintf(&query, `SELECT DISTINCT value, value_type FROM %#q WHERE name = %s`, table, singleQuoted(tag.Name)) + switch scope := tag.Scope; scope { + case traceql.ScopeNone: + case traceql.ScopeResource: + // Tempo merges scope attributes and resource attributes. + fmt.Fprintf(&query, " AND scope IN (%d, %d)", scope, traceql.ScopeInstrumentation) + case traceql.ScopeSpan: + fmt.Fprintf(&query, " AND scope = %d", scope) + default: + return nil, errors.Errorf("unexpected scope %v", scope) + } + var ( value proto.ColStr valueType proto.ColEnum8 r []tracestorage.Tag ) - if err := q.ch.Do(ctx, ch.Query{ Logger: zctx.From(ctx).Named("ch"), - Body: fmt.Sprintf("SELECT DISTINCT value, value_type FROM %#q WHERE name = %s", table, singleQuoted(tagName)), + Body: query.String(), Result: proto.Results{ {Name: "value", Data: &value}, {Name: "value_type", Data: proto.Wrap(&valueType, valueTypeDDL)}, @@ -156,7 +301,7 @@ func (q *Querier) TagValues(ctx context.Context, tagName string) (_ iterators.It return value.ForEach(func(i int, value string) error { typ := valueType.Row(i) r = append(r, tracestorage.Tag{ - Name: tagName, + Name: tag.Name, Value: value, Type: int32(typ), }) diff --git a/internal/chstorage/schema_traces.go b/internal/chstorage/schema_traces.go index 9455ec3d..3588041f 100644 --- a/internal/chstorage/schema_traces.go +++ b/internal/chstorage/schema_traces.go @@ -54,9 +54,11 @@ ORDER BY (service_namespace, service_name, resource, start) ( name LowCardinality(String), value String, - value_type Enum8(` + valueTypeDDL + `) + value_type Enum8(` + valueTypeDDL + `), + scope Enum8(` + scopeTypeDDL + `) ) ENGINE = ReplacingMergeTree ORDER BY (value_type, name, value);` valueTypeDDL = `'EMPTY' = 0,'STR' = 1,'INT' = 2,'DOUBLE' = 3,'BOOL' = 4,'MAP' = 5,'SLICE' = 6,'BYTES' = 7` + scopeTypeDDL = `'NONE' = 0, 'RESOURCE' = 1, 'SPAN' = 2` ) diff --git a/internal/generate.go b/internal/generate.go index 170c8e36..3b3b12a9 100644 --- a/internal/generate.go +++ b/internal/generate.go @@ -1,10 +1,10 @@ // Package internal contains go:generate annotations. package internal -//go:generate go run github.com/ogen-go/ogen/cmd/ogen -v --target tempoapi --package tempoapi ../_oas/tempo.yml -//go:generate go run github.com/ogen-go/ogen/cmd/ogen -v --target lokiapi --package lokiapi ../_oas/loki.yml -//go:generate go run github.com/ogen-go/ogen/cmd/ogen -v --target promapi --package promapi ../_oas/prometheus.yml -//go:generate go run github.com/ogen-go/ogen/cmd/ogen -v --target pyroscopeapi --package pyroscopeapi ../_oas/pyroscope.yml -//go:generate go run github.com/ogen-go/ogen/cmd/ogen -v --target sentryapi --package sentryapi ../_oas/sentry.yml +//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target tempoapi --package tempoapi ../_oas/tempo.yml +//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target lokiapi --package lokiapi ../_oas/loki.yml +//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target promapi --package promapi ../_oas/prometheus.yml +//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target pyroscopeapi --package pyroscopeapi ../_oas/pyroscope.yml +//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target sentryapi --package sentryapi ../_oas/sentry.yml -//go:generate go run github.com/ogen-go/ogen/cmd/ogen -v --target otelbotapi --package otelbotapi ../_oas/otelbot.yml +//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target otelbotapi --package otelbotapi ../_oas/otelbot.yml diff --git a/internal/lokihandler/lokihandler.go b/internal/lokihandler/lokihandler.go index e4b84f40..74f9ab39 100644 --- a/internal/lokihandler/lokihandler.go +++ b/internal/lokihandler/lokihandler.go @@ -20,14 +20,22 @@ import ( "github.com/go-faster/oteldb/internal/otelstorage" ) -var _ lokiapi.Handler = (*LokiAPI)(nil) - // LokiAPI implements lokiapi.Handler. type LokiAPI struct { q logstorage.Querier engine *logqlengine.Engine } +var _ lokiapi.Handler = (*LokiAPI)(nil) + +// NewLokiAPI creates new LokiAPI. +func NewLokiAPI(q logstorage.Querier, engine *logqlengine.Engine) *LokiAPI { + return &LokiAPI{ + q: q, + engine: engine, + } +} + // IndexStats implements indexStats operation. // // Get index stats. @@ -38,14 +46,6 @@ func (h *LokiAPI) IndexStats(context.Context, lokiapi.IndexStatsParams) (*lokiap return &lokiapi.IndexStats{}, nil } -// NewLokiAPI creates new LokiAPI. -func NewLokiAPI(q logstorage.Querier, engine *logqlengine.Engine) *LokiAPI { - return &LokiAPI{ - q: q, - engine: engine, - } -} - // LabelValues implements labelValues operation. // Get values of label. // diff --git a/internal/promhandler/promhandler.go b/internal/promhandler/promhandler.go index ca25c000..e9fbebe6 100644 --- a/internal/promhandler/promhandler.go +++ b/internal/promhandler/promhandler.go @@ -18,8 +18,6 @@ import ( "github.com/go-faster/oteldb/internal/promapi" ) -var _ promapi.Handler = (*PromAPI)(nil) - // Engine is a Prometheus engine interface. type Engine interface { NewInstantQuery(ctx context.Context, q storage.Queryable, opts promql.QueryOpts, qs string, ts time.Time) (promql.Query, error) @@ -35,6 +33,8 @@ type PromAPI struct { lookbackDelta time.Duration } +var _ promapi.Handler = (*PromAPI)(nil) + // NewPromAPI creates new PromAPI. func NewPromAPI( eng Engine, diff --git a/internal/tempoapi/oas_client_gen.go b/internal/tempoapi/oas_client_gen.go index 54833d0c..979ab545 100644 --- a/internal/tempoapi/oas_client_gen.go +++ b/internal/tempoapi/oas_client_gen.go @@ -46,14 +46,20 @@ type Invoker interface { // This endpoint retrieves all discovered values and their data types for the given TraceQL // identifier. // - // GET /api/v2/search/tag/{tag_name}/values + // GET /api/v2/search/tag/{attribute_selector}/values SearchTagValuesV2(ctx context.Context, params SearchTagValuesV2Params) (*TagValuesV2, error) // SearchTags invokes searchTags operation. // // This endpoint retrieves all discovered tag names that can be used in search. // // GET /api/search/tags - SearchTags(ctx context.Context) (*TagNames, error) + SearchTags(ctx context.Context, params SearchTagsParams) (*TagNames, error) + // SearchTagsV2 invokes searchTagsV2 operation. + // + // This endpoint retrieves all discovered tag names that can be used in search. + // + // GET /api/v2/search/tags + SearchTagsV2(ctx context.Context, params SearchTagsV2Params) (*TagNamesV2, error) // TraceByID invokes traceByID operation. // // Querying traces by id. @@ -357,6 +363,23 @@ func (c *Client) sendSearch(ctx context.Context, params SearchParams) (res *Trac return res, errors.Wrap(err, "encode query") } } + { + // Encode "spss" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "spss", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Spss.Get(); ok { + return e.EncodeValue(conv.IntToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } u.RawQuery = q.Values().Encode() stage = "EncodeRequest" @@ -450,6 +473,61 @@ func (c *Client) sendSearchTagValues(ctx context.Context, params SearchTagValues pathParts[2] = "/values" uri.AddPathParts(u, pathParts[:]...) + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "q" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "q", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Q.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + { + // Encode "start" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "start", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Start.Get(); ok { + return e.EncodeValue(conv.UnixSecondsToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + { + // Encode "end" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "end", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.End.Get(); ok { + return e.EncodeValue(conv.UnixSecondsToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + stage = "EncodeRequest" r, err := ht.NewRequest(ctx, "GET", u) if err != nil { @@ -477,7 +555,7 @@ func (c *Client) sendSearchTagValues(ctx context.Context, params SearchTagValues // This endpoint retrieves all discovered values and their data types for the given TraceQL // identifier. // -// GET /api/v2/search/tag/{tag_name}/values +// GET /api/v2/search/tag/{attribute_selector}/values func (c *Client) SearchTagValuesV2(ctx context.Context, params SearchTagValuesV2Params) (*TagValuesV2, error) { res, err := c.sendSearchTagValuesV2(ctx, params) return res, err @@ -487,7 +565,7 @@ func (c *Client) sendSearchTagValuesV2(ctx context.Context, params SearchTagValu otelAttrs := []attribute.KeyValue{ otelogen.OperationID("searchTagValuesV2"), semconv.HTTPMethodKey.String("GET"), - semconv.HTTPRouteKey.String("/api/v2/search/tag/{tag_name}/values"), + semconv.HTTPRouteKey.String("/api/v2/search/tag/{attribute_selector}/values"), } // Run stopwatch. @@ -522,14 +600,14 @@ func (c *Client) sendSearchTagValuesV2(ctx context.Context, params SearchTagValu var pathParts [3]string pathParts[0] = "/api/v2/search/tag/" { - // Encode "tag_name" parameter. + // Encode "attribute_selector" parameter. e := uri.NewPathEncoder(uri.PathEncoderConfig{ - Param: "tag_name", + Param: "attribute_selector", Style: uri.PathStyleSimple, Explode: false, }) if err := func() error { - return e.EncodeValue(conv.StringToString(params.TagName)) + return e.EncodeValue(conv.StringToString(params.AttributeSelector)) }(); err != nil { return res, errors.Wrap(err, "encode path") } @@ -542,6 +620,61 @@ func (c *Client) sendSearchTagValuesV2(ctx context.Context, params SearchTagValu pathParts[2] = "/values" uri.AddPathParts(u, pathParts[:]...) + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "q" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "q", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Q.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + { + // Encode "start" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "start", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Start.Get(); ok { + return e.EncodeValue(conv.UnixSecondsToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + { + // Encode "end" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "end", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.End.Get(); ok { + return e.EncodeValue(conv.UnixSecondsToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + stage = "EncodeRequest" r, err := ht.NewRequest(ctx, "GET", u) if err != nil { @@ -569,12 +702,12 @@ func (c *Client) sendSearchTagValuesV2(ctx context.Context, params SearchTagValu // This endpoint retrieves all discovered tag names that can be used in search. // // GET /api/search/tags -func (c *Client) SearchTags(ctx context.Context) (*TagNames, error) { - res, err := c.sendSearchTags(ctx) +func (c *Client) SearchTags(ctx context.Context, params SearchTagsParams) (*TagNames, error) { + res, err := c.sendSearchTags(ctx, params) return res, err } -func (c *Client) sendSearchTags(ctx context.Context) (res *TagNames, err error) { +func (c *Client) sendSearchTags(ctx context.Context, params SearchTagsParams) (res *TagNames, err error) { otelAttrs := []attribute.KeyValue{ otelogen.OperationID("searchTags"), semconv.HTTPMethodKey.String("GET"), @@ -614,6 +747,61 @@ func (c *Client) sendSearchTags(ctx context.Context) (res *TagNames, err error) pathParts[0] = "/api/search/tags" uri.AddPathParts(u, pathParts[:]...) + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "scope" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "scope", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Scope.Get(); ok { + return e.EncodeValue(conv.StringToString(string(val))) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + { + // Encode "start" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "start", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Start.Get(); ok { + return e.EncodeValue(conv.UnixSecondsToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + { + // Encode "end" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "end", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.End.Get(); ok { + return e.EncodeValue(conv.UnixSecondsToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + stage = "EncodeRequest" r, err := ht.NewRequest(ctx, "GET", u) if err != nil { @@ -636,6 +824,133 @@ func (c *Client) sendSearchTags(ctx context.Context) (res *TagNames, err error) return result, nil } +// SearchTagsV2 invokes searchTagsV2 operation. +// +// This endpoint retrieves all discovered tag names that can be used in search. +// +// GET /api/v2/search/tags +func (c *Client) SearchTagsV2(ctx context.Context, params SearchTagsV2Params) (*TagNamesV2, error) { + res, err := c.sendSearchTagsV2(ctx, params) + return res, err +} + +func (c *Client) sendSearchTagsV2(ctx context.Context, params SearchTagsV2Params) (res *TagNamesV2, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("searchTagsV2"), + semconv.HTTPMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/api/v2/search/tags"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(float64(elapsedDuration)/float64(time.Millisecond)), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, "SearchTagsV2", + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/api/v2/search/tags" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "scope" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "scope", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Scope.Get(); ok { + return e.EncodeValue(conv.StringToString(string(val))) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + { + // Encode "start" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "start", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Start.Get(); ok { + return e.EncodeValue(conv.UnixSecondsToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + { + // Encode "end" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "end", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.End.Get(); ok { + return e.EncodeValue(conv.UnixSecondsToString(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "GET", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeSearchTagsV2Response(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // TraceByID invokes traceByID operation. // // Querying traces by id. diff --git a/internal/tempoapi/oas_faker_gen.go b/internal/tempoapi/oas_faker_gen.go index 9a9f8c03..681ee5c4 100644 --- a/internal/tempoapi/oas_faker_gen.go +++ b/internal/tempoapi/oas_faker_gen.go @@ -150,6 +150,27 @@ func (s *OptTempoSpanSet) SetFake() { s.SetTo(elem) } +// SetFake set fake values. +func (s *ScopeTags) SetFake() { + { + { + s.Name.SetFake() + } + } + { + { + s.Tags = nil + for i := 0; i < 0; i++ { + var elem string + { + elem = "string" + } + s.Tags = append(s.Tags, elem) + } + } + } +} + // SetFake set fake values. func (s *StringValue) SetFake() { { @@ -175,6 +196,27 @@ func (s *TagNames) SetFake() { } } +// SetFake set fake values. +func (s *TagNamesV2) SetFake() { + { + { + s.Scopes = nil + for i := 0; i < 0; i++ { + var elem ScopeTags + { + elem.SetFake() + } + s.Scopes = append(s.Scopes, elem) + } + } + } +} + +// SetFake set fake values. +func (s *TagScope) SetFake() { + *s = TagScopeSpan +} + // SetFake set fake values. func (s *TagValue) SetFake() { { diff --git a/internal/tempoapi/oas_handlers_gen.go b/internal/tempoapi/oas_handlers_gen.go index cf292554..f51e3b26 100644 --- a/internal/tempoapi/oas_handlers_gen.go +++ b/internal/tempoapi/oas_handlers_gen.go @@ -220,6 +220,10 @@ func (s *Server) handleSearchRequest(args [0]string, argsEscaped bool, w http.Re Name: "end", In: "query", }: params.End, + { + Name: "spss", + In: "query", + }: params.Spss, }, Raw: r, } @@ -343,6 +347,18 @@ func (s *Server) handleSearchTagValuesRequest(args [1]string, argsEscaped bool, Name: "tag_name", In: "path", }: params.TagName, + { + Name: "q", + In: "query", + }: params.Q, + { + Name: "start", + In: "query", + }: params.Start, + { + Name: "end", + In: "query", + }: params.End, }, Raw: r, } @@ -399,12 +415,12 @@ func (s *Server) handleSearchTagValuesRequest(args [1]string, argsEscaped bool, // This endpoint retrieves all discovered values and their data types for the given TraceQL // identifier. // -// GET /api/v2/search/tag/{tag_name}/values +// GET /api/v2/search/tag/{attribute_selector}/values func (s *Server) handleSearchTagValuesV2Request(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { otelAttrs := []attribute.KeyValue{ otelogen.OperationID("searchTagValuesV2"), semconv.HTTPMethodKey.String("GET"), - semconv.HTTPRouteKey.String("/api/v2/search/tag/{tag_name}/values"), + semconv.HTTPRouteKey.String("/api/v2/search/tag/{attribute_selector}/values"), } // Start a span for this request. @@ -464,9 +480,21 @@ func (s *Server) handleSearchTagValuesV2Request(args [1]string, argsEscaped bool Body: nil, Params: middleware.Parameters{ { - Name: "tag_name", + Name: "attribute_selector", In: "path", - }: params.TagName, + }: params.AttributeSelector, + { + Name: "q", + In: "query", + }: params.Q, + { + Name: "start", + In: "query", + }: params.Start, + { + Name: "end", + In: "query", + }: params.End, }, Raw: r, } @@ -560,8 +588,22 @@ func (s *Server) handleSearchTagsRequest(args [0]string, argsEscaped bool, w htt span.SetStatus(codes.Error, stage) s.errors.Add(ctx, 1, metric.WithAttributeSet(labeler.AttributeSet())) } - err error + err error + opErrContext = ogenerrors.OperationContext{ + Name: "SearchTags", + ID: "searchTags", + } ) + params, err := decodeSearchTagsParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } var response *TagNames if m := s.cfg.Middleware; m != nil { @@ -571,13 +613,26 @@ func (s *Server) handleSearchTagsRequest(args [0]string, argsEscaped bool, w htt OperationSummary: "", OperationID: "searchTags", Body: nil, - Params: middleware.Parameters{}, - Raw: r, + Params: middleware.Parameters{ + { + Name: "scope", + In: "query", + }: params.Scope, + { + Name: "start", + In: "query", + }: params.Start, + { + Name: "end", + In: "query", + }: params.End, + }, + Raw: r, } type ( Request = struct{} - Params = struct{} + Params = SearchTagsParams Response = *TagNames ) response, err = middleware.HookMiddleware[ @@ -587,14 +642,14 @@ func (s *Server) handleSearchTagsRequest(args [0]string, argsEscaped bool, w htt ]( m, mreq, - nil, + unpackSearchTagsParams, func(ctx context.Context, request Request, params Params) (response Response, err error) { - response, err = s.h.SearchTags(ctx) + response, err = s.h.SearchTags(ctx, params) return response, err }, ) } else { - response, err = s.h.SearchTags(ctx) + response, err = s.h.SearchTags(ctx, params) } if err != nil { if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { @@ -622,6 +677,137 @@ func (s *Server) handleSearchTagsRequest(args [0]string, argsEscaped bool, w htt } } +// handleSearchTagsV2Request handles searchTagsV2 operation. +// +// This endpoint retrieves all discovered tag names that can be used in search. +// +// GET /api/v2/search/tags +func (s *Server) handleSearchTagsV2Request(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("searchTagsV2"), + semconv.HTTPMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/api/v2/search/tags"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), "SearchTagsV2", + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + attrOpt := metric.WithAttributeSet(labeler.AttributeSet()) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(float64(elapsedDuration)/float64(time.Millisecond)), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + s.errors.Add(ctx, 1, metric.WithAttributeSet(labeler.AttributeSet())) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: "SearchTagsV2", + ID: "searchTagsV2", + } + ) + params, err := decodeSearchTagsV2Params(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *TagNamesV2 + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: "SearchTagsV2", + OperationSummary: "", + OperationID: "searchTagsV2", + Body: nil, + Params: middleware.Parameters{ + { + Name: "scope", + In: "query", + }: params.Scope, + { + Name: "start", + In: "query", + }: params.Start, + { + Name: "end", + In: "query", + }: params.End, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = SearchTagsV2Params + Response = *TagNamesV2 + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackSearchTagsV2Params, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.SearchTagsV2(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.SearchTagsV2(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeSearchTagsV2Response(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleTraceByIDRequest handles traceByID operation. // // Querying traces by id. diff --git a/internal/tempoapi/oas_json_gen.go b/internal/tempoapi/oas_json_gen.go index a340086c..a0649bc7 100644 --- a/internal/tempoapi/oas_json_gen.go +++ b/internal/tempoapi/oas_json_gen.go @@ -1084,6 +1084,130 @@ func (s *OptTempoSpanSet) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode implements json.Marshaler. +func (s *ScopeTags) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ScopeTags) encodeFields(e *jx.Encoder) { + { + e.FieldStart("name") + s.Name.Encode(e) + } + { + if s.Tags != nil { + e.FieldStart("tags") + e.ArrStart() + for _, elem := range s.Tags { + e.Str(elem) + } + e.ArrEnd() + } + } +} + +var jsonFieldsNameOfScopeTags = [2]string{ + 0: "name", + 1: "tags", +} + +// Decode decodes ScopeTags from json. +func (s *ScopeTags) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ScopeTags to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "name": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Name.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"name\"") + } + case "tags": + if err := func() error { + s.Tags = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + s.Tags = append(s.Tags, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"tags\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ScopeTags") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfScopeTags) { + name = jsonFieldsNameOfScopeTags[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ScopeTags) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ScopeTags) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *StringValue) Encode(e *jx.Encoder) { e.ObjStart() @@ -1256,6 +1380,124 @@ func (s *TagNames) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode implements json.Marshaler. +func (s *TagNamesV2) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *TagNamesV2) encodeFields(e *jx.Encoder) { + { + if s.Scopes != nil { + e.FieldStart("scopes") + e.ArrStart() + for _, elem := range s.Scopes { + elem.Encode(e) + } + e.ArrEnd() + } + } +} + +var jsonFieldsNameOfTagNamesV2 = [1]string{ + 0: "scopes", +} + +// Decode decodes TagNamesV2 from json. +func (s *TagNamesV2) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode TagNamesV2 to nil") + } + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "scopes": + if err := func() error { + s.Scopes = make([]ScopeTags, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem ScopeTags + if err := elem.Decode(d); err != nil { + return err + } + s.Scopes = append(s.Scopes, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"scopes\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode TagNamesV2") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *TagNamesV2) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *TagNamesV2) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes TagScope as json. +func (s TagScope) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes TagScope from json. +func (s *TagScope) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode TagScope to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch TagScope(v) { + case TagScopeSpan: + *s = TagScopeSpan + case TagScopeResource: + *s = TagScopeResource + case TagScopeIntrinsic: + *s = TagScopeIntrinsic + case TagScopeNone: + *s = TagScopeNone + default: + *s = TagScope(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s TagScope) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *TagScope) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *TagValue) Encode(e *jx.Encoder) { e.ObjStart() diff --git a/internal/tempoapi/oas_parameters_gen.go b/internal/tempoapi/oas_parameters_gen.go index 9acb01d4..9150f34e 100644 --- a/internal/tempoapi/oas_parameters_gen.go +++ b/internal/tempoapi/oas_parameters_gen.go @@ -41,6 +41,8 @@ type SearchParams struct { // ingesters. // If the parameters are provided, it will search the backend as well. End OptUnixSeconds + // Limit the number of spans per span-set. Default value is 3. + Spss OptInt } func unpackSearchParams(packed middleware.Parameters) (params SearchParams) { @@ -107,6 +109,15 @@ func unpackSearchParams(packed middleware.Parameters) (params SearchParams) { params.End = v.(OptUnixSeconds) } } + { + key := middleware.ParameterKey{ + Name: "spss", + In: "query", + } + if v, ok := packed[key]; ok { + params.Spss = v.(OptInt) + } + } return params } @@ -399,6 +410,47 @@ func decodeSearchParams(args [0]string, argsEscaped bool, r *http.Request) (para Err: err, } } + // Decode query: spss. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "spss", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotSpssVal int + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt(val) + if err != nil { + return err + } + + paramsDotSpssVal = c + return nil + }(); err != nil { + return err + } + params.Spss.SetTo(paramsDotSpssVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "spss", + In: "query", + Err: err, + } + } return params, nil } @@ -406,6 +458,20 @@ func decodeSearchParams(args [0]string, argsEscaped bool, r *http.Request) (para type SearchTagValuesParams struct { // Tag name. TagName string + // If provided, the tag values returned by the API are filtered to only return values seen on spans + // matching your filter parameters. + // Queries can be incomplete: for example, `{ .cluster = }`. Tempo extracts only the valid matchers + // and build a valid query. + // Only queries with a single selector `{}`` and AND `&&` operators are supported. + // - Example supported: `{ .cluster = "us-east-1" && .service = "frontend" }` + // - Example unsupported: `{ .cluster = "us-east-1" || .service = "frontend" } && { .cluster = + // "us-east-2" }`. + Q OptString + // Along with `end` define a time range from which tags should be returned. + Start OptUnixSeconds + // Along with `start` define a time range from which tags should be returned. + // Providing both `start` and `end` includes blocks for the specified time range only. + End OptUnixSeconds } func unpackSearchTagValuesParams(packed middleware.Parameters) (params SearchTagValuesParams) { @@ -416,10 +482,38 @@ func unpackSearchTagValuesParams(packed middleware.Parameters) (params SearchTag } params.TagName = packed[key].(string) } + { + key := middleware.ParameterKey{ + Name: "q", + In: "query", + } + if v, ok := packed[key]; ok { + params.Q = v.(OptString) + } + } + { + key := middleware.ParameterKey{ + Name: "start", + In: "query", + } + if v, ok := packed[key]; ok { + params.Start = v.(OptUnixSeconds) + } + } + { + key := middleware.ParameterKey{ + Name: "end", + In: "query", + } + if v, ok := packed[key]; ok { + params.End = v.(OptUnixSeconds) + } + } return params } func decodeSearchTagValuesParams(args [1]string, argsEscaped bool, r *http.Request) (params SearchTagValuesParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) // Decode path: tag_name. if err := func() error { param := args[0] @@ -465,28 +559,193 @@ func decodeSearchTagValuesParams(args [1]string, argsEscaped bool, r *http.Reque Err: err, } } + // Decode query: q. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "q", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotQVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + paramsDotQVal = c + return nil + }(); err != nil { + return err + } + params.Q.SetTo(paramsDotQVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "q", + In: "query", + Err: err, + } + } + // Decode query: start. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "start", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotStartVal time.Time + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUnixSeconds(val) + if err != nil { + return err + } + + paramsDotStartVal = c + return nil + }(); err != nil { + return err + } + params.Start.SetTo(paramsDotStartVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "start", + In: "query", + Err: err, + } + } + // Decode query: end. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "end", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotEndVal time.Time + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUnixSeconds(val) + if err != nil { + return err + } + + paramsDotEndVal = c + return nil + }(); err != nil { + return err + } + params.End.SetTo(paramsDotEndVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "end", + In: "query", + Err: err, + } + } return params, nil } // SearchTagValuesV2Params is parameters of searchTagValuesV2 operation. type SearchTagValuesV2Params struct { - // Tag name. - TagName string + // TraceQL attribute selector (`.service.name`, `resource.service.name`, etc.). + AttributeSelector string + // If provided, the tag values returned by the API are filtered to only return values seen on spans + // matching your filter parameters. + // Queries can be incomplete: for example, `{ .cluster = }`. Tempo extracts only the valid matchers + // and build a valid query. + // Only queries with a single selector `{}`` and AND `&&` operators are supported. + // - Example supported: `{ .cluster = "us-east-1" && .service = "frontend" }` + // - Example unsupported: `{ .cluster = "us-east-1" || .service = "frontend" } && { .cluster = + // "us-east-2" }`. + Q OptString + // Along with `end` define a time range from which tags should be returned. + Start OptUnixSeconds + // Along with `start` define a time range from which tags should be returned. + // Providing both `start` and `end` includes blocks for the specified time range only. + End OptUnixSeconds } func unpackSearchTagValuesV2Params(packed middleware.Parameters) (params SearchTagValuesV2Params) { { key := middleware.ParameterKey{ - Name: "tag_name", + Name: "attribute_selector", In: "path", } - params.TagName = packed[key].(string) + params.AttributeSelector = packed[key].(string) + } + { + key := middleware.ParameterKey{ + Name: "q", + In: "query", + } + if v, ok := packed[key]; ok { + params.Q = v.(OptString) + } + } + { + key := middleware.ParameterKey{ + Name: "start", + In: "query", + } + if v, ok := packed[key]; ok { + params.Start = v.(OptUnixSeconds) + } + } + { + key := middleware.ParameterKey{ + Name: "end", + In: "query", + } + if v, ok := packed[key]; ok { + params.End = v.(OptUnixSeconds) + } } return params } func decodeSearchTagValuesV2Params(args [1]string, argsEscaped bool, r *http.Request) (params SearchTagValuesV2Params, _ error) { - // Decode path: tag_name. + q := uri.NewQueryDecoder(r.URL.Query()) + // Decode path: attribute_selector. if err := func() error { param := args[0] if argsEscaped { @@ -498,7 +757,7 @@ func decodeSearchTagValuesV2Params(args [1]string, argsEscaped bool, r *http.Req } if len(param) > 0 { d := uri.NewPathDecoder(uri.PathDecoderConfig{ - Param: "tag_name", + Param: "attribute_selector", Value: param, Style: uri.PathStyleSimple, Explode: false, @@ -515,7 +774,7 @@ func decodeSearchTagValuesV2Params(args [1]string, argsEscaped bool, r *http.Req return err } - params.TagName = c + params.AttributeSelector = c return nil }(); err != nil { return err @@ -526,11 +785,506 @@ func decodeSearchTagValuesV2Params(args [1]string, argsEscaped bool, r *http.Req return nil }(); err != nil { return params, &ogenerrors.DecodeParamError{ - Name: "tag_name", + Name: "attribute_selector", In: "path", Err: err, } } + // Decode query: q. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "q", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotQVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + paramsDotQVal = c + return nil + }(); err != nil { + return err + } + params.Q.SetTo(paramsDotQVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "q", + In: "query", + Err: err, + } + } + // Decode query: start. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "start", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotStartVal time.Time + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUnixSeconds(val) + if err != nil { + return err + } + + paramsDotStartVal = c + return nil + }(); err != nil { + return err + } + params.Start.SetTo(paramsDotStartVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "start", + In: "query", + Err: err, + } + } + // Decode query: end. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "end", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotEndVal time.Time + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUnixSeconds(val) + if err != nil { + return err + } + + paramsDotEndVal = c + return nil + }(); err != nil { + return err + } + params.End.SetTo(paramsDotEndVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "end", + In: "query", + Err: err, + } + } + return params, nil +} + +// SearchTagsParams is parameters of searchTags operation. +type SearchTagsParams struct { + // Specifies the scope of the tags, this is an optional parameter, if not specified it means all + // scopes. + Scope OptTagScope + // Along with `end` define a time range from which tags should be returned. + Start OptUnixSeconds + // Along with `start` define a time range from which tags should be returned. + // Providing both `start` and `end` includes blocks for the specified time range only. + End OptUnixSeconds +} + +func unpackSearchTagsParams(packed middleware.Parameters) (params SearchTagsParams) { + { + key := middleware.ParameterKey{ + Name: "scope", + In: "query", + } + if v, ok := packed[key]; ok { + params.Scope = v.(OptTagScope) + } + } + { + key := middleware.ParameterKey{ + Name: "start", + In: "query", + } + if v, ok := packed[key]; ok { + params.Start = v.(OptUnixSeconds) + } + } + { + key := middleware.ParameterKey{ + Name: "end", + In: "query", + } + if v, ok := packed[key]; ok { + params.End = v.(OptUnixSeconds) + } + } + return params +} + +func decodeSearchTagsParams(args [0]string, argsEscaped bool, r *http.Request) (params SearchTagsParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) + // Decode query: scope. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "scope", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotScopeVal TagScope + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + paramsDotScopeVal = TagScope(c) + return nil + }(); err != nil { + return err + } + params.Scope.SetTo(paramsDotScopeVal) + return nil + }); err != nil { + return err + } + if err := func() error { + if value, ok := params.Scope.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "scope", + In: "query", + Err: err, + } + } + // Decode query: start. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "start", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotStartVal time.Time + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUnixSeconds(val) + if err != nil { + return err + } + + paramsDotStartVal = c + return nil + }(); err != nil { + return err + } + params.Start.SetTo(paramsDotStartVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "start", + In: "query", + Err: err, + } + } + // Decode query: end. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "end", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotEndVal time.Time + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUnixSeconds(val) + if err != nil { + return err + } + + paramsDotEndVal = c + return nil + }(); err != nil { + return err + } + params.End.SetTo(paramsDotEndVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "end", + In: "query", + Err: err, + } + } + return params, nil +} + +// SearchTagsV2Params is parameters of searchTagsV2 operation. +type SearchTagsV2Params struct { + // Specifies the scope of the tags, this is an optional parameter, if not specified it means all + // scopes. + Scope OptTagScope + // Along with `end` define a time range from which tags should be returned. + Start OptUnixSeconds + // Along with `start` define a time range from which tags should be returned. + // Providing both `start` and `end` includes blocks for the specified time range only. + End OptUnixSeconds +} + +func unpackSearchTagsV2Params(packed middleware.Parameters) (params SearchTagsV2Params) { + { + key := middleware.ParameterKey{ + Name: "scope", + In: "query", + } + if v, ok := packed[key]; ok { + params.Scope = v.(OptTagScope) + } + } + { + key := middleware.ParameterKey{ + Name: "start", + In: "query", + } + if v, ok := packed[key]; ok { + params.Start = v.(OptUnixSeconds) + } + } + { + key := middleware.ParameterKey{ + Name: "end", + In: "query", + } + if v, ok := packed[key]; ok { + params.End = v.(OptUnixSeconds) + } + } + return params +} + +func decodeSearchTagsV2Params(args [0]string, argsEscaped bool, r *http.Request) (params SearchTagsV2Params, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) + // Decode query: scope. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "scope", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotScopeVal TagScope + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + paramsDotScopeVal = TagScope(c) + return nil + }(); err != nil { + return err + } + params.Scope.SetTo(paramsDotScopeVal) + return nil + }); err != nil { + return err + } + if err := func() error { + if value, ok := params.Scope.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "scope", + In: "query", + Err: err, + } + } + // Decode query: start. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "start", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotStartVal time.Time + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUnixSeconds(val) + if err != nil { + return err + } + + paramsDotStartVal = c + return nil + }(); err != nil { + return err + } + params.Start.SetTo(paramsDotStartVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "start", + In: "query", + Err: err, + } + } + // Decode query: end. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "end", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotEndVal time.Time + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUnixSeconds(val) + if err != nil { + return err + } + + paramsDotEndVal = c + return nil + }(); err != nil { + return err + } + params.End.SetTo(paramsDotEndVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "end", + In: "query", + Err: err, + } + } return params, nil } diff --git a/internal/tempoapi/oas_response_decoders_gen.go b/internal/tempoapi/oas_response_decoders_gen.go index d2b57d63..97f31eb7 100644 --- a/internal/tempoapi/oas_response_decoders_gen.go +++ b/internal/tempoapi/oas_response_decoders_gen.go @@ -423,6 +423,98 @@ func decodeSearchTagsResponse(resp *http.Response) (res *TagNames, _ error) { return res, errors.Wrap(defRes, "error") } +func decodeSearchTagsV2Response(resp *http.Response) (res *TagNamesV2, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response TagNamesV2 + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeTraceByIDResponse(resp *http.Response) (res TraceByIDRes, _ error) { switch resp.StatusCode { case 200: diff --git a/internal/tempoapi/oas_response_encoders_gen.go b/internal/tempoapi/oas_response_encoders_gen.go index e83377ce..3f961e55 100644 --- a/internal/tempoapi/oas_response_encoders_gen.go +++ b/internal/tempoapi/oas_response_encoders_gen.go @@ -83,6 +83,20 @@ func encodeSearchTagsResponse(response *TagNames, w http.ResponseWriter, span tr return nil } +func encodeSearchTagsV2Response(response *TagNamesV2, w http.ResponseWriter, span trace.Span) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil +} + func encodeTraceByIDResponse(response TraceByIDRes, w http.ResponseWriter, span trace.Span) error { switch response := response.(type) { case *TraceByID: diff --git a/internal/tempoapi/oas_router_gen.go b/internal/tempoapi/oas_router_gen.go index b84e02fa..67369dde 100644 --- a/internal/tempoapi/oas_router_gen.go +++ b/internal/tempoapi/oas_router_gen.go @@ -215,30 +215,68 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } elem = origElem - case 'v': // Prefix: "v2/search/tag/" + case 'v': // Prefix: "v2/search/tag" origElem := elem - if l := len("v2/search/tag/"); len(elem) >= l && elem[0:l] == "v2/search/tag/" { + if l := len("v2/search/tag"); len(elem) >= l && elem[0:l] == "v2/search/tag" { elem = elem[l:] } else { break } - // Param: "tag_name" - // Match until "/" - idx := strings.IndexByte(elem, '/') - if idx < 0 { - idx = len(elem) - } - args[0] = elem[:idx] - elem = elem[idx:] - if len(elem) == 0 { break } switch elem[0] { - case '/': // Prefix: "/values" + case '/': // Prefix: "/" + origElem := elem + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "attribute_selector" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/values" + origElem := elem + if l := len("/values"); len(elem) >= l && elem[0:l] == "/values" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleSearchTagValuesV2Request([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET") + } + + return + } + + elem = origElem + } + + elem = origElem + case 's': // Prefix: "s" origElem := elem - if l := len("/values"); len(elem) >= l && elem[0:l] == "/values" { + if l := len("s"); len(elem) >= l && elem[0:l] == "s" { elem = elem[l:] } else { break @@ -248,9 +286,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Leaf node. switch r.Method { case "GET": - s.handleSearchTagValuesV2Request([1]string{ - args[0], - }, elemIsEscaped, w, r) + s.handleSearchTagsV2Request([0]string{}, elemIsEscaped, w, r) default: s.notAllowed(w, r, "GET") } @@ -527,30 +563,70 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } elem = origElem - case 'v': // Prefix: "v2/search/tag/" + case 'v': // Prefix: "v2/search/tag" origElem := elem - if l := len("v2/search/tag/"); len(elem) >= l && elem[0:l] == "v2/search/tag/" { + if l := len("v2/search/tag"); len(elem) >= l && elem[0:l] == "v2/search/tag" { elem = elem[l:] } else { break } - // Param: "tag_name" - // Match until "/" - idx := strings.IndexByte(elem, '/') - if idx < 0 { - idx = len(elem) - } - args[0] = elem[:idx] - elem = elem[idx:] - if len(elem) == 0 { break } switch elem[0] { - case '/': // Prefix: "/values" + case '/': // Prefix: "/" + origElem := elem + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "attribute_selector" + // Match until "/" + idx := strings.IndexByte(elem, '/') + if idx < 0 { + idx = len(elem) + } + args[0] = elem[:idx] + elem = elem[idx:] + + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/values" + origElem := elem + if l := len("/values"); len(elem) >= l && elem[0:l] == "/values" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + // Leaf: SearchTagValuesV2 + r.name = "SearchTagValuesV2" + r.summary = "" + r.operationID = "searchTagValuesV2" + r.pathPattern = "/api/v2/search/tag/{attribute_selector}/values" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + elem = origElem + } + + elem = origElem + case 's': // Prefix: "s" origElem := elem - if l := len("/values"); len(elem) >= l && elem[0:l] == "/values" { + if l := len("s"); len(elem) >= l && elem[0:l] == "s" { elem = elem[l:] } else { break @@ -559,13 +635,13 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { if len(elem) == 0 { switch method { case "GET": - // Leaf: SearchTagValuesV2 - r.name = "SearchTagValuesV2" + // Leaf: SearchTagsV2 + r.name = "SearchTagsV2" r.summary = "" - r.operationID = "searchTagValuesV2" - r.pathPattern = "/api/v2/search/tag/{tag_name}/values" + r.operationID = "searchTagsV2" + r.pathPattern = "/api/v2/search/tags" r.args = args - r.count = 1 + r.count = 0 return r, true default: return diff --git a/internal/tempoapi/oas_schemas_gen.go b/internal/tempoapi/oas_schemas_gen.go index af02457a..34c39129 100644 --- a/internal/tempoapi/oas_schemas_gen.go +++ b/internal/tempoapi/oas_schemas_gen.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "time" + + "github.com/go-faster/errors" ) func (s *ErrorStatusCode) Error() string { @@ -505,6 +507,52 @@ func (o OptString) Or(d string) string { return d } +// NewOptTagScope returns new OptTagScope with value set to v. +func NewOptTagScope(v TagScope) OptTagScope { + return OptTagScope{ + Value: v, + Set: true, + } +} + +// OptTagScope is optional TagScope. +type OptTagScope struct { + Value TagScope + Set bool +} + +// IsSet returns true if OptTagScope was set. +func (o OptTagScope) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptTagScope) Reset() { + var v TagScope + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptTagScope) SetTo(v TagScope) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptTagScope) Get() (v TagScope, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptTagScope) Or(d TagScope) TagScope { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptTempoSpanSet returns new OptTempoSpanSet with value set to v. func NewOptTempoSpanSet(v TempoSpanSet) OptTempoSpanSet { return OptTempoSpanSet{ @@ -597,6 +645,32 @@ func (o OptUnixSeconds) Or(d time.Time) time.Time { return d } +// Ref: #/components/schemas/ScopeTags +type ScopeTags struct { + Name TagScope `json:"name"` + Tags []string `json:"tags"` +} + +// GetName returns the value of Name. +func (s *ScopeTags) GetName() TagScope { + return s.Name +} + +// GetTags returns the value of Tags. +func (s *ScopeTags) GetTags() []string { + return s.Tags +} + +// SetName sets the value of Name. +func (s *ScopeTags) SetName(val TagScope) { + s.Name = val +} + +// SetTags sets the value of Tags. +func (s *ScopeTags) SetTags(val []string) { + s.Tags = val +} + // Ref: #/components/schemas/StringValue type StringValue struct { StringValue string `json:"stringValue"` @@ -627,6 +701,77 @@ func (s *TagNames) SetTagNames(val []string) { s.TagNames = val } +// Ref: #/components/schemas/TagNamesV2 +type TagNamesV2 struct { + Scopes []ScopeTags `json:"scopes"` +} + +// GetScopes returns the value of Scopes. +func (s *TagNamesV2) GetScopes() []ScopeTags { + return s.Scopes +} + +// SetScopes sets the value of Scopes. +func (s *TagNamesV2) SetScopes(val []ScopeTags) { + s.Scopes = val +} + +// Ref: #/components/schemas/TagScope +type TagScope string + +const ( + TagScopeSpan TagScope = "span" + TagScopeResource TagScope = "resource" + TagScopeIntrinsic TagScope = "intrinsic" + TagScopeNone TagScope = "none" +) + +// AllValues returns all TagScope values. +func (TagScope) AllValues() []TagScope { + return []TagScope{ + TagScopeSpan, + TagScopeResource, + TagScopeIntrinsic, + TagScopeNone, + } +} + +// MarshalText implements encoding.TextMarshaler. +func (s TagScope) MarshalText() ([]byte, error) { + switch s { + case TagScopeSpan: + return []byte(s), nil + case TagScopeResource: + return []byte(s), nil + case TagScopeIntrinsic: + return []byte(s), nil + case TagScopeNone: + return []byte(s), nil + default: + return nil, errors.Errorf("invalid value: %q", s) + } +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (s *TagScope) UnmarshalText(data []byte) error { + switch TagScope(data) { + case TagScopeSpan: + *s = TagScopeSpan + return nil + case TagScopeResource: + *s = TagScopeResource + return nil + case TagScopeIntrinsic: + *s = TagScopeIntrinsic + return nil + case TagScopeNone: + *s = TagScopeNone + return nil + default: + return errors.Errorf("invalid value: %q", data) + } +} + // Ref: #/components/schemas/TagValue type TagValue struct { Type string `json:"type"` diff --git a/internal/tempoapi/oas_server_gen.go b/internal/tempoapi/oas_server_gen.go index eaabbb38..808b20d3 100644 --- a/internal/tempoapi/oas_server_gen.go +++ b/internal/tempoapi/oas_server_gen.go @@ -31,14 +31,20 @@ type Handler interface { // This endpoint retrieves all discovered values and their data types for the given TraceQL // identifier. // - // GET /api/v2/search/tag/{tag_name}/values + // GET /api/v2/search/tag/{attribute_selector}/values SearchTagValuesV2(ctx context.Context, params SearchTagValuesV2Params) (*TagValuesV2, error) // SearchTags implements searchTags operation. // // This endpoint retrieves all discovered tag names that can be used in search. // // GET /api/search/tags - SearchTags(ctx context.Context) (*TagNames, error) + SearchTags(ctx context.Context, params SearchTagsParams) (*TagNames, error) + // SearchTagsV2 implements searchTagsV2 operation. + // + // This endpoint retrieves all discovered tag names that can be used in search. + // + // GET /api/v2/search/tags + SearchTagsV2(ctx context.Context, params SearchTagsV2Params) (*TagNamesV2, error) // TraceByID implements traceByID operation. // // Querying traces by id. diff --git a/internal/tempoapi/oas_test_examples_gen_test.go b/internal/tempoapi/oas_test_examples_gen_test.go index 5c27ae25..137497f6 100644 --- a/internal/tempoapi/oas_test_examples_gen_test.go +++ b/internal/tempoapi/oas_test_examples_gen_test.go @@ -131,6 +131,18 @@ func TestKvlistValue_EncodeDecode(t *testing.T) { var typ2 KvlistValue require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) } +func TestScopeTags_EncodeDecode(t *testing.T) { + var typ ScopeTags + typ.SetFake() + + e := jx.Encoder{} + typ.Encode(&e) + data := e.Bytes() + require.True(t, std.Valid(data), "Encoded: %s", data) + + var typ2 ScopeTags + require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) +} func TestStringValue_EncodeDecode(t *testing.T) { var typ StringValue typ.SetFake() @@ -155,6 +167,30 @@ func TestTagNames_EncodeDecode(t *testing.T) { var typ2 TagNames require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) } +func TestTagNamesV2_EncodeDecode(t *testing.T) { + var typ TagNamesV2 + typ.SetFake() + + e := jx.Encoder{} + typ.Encode(&e) + data := e.Bytes() + require.True(t, std.Valid(data), "Encoded: %s", data) + + var typ2 TagNamesV2 + require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) +} +func TestTagScope_EncodeDecode(t *testing.T) { + var typ TagScope + typ.SetFake() + + e := jx.Encoder{} + typ.Encode(&e) + data := e.Bytes() + require.True(t, std.Valid(data), "Encoded: %s", data) + + var typ2 TagScope + require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) +} func TestTagValue_EncodeDecode(t *testing.T) { var typ TagValue typ.SetFake() diff --git a/internal/tempoapi/oas_unimplemented_gen.go b/internal/tempoapi/oas_unimplemented_gen.go index fe9f2d50..26be62ea 100644 --- a/internal/tempoapi/oas_unimplemented_gen.go +++ b/internal/tempoapi/oas_unimplemented_gen.go @@ -45,7 +45,7 @@ func (UnimplementedHandler) SearchTagValues(ctx context.Context, params SearchTa // This endpoint retrieves all discovered values and their data types for the given TraceQL // identifier. // -// GET /api/v2/search/tag/{tag_name}/values +// GET /api/v2/search/tag/{attribute_selector}/values func (UnimplementedHandler) SearchTagValuesV2(ctx context.Context, params SearchTagValuesV2Params) (r *TagValuesV2, _ error) { return r, ht.ErrNotImplemented } @@ -55,7 +55,16 @@ func (UnimplementedHandler) SearchTagValuesV2(ctx context.Context, params Search // This endpoint retrieves all discovered tag names that can be used in search. // // GET /api/search/tags -func (UnimplementedHandler) SearchTags(ctx context.Context) (r *TagNames, _ error) { +func (UnimplementedHandler) SearchTags(ctx context.Context, params SearchTagsParams) (r *TagNames, _ error) { + return r, ht.ErrNotImplemented +} + +// SearchTagsV2 implements searchTagsV2 operation. +// +// This endpoint retrieves all discovered tag names that can be used in search. +// +// GET /api/v2/search/tags +func (UnimplementedHandler) SearchTagsV2(ctx context.Context, params SearchTagsV2Params) (r *TagNamesV2, _ error) { return r, ht.ErrNotImplemented } diff --git a/internal/tempoapi/oas_validators_gen.go b/internal/tempoapi/oas_validators_gen.go index c2a72449..686226bd 100644 --- a/internal/tempoapi/oas_validators_gen.go +++ b/internal/tempoapi/oas_validators_gen.go @@ -188,6 +188,81 @@ func (s *KvlistValue) Validate() error { return nil } +func (s *ScopeTags) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Name.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "name", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *TagNamesV2) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + var failures []validate.FieldError + for i, elem := range s.Scopes { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "scopes", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s TagScope) Validate() error { + switch s { + case "span": + return nil + case "resource": + return nil + case "intrinsic": + return nil + case "none": + return nil + default: + return errors.Errorf("invalid value: %v", s) + } +} + func (s *TempoSpan) Validate() error { if s == nil { return validate.ErrNilPointer diff --git a/internal/tempohandler/options.go b/internal/tempohandler/options.go new file mode 100644 index 00000000..a7f5b610 --- /dev/null +++ b/internal/tempohandler/options.go @@ -0,0 +1,12 @@ +package tempohandler + +// TempoAPIOptions describes [TempoAPI] options. +type TempoAPIOptions struct { + // EnableAutocompleteQuery whether if handler should parse + // the `q` parameter in tag requests + // + // See https://grafana.com/docs/tempo/latest/api_docs/#filtered-tag-values. + EnableAutocompleteQuery bool +} + +func (opts *TempoAPIOptions) setDefaults() {} diff --git a/internal/tempohandler/tempohandler.go b/internal/tempohandler/tempohandler.go index 1bbc3063..870c6cfc 100644 --- a/internal/tempohandler/tempohandler.go +++ b/internal/tempohandler/tempohandler.go @@ -9,34 +9,44 @@ import ( "strings" "time" + "github.com/go-faster/errors" + "github.com/go-faster/sdk/zctx" "github.com/go-logfmt/logfmt" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.uber.org/zap" - - "github.com/go-faster/errors" - "github.com/go-faster/sdk/zctx" + "golang.org/x/exp/maps" "github.com/go-faster/oteldb/internal/iterators" "github.com/go-faster/oteldb/internal/otelstorage" "github.com/go-faster/oteldb/internal/tempoapi" + "github.com/go-faster/oteldb/internal/traceql" "github.com/go-faster/oteldb/internal/traceql/traceqlengine" "github.com/go-faster/oteldb/internal/tracestorage" ) -var _ tempoapi.Handler = (*TempoAPI)(nil) - // TempoAPI implements tempoapi.Handler. type TempoAPI struct { q tracestorage.Querier engine *traceqlengine.Engine + + enableAutocomplete bool } +var _ tempoapi.Handler = (*TempoAPI)(nil) + // NewTempoAPI creates new TempoAPI. -func NewTempoAPI(q tracestorage.Querier, engine *traceqlengine.Engine) *TempoAPI { +func NewTempoAPI( + q tracestorage.Querier, + engine *traceqlengine.Engine, + opts TempoAPIOptions, +) *TempoAPI { + opts.setDefaults() + return &TempoAPI{ - q: q, - engine: engine, + q: q, + engine: engine, + enableAutocomplete: opts.EnableAutocompleteQuery, } } @@ -147,9 +157,21 @@ func parseLogfmt(q string) (tags map[string]string, _ error) { func (h *TempoAPI) SearchTagValues(ctx context.Context, params tempoapi.SearchTagValuesParams) (resp *tempoapi.TagValues, _ error) { lg := zctx.From(ctx) - iter, err := h.q.TagValues(ctx, params.TagName) + var ( + attr = traceql.Attribute{Name: params.TagName} + query traceql.Autocomplete + ) + if q, ok := params.Q.Get(); ok && h.enableAutocomplete { + query = traceql.ParseAutocomplete(q) + } + + iter, err := h.q.TagValues(ctx, attr, tracestorage.TagValuesOptions{ + Query: query, + Start: timeToTimestamp(params.Start), + End: timeToTimestamp(params.End), + }) if err != nil { - return nil, errors.Wrap(err, "query") + return nil, errors.Wrap(err, "get tag values") } defer func() { _ = iter.Close() @@ -164,6 +186,7 @@ func (h *TempoAPI) SearchTagValues(ctx context.Context, params tempoapi.SearchTa } lg.Debug("Got tag values", zap.String("tag_name", params.TagName), + zap.String("q", params.Q.Or("")), zap.Int("count", len(values)), ) @@ -177,13 +200,26 @@ func (h *TempoAPI) SearchTagValues(ctx context.Context, params tempoapi.SearchTa // This endpoint retrieves all discovered values and their data types for the given TraceQL // identifier. // -// GET /api/v2/search/tag/{tag_name}/values +// GET /api/v2/search/tag/{attribute_selector}/values func (h *TempoAPI) SearchTagValuesV2(ctx context.Context, params tempoapi.SearchTagValuesV2Params) (resp *tempoapi.TagValuesV2, _ error) { lg := zctx.From(ctx) - iter, err := h.q.TagValues(ctx, params.TagName) + attr, err := traceql.ParseAttribute(params.AttributeSelector) if err != nil { - return nil, errors.Wrap(err, "query") + return nil, err + } + var query traceql.Autocomplete + if q, ok := params.Q.Get(); ok && h.enableAutocomplete { + query = traceql.ParseAutocomplete(q) + } + + iter, err := h.q.TagValues(ctx, attr, tracestorage.TagValuesOptions{ + Query: query, + Start: timeToTimestamp(params.Start), + End: timeToTimestamp(params.End), + }) + if err != nil { + return nil, errors.Wrap(err, "get tag values") } defer func() { _ = iter.Close() @@ -218,8 +254,9 @@ func (h *TempoAPI) SearchTagValuesV2(ctx context.Context, params tempoapi.Search }); err != nil { return nil, errors.Wrap(err, "map tags") } - lg.Debug("Got tag types and values", - zap.String("tag_name", params.TagName), + lg.Debug("Got tag values", + zap.String("attribute_selector", params.AttributeSelector), + zap.String("q", params.Q.Or("")), zap.Int("count", len(values)), ) @@ -233,17 +270,114 @@ func (h *TempoAPI) SearchTagValuesV2(ctx context.Context, params tempoapi.Search // This endpoint retrieves all discovered tag names that can be used in search. // // GET /api/search/tags -func (h *TempoAPI) SearchTags(ctx context.Context) (resp *tempoapi.TagNames, _ error) { +func (h *TempoAPI) SearchTags(ctx context.Context, params tempoapi.SearchTagsParams) (resp *tempoapi.TagNames, _ error) { lg := zctx.From(ctx) - names, err := h.q.TagNames(ctx) + var scope traceql.AttributeScope + switch params.Scope.Or(tempoapi.TagScopeNone) { + case tempoapi.TagScopeSpan: + scope = traceql.ScopeSpan + case tempoapi.TagScopeResource: + scope = traceql.ScopeResource + case tempoapi.TagScopeIntrinsic: + lg.Debug("Return intrinsic names") + return &tempoapi.TagNames{ + TagNames: traceql.IntrinsicNames(), + }, nil + case tempoapi.TagScopeNone: + scope = traceql.ScopeNone + } + + tags, err := h.q.TagNames(ctx, tracestorage.TagNamesOptions{ + Scope: scope, + Start: timeToTimestamp(params.Start), + End: timeToTimestamp(params.End), + }) if err != nil { - return nil, errors.Wrap(err, "query") + return nil, errors.Wrap(err, "get tag names") + } + + names := make(map[string]struct{}, len(tags)) + for _, tag := range tags { + names[tag.Name] = struct{}{} } lg.Debug("Got tag names", zap.Int("count", len(names))) return &tempoapi.TagNames{ - TagNames: names, + TagNames: maps.Keys(names), + }, nil +} + +// SearchTagsV2 implements searchTagsV2 operation. +// +// This endpoint retrieves all discovered tag names that can be used in search. +// +// GET /api/v2/search/tags +func (h *TempoAPI) SearchTagsV2(ctx context.Context, params tempoapi.SearchTagsV2Params) (*tempoapi.TagNamesV2, error) { + lg := zctx.From(ctx) + + var ( + searchScope traceql.AttributeScope + intrinsic = tempoapi.ScopeTags{ + Name: tempoapi.TagScopeIntrinsic, + Tags: traceql.IntrinsicNames(), + } + ) + switch params.Scope.Or(tempoapi.TagScopeNone) { + case tempoapi.TagScopeSpan: + searchScope = traceql.ScopeSpan + case tempoapi.TagScopeResource: + searchScope = traceql.ScopeResource + case tempoapi.TagScopeIntrinsic: + lg.Debug("Return intrinsic names") + return &tempoapi.TagNamesV2{ + Scopes: []tempoapi.ScopeTags{intrinsic}, + }, nil + case tempoapi.TagScopeNone: + searchScope = traceql.ScopeNone + } + + tags, err := h.q.TagNames(ctx, tracestorage.TagNamesOptions{ + Scope: searchScope, + Start: timeToTimestamp(params.Start), + End: timeToTimestamp(params.End), + }) + if err != nil { + return nil, errors.Wrap(err, "get tag names") + } + + scopes := make(map[tempoapi.TagScope]tempoapi.ScopeTags, 4) + if searchScope == traceql.ScopeNone { + // Add intrinsics to the result, if all scopes are requested. + scopes[intrinsic.Name] = intrinsic + } + for _, tag := range tags { + var tagScope tempoapi.TagScope + switch tag.Scope { + case traceql.ScopeNone: + tagScope = tempoapi.TagScopeNone + case traceql.ScopeResource, traceql.ScopeInstrumentation: + tagScope = tempoapi.TagScopeResource + case traceql.ScopeSpan: + tagScope = tempoapi.TagScopeSpan + default: + lg.Warn("Unexpected tag scope", + zap.Stringer("scope", tag.Scope), + zap.String("tag", tag.Name), + ) + continue + } + + scopeTags, ok := scopes[tagScope] + if !ok { + scopeTags.Name = tagScope + } + scopeTags.Tags = append(scopeTags.Tags, tag.Name) + scopes[tagScope] = scopeTags + } + + return &tempoapi.TagNamesV2{ + Scopes: maps.Values(scopes), }, nil } diff --git a/internal/tempoproxy/tempoproxy.go b/internal/tempoproxy/tempoproxy.go index a7f001ca..7fb05586 100644 --- a/internal/tempoproxy/tempoproxy.go +++ b/internal/tempoproxy/tempoproxy.go @@ -65,8 +65,17 @@ func (s *Server) SearchTagValuesV2(ctx context.Context, params tempoapi.SearchTa // This endpoint retrieves all discovered tag names that can be used in search. // // GET /api/search/tags -func (s *Server) SearchTags(ctx context.Context) (*tempoapi.TagNames, error) { - return s.api.SearchTags(ctx) +func (s *Server) SearchTags(ctx context.Context, params tempoapi.SearchTagsParams) (*tempoapi.TagNames, error) { + return s.api.SearchTags(ctx, params) +} + +// SearchTagsV2 implements searchTagsV2 operation. +// +// This endpoint retrieves all discovered tag names that can be used in search. +// +// GET /api/v2/search/tags +func (s *Server) SearchTagsV2(ctx context.Context, params tempoapi.SearchTagsV2Params) (*tempoapi.TagNamesV2, error) { + return s.api.SearchTagsV2(ctx, params) } // TraceByID implements traceByID operation. diff --git a/internal/traceql/attribute.go b/internal/traceql/attribute.go index a5f8a5bc..382a6a5b 100644 --- a/internal/traceql/attribute.go +++ b/internal/traceql/attribute.go @@ -1,6 +1,26 @@ package traceql -import "strings" +import ( + "fmt" + "strings" + + "github.com/go-faster/errors" +) + +// ParseAttribute parses attribute from given string. +func ParseAttribute(attr string) (a Attribute, _ error) { + p, err := newParser(attr) + if err != nil { + return a, err + } + + a, ok := p.tryAttribute() + if !ok { + return a, errors.Errorf("invalid attribute %q", attr) + } + + return a, nil +} // Attribute is a span attribute. type Attribute struct { @@ -100,6 +120,19 @@ const ( TraceDuration ) +var intrinsicNames = func() (r []string) { + r = make([]string, 0, TraceDuration) + for i := SpanDuration; i <= TraceDuration; i++ { + r = append(r, Attribute{Prop: i}.String()) + } + return r +}() + +// IntrinsicNames returns a slice of intrinsics. +func IntrinsicNames() (r []string) { + return intrinsicNames +} + // AttributeScope is an attribute scope. type AttributeScope uint8 @@ -107,4 +140,21 @@ const ( ScopeNone AttributeScope = iota ScopeResource ScopeSpan + ScopeInstrumentation ) + +// String implements [fmt.Stringer]. +func (s AttributeScope) String() string { + switch s { + case ScopeNone: + return "none" + case ScopeResource: + return "resource" + case ScopeSpan: + return "span" + case ScopeInstrumentation: + return "" + default: + return fmt.Sprintf("unknown scope %d", uint8(s)) + } +} diff --git a/internal/traceql/attribute_test.go b/internal/traceql/attribute_test.go new file mode 100644 index 00000000..23c7ebb4 --- /dev/null +++ b/internal/traceql/attribute_test.go @@ -0,0 +1,37 @@ +package traceql + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseAttribute(t *testing.T) { + tests := []struct { + attr string + wantA Attribute + wantErr bool + }{ + {`.service.name`, Attribute{Name: "service.name"}, false}, + {`span.service.name`, Attribute{Name: "service.name", Scope: ScopeSpan}, false}, + {`resource.service.name`, Attribute{Name: "service.name", Scope: ScopeResource}, false}, + {`parent.span.service.name`, Attribute{Name: "service.name", Scope: ScopeSpan, Parent: true}, false}, + {`status`, Attribute{Prop: SpanStatus}, false}, + + {`{`, Attribute{}, true}, + {``, Attribute{}, true}, + } + for i, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { + gotA, err := ParseAttribute(tt.attr) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.wantA, gotA) + }) + } +} diff --git a/internal/traceql/autocomplete.go b/internal/traceql/autocomplete.go new file mode 100644 index 00000000..44c058e5 --- /dev/null +++ b/internal/traceql/autocomplete.go @@ -0,0 +1,87 @@ +package traceql + +import "github.com/go-faster/oteldb/internal/traceql/lexer" + +// Autocomplete is a AND set of spanset matchers. +type Autocomplete struct { + Matchers []BinaryFieldExpr +} + +// ParseAutocomplete parses matchers from potentially uncomplete TraceQL spanset filter from string. +func ParseAutocomplete(input string) (c Autocomplete) { + p, err := newParser(input) + if err != nil { + return c + } + + if err := p.consume(lexer.OpenBrace); err != nil { + return c + } + + for { + left, ok, err := parseSimpleFieldExpr(&p) + if err != nil || !ok { + return c + } + + op, ok := p.peekBinaryOp() + if !ok || !(op.IsOrdering() || op.IsRegex()) { + return c + } + // Consume op. + p.next() + + right, ok, err := parseSimpleFieldExpr(&p) + switch { + case err != nil: + return c + case !ok: + // Handle cases like `{ .foo = && .bar = 10 }`. + op, ok = p.peekBinaryOp() + if !ok { + return c + } + if op != OpAnd { + return Autocomplete{} + } + default: + c.Matchers = append(c.Matchers, BinaryFieldExpr{ + Left: left, + Op: op, + Right: right, + }) + } + + switch t := p.peek(); t.Type { + case lexer.EOF: + return c + case lexer.CloseBrace: + p.next() + return c + default: + op, ok := p.peekBinaryOp() + if !ok { + return c + } + if op != OpAnd { + return Autocomplete{} + } + // Consume op. + p.next() + } + } +} + +func parseSimpleFieldExpr(p *parser) (FieldExpr, bool, error) { + switch s, ok, err := p.tryStatic(); { + case err != nil: + return nil, false, err + case ok: + return s, true, nil + } + + if a, ok := p.tryAttribute(); ok { + return &a, true, nil + } + return nil, false, nil +} diff --git a/internal/traceql/autocomplete_test.go b/internal/traceql/autocomplete_test.go new file mode 100644 index 00000000..c9bcf87d --- /dev/null +++ b/internal/traceql/autocomplete_test.go @@ -0,0 +1,156 @@ +package traceql + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseAutocomplete(t *testing.T) { + tests := []struct { + input string + want []BinaryFieldExpr + }{ + { + ``, + nil, + }, + { + `{}`, + nil, + }, + { + `{ .a = }`, + nil, + }, + { + `{ .a = `, + nil, + }, + // Simple cases. + { + `{ .a = 10 }`, + []BinaryFieldExpr{ + { + Left: &Attribute{Name: "a"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 10}, + }, + }, + }, + { + `{ .a = 10 && .b =~ "foo.+" }`, + []BinaryFieldExpr{ + { + Left: &Attribute{Name: "a"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 10}, + }, + { + Left: &Attribute{Name: "b"}, + Op: OpRe, + Right: &Static{Type: TypeString, Str: "foo.+"}, + }, + }, + }, + // Missing brace. + { + `{ .a = 10 `, + []BinaryFieldExpr{ + { + Left: &Attribute{Name: "a"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 10}, + }, + }, + }, + // Missing sub-expression. + { + `{ .a = && .b = 20 && .c = 30 }`, + []BinaryFieldExpr{ + { + Left: &Attribute{Name: "b"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 20}, + }, + { + Left: &Attribute{Name: "c"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 30}, + }, + }, + }, + { + `{ .a = 10 && .b = && .c = 30 }`, + []BinaryFieldExpr{ + { + Left: &Attribute{Name: "a"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 10}, + }, + { + Left: &Attribute{Name: "c"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 30}, + }, + }, + }, + { + `{ .a = 10 && .b = 20 && .c = }`, + []BinaryFieldExpr{ + { + Left: &Attribute{Name: "a"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 10}, + }, + { + Left: &Attribute{Name: "b"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 20}, + }, + }, + }, + { + `{ .a = && .b = && .c = 30 }`, + []BinaryFieldExpr{ + { + Left: &Attribute{Name: "c"}, + Op: OpEq, + Right: &Static{Type: TypeInt, Data: 30}, + }, + }, + }, + // Contains OR operation. + { + `{ .a = 10 && .b = 20 || .c = 30 }`, + nil, + }, + { + `{ .a = 10 && .b = || .c = 30 }`, + nil, + }, + // Complicated sub-expression. + { + `{ .status = 2*100 && .baz = 10 }`, + nil, + }, + { + `{ .status / 100 = 2 && .baz = 10 }`, + nil, + }, + } + for i, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { + defer func() { + if t.Failed() { + t.Logf("Input: %#q", tt.input) + } + }() + + got := ParseAutocomplete(tt.input) + require.Equal(t, tt.want, got.Matchers) + }) + } +} diff --git a/internal/traceql/parser_field_expr.go b/internal/traceql/parser_field_expr.go index ad425947..2fc04b74 100644 --- a/internal/traceql/parser_field_expr.go +++ b/internal/traceql/parser_field_expr.go @@ -58,35 +58,17 @@ func (p *parser) parseFieldExpr1() (FieldExpr, error) { Expr: expr, Op: op, }, nil - case lexer.String, - lexer.Integer, - lexer.Number, - lexer.True, - lexer.False, - lexer.Nil, - lexer.Duration, - lexer.StatusOk, - lexer.StatusError, - lexer.StatusUnset, - lexer.KindUnspecified, - lexer.KindInternal, - lexer.KindServer, - lexer.KindClient, - lexer.KindProducer, - lexer.KindConsumer: - return p.parseStatic() - case lexer.SpanDuration, - lexer.ChildCount, - lexer.Name, - lexer.Status, - lexer.Kind, - lexer.Parent, - lexer.RootName, - lexer.RootServiceName, - lexer.TraceDuration, - lexer.Ident: - return p.parseAttribute() default: + switch s, ok, err := p.tryStatic(); { + case err != nil: + return nil, err + case ok: + return s, nil + } + + if a, ok := p.tryAttribute(); ok { + return &a, nil + } return nil, p.unexpectedToken(t) } } @@ -179,65 +161,85 @@ func (p *parser) peekBinaryOp() (op BinaryOp, _ bool) { } } -func (p *parser) parseStatic() (s *Static, _ error) { +func (p *parser) parseStatic() (*Static, error) { + switch s, ok, err := p.tryStatic(); { + case err != nil: + return nil, err + case ok: + return s, nil + default: + return nil, p.unexpectedToken(p.peek()) + } +} + +func (p *parser) tryStatic() (s *Static, ok bool, _ error) { s = new(Static) - switch t := p.next(); t.Type { + switch t := p.peek(); t.Type { case lexer.String: + p.next() s.SetString(t.Text) case lexer.Integer: - p.unread() v, err := p.parseInteger() if err != nil { - return s, err + return s, false, err } s.SetInt(v) case lexer.Number: - p.unread() v, err := p.parseNumber() if err != nil { - return s, err + return s, false, err } s.SetNumber(v) case lexer.True: + p.next() s.SetBool(true) case lexer.False: + p.next() s.SetBool(false) case lexer.Nil: + p.next() s.SetNil() case lexer.Duration: - p.unread() v, err := p.parseDuration() if err != nil { - return s, err + return s, false, err } s.SetDuration(v) case lexer.StatusOk: + p.next() s.SetSpanStatus(ptrace.StatusCodeOk) case lexer.StatusError: + p.next() s.SetSpanStatus(ptrace.StatusCodeError) case lexer.StatusUnset: + p.next() s.SetSpanStatus(ptrace.StatusCodeUnset) case lexer.KindUnspecified: + p.next() s.SetSpanKind(ptrace.SpanKindUnspecified) case lexer.KindInternal: + p.next() s.SetSpanKind(ptrace.SpanKindInternal) case lexer.KindServer: + p.next() s.SetSpanKind(ptrace.SpanKindServer) case lexer.KindClient: + p.next() s.SetSpanKind(ptrace.SpanKindClient) case lexer.KindProducer: + p.next() s.SetSpanKind(ptrace.SpanKindProducer) case lexer.KindConsumer: + p.next() s.SetSpanKind(ptrace.SpanKindConsumer) default: - return s, p.unexpectedToken(t) + return s, false, nil } - return s, nil + return s, true, nil } -func (p *parser) parseAttribute() (a *Attribute, _ error) { - a = new(Attribute) - switch t := p.next(); t.Type { +func (p *parser) tryAttribute() (a Attribute, _ bool) { + switch t := p.peek(); t.Type { case lexer.SpanDuration: a.Prop = SpanDuration case lexer.ChildCount: @@ -256,34 +258,38 @@ func (p *parser) parseAttribute() (a *Attribute, _ error) { a.Prop = RootServiceName case lexer.TraceDuration: a.Prop = TraceDuration - case lexer.Ident: - attr := t.Text - attr, a.Parent = strings.CutPrefix(attr, "parent.") - - uncut := attr - scope, attr, ok := strings.Cut(attr, ".") - if !ok { - a.Name = uncut - } else { - switch scope { - case "resource": - a.Name = attr - a.Scope = ScopeResource - case "span": - a.Name = attr - a.Scope = ScopeSpan - case "": - a.Name = attr - a.Scope = ScopeNone - default: - a.Name = uncut - a.Scope = ScopeNone - } - } + parseAttributeSelector(t.Text, &a) default: - return a, p.unexpectedToken(t) + return a, false + } + p.next() + + return a, true +} + +func parseAttributeSelector(attr string, a *Attribute) { + attr, a.Parent = strings.CutPrefix(attr, "parent.") + + uncut := attr + scope, attr, ok := strings.Cut(attr, ".") + if !ok { + a.Name = uncut + return } - return a, nil + switch scope { + case "resource": + a.Name = attr + a.Scope = ScopeResource + case "span": + a.Name = attr + a.Scope = ScopeSpan + case "": + a.Name = attr + a.Scope = ScopeNone + default: + a.Name = uncut + a.Scope = ScopeNone + } } diff --git a/internal/tracestorage/consumer.go b/internal/tracestorage/consumer.go index b5d6463b..59982720 100644 --- a/internal/tracestorage/consumer.go +++ b/internal/tracestorage/consumer.go @@ -7,6 +7,8 @@ import ( "github.com/google/uuid" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" + + "github.com/go-faster/oteldb/internal/traceql" ) // Consumer consumes given traces and inserts them using given Inserter. @@ -24,15 +26,12 @@ func NewConsumer(i Inserter) *Consumer { // ConsumeTraces implements otelreceiver.Consumer. func (c *Consumer) ConsumeTraces(ctx context.Context, traces ptrace.Traces) error { tags := map[Tag]struct{}{} - addName := func(s string) { - tags[Tag{"name", s, int32(pcommon.ValueTypeStr)}] = struct{}{} - } - addTags := func(attrs pcommon.Map) { + addTags := func(attrs pcommon.Map, scope traceql.AttributeScope) { attrs.Range(func(k string, v pcommon.Value) bool { switch t := v.Type(); t { case pcommon.ValueTypeMap, pcommon.ValueTypeSlice: default: - tags[Tag{k, v.AsString(), int32(t)}] = struct{}{} + tags[Tag{k, v.AsString(), int32(t), scope}] = struct{}{} } return true }) @@ -46,21 +45,19 @@ func (c *Consumer) ConsumeTraces(ctx context.Context, traces ptrace.Traces) erro batchID := uuid.New() resSpan := resSpans.At(i) res := resSpan.Resource() - addTags(res.Attributes()) + addTags(res.Attributes(), traceql.ScopeResource) scopeSpans := resSpan.ScopeSpans() for i := 0; i < scopeSpans.Len(); i++ { scopeSpan := scopeSpans.At(i) scope := scopeSpan.Scope() - addTags(scope.Attributes()) + addTags(scope.Attributes(), traceql.ScopeInstrumentation) spans := scopeSpan.Spans() for i := 0; i < spans.Len(); i++ { span := spans.At(i) - // Add span name as well. For some reason, Grafana is looking for it too. - addName(span.Name()) insertBatch = append(insertBatch, NewSpanFromOTEL(batchID, res, scope, span)) - addTags(span.Attributes()) + addTags(span.Attributes(), traceql.ScopeSpan) } } } diff --git a/internal/tracestorage/schema.go b/internal/tracestorage/schema.go index 6faaf6b2..43983586 100644 --- a/internal/tracestorage/schema.go +++ b/internal/tracestorage/schema.go @@ -4,6 +4,7 @@ import ( "github.com/google/uuid" "github.com/go-faster/oteldb/internal/otelstorage" + "github.com/go-faster/oteldb/internal/traceql" ) // Span is a data structure for span. @@ -61,7 +62,8 @@ type Link struct { // Tag is a data structure for tag. type Tag struct { - Name string `json:"name"` - Value string `json:"value"` - Type int32 `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + Type int32 `json:"type"` + Scope traceql.AttributeScope `json:"scope"` } diff --git a/internal/tracestorage/tracestorage.go b/internal/tracestorage/tracestorage.go index 95ae2fd5..62b754b1 100644 --- a/internal/tracestorage/tracestorage.go +++ b/internal/tracestorage/tracestorage.go @@ -7,6 +7,7 @@ import ( "github.com/go-faster/oteldb/internal/iterators" "github.com/go-faster/oteldb/internal/otelstorage" + "github.com/go-faster/oteldb/internal/traceql" ) // Querier is a trace storage query interface. @@ -15,15 +16,15 @@ type Querier interface { SearchTags(ctx context.Context, tags map[string]string, opts SearchTagsOptions) (iterators.Iterator[Span], error) // TagNames returns all available tag names. - TagNames(ctx context.Context) ([]string, error) + TagNames(ctx context.Context, opts TagNamesOptions) ([]TagName, error) // TagValues returns all available tag values for given tag. - TagValues(ctx context.Context, tagName string) (iterators.Iterator[Tag], error) + TagValues(ctx context.Context, attr traceql.Attribute, opts TagValuesOptions) (iterators.Iterator[Tag], error) // TraceByID returns spans of given trace. TraceByID(ctx context.Context, id otelstorage.TraceID, opts TraceByIDOptions) (iterators.Iterator[Span], error) } -// SearchTagsOptions defines options for SearchTags method. +// SearchTagsOptions defines options for [Querier.SearchTags]. type SearchTagsOptions struct { MinDuration time.Duration MaxDuration time.Duration @@ -38,7 +39,40 @@ type SearchTagsOptions struct { End otelstorage.Timestamp } -// TraceByIDOptions defines options for TraceByID method. +// TagNamesOptions defines options for [Querier.TagNames]. +type TagNamesOptions struct { + // Scope defines attribute scope to lookup. + // + // Querier should return attributes from all scopes, if it is zero. + Scope traceql.AttributeScope + // Start defines time range for search. + // + // Querier ignores parameter, if it is zero. + Start otelstorage.Timestamp + // End defines time range for search. + // + // Querier ignores parameter, if it is zero. + End otelstorage.Timestamp +} + +// TagValuesOptions defines options for [Querier.TagValues]. +type TagValuesOptions struct { + // Query is a set of spanset matchers to only return tags seen + // on matching spansets. + // + // Querier ignores parameter, if it is zero. + Query traceql.Autocomplete + // Start defines time range for search. + // + // Querier ignores parameter, if it is zero. + Start otelstorage.Timestamp + // End defines time range for search. + // + // Querier ignores parameter, if it is zero. + End otelstorage.Timestamp +} + +// TraceByIDOptions defines options for [Querier.TraceByID] method. type TraceByIDOptions struct { // Start defines time range for search. // @@ -50,6 +84,12 @@ type TraceByIDOptions struct { End otelstorage.Timestamp } +// TagNames is a set of tags by scope. +type TagName struct { + Scope traceql.AttributeScope + Name string +} + // Inserter is a trace storage insert interface. type Inserter interface { // InsertSpans inserts given spans.