From 8e271d2c923358faebe6a11a6648616d6bf3de45 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 21 May 2024 07:42:03 +0300 Subject: [PATCH 01/22] feat(oas): update Tempo API --- _oas/tempo.yml | 154 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 4 deletions(-) diff --git a/_oas/tempo.yml b/_oas/tempo.yml index 24dc7dfd..df779515 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,110 @@ paths: schema: type: string description: Tag name. + + - 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 +315,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 +474,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 +510,13 @@ components: value: type: string + TagScope: + type: string + enum: + - "span" + - "resource" + - "intrinsic" + - "none" + Error: type: string From 149797b563a1c28190ceb3c17517872cf473e141 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 21 May 2024 07:42:16 +0300 Subject: [PATCH 02/22] chore: commit generated files --- internal/tempoapi/oas_client_gen.go | 318 +++++++- internal/tempoapi/oas_faker_gen.go | 42 ++ internal/tempoapi/oas_handlers_gen.go | 204 ++++- internal/tempoapi/oas_json_gen.go | 242 ++++++ internal/tempoapi/oas_parameters_gen.go | 711 +++++++++++++++++- .../tempoapi/oas_response_decoders_gen.go | 92 +++ .../tempoapi/oas_response_encoders_gen.go | 14 + internal/tempoapi/oas_router_gen.go | 144 +++- internal/tempoapi/oas_schemas_gen.go | 145 ++++ internal/tempoapi/oas_server_gen.go | 10 +- .../tempoapi/oas_test_examples_gen_test.go | 36 + internal/tempoapi/oas_unimplemented_gen.go | 13 +- internal/tempoapi/oas_validators_gen.go | 75 ++ 13 files changed, 1979 insertions(+), 67 deletions(-) diff --git a/internal/tempoapi/oas_client_gen.go b/internal/tempoapi/oas_client_gen.go index 54833d0c..0cc4a595 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,44 @@ func (c *Client) sendSearchTagValues(ctx context.Context, params SearchTagValues pathParts[2] = "/values" uri.AddPathParts(u, pathParts[:]...) + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // 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 +538,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 +548,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 +583,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 +603,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 +685,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 +730,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 +807,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..89e13dd3 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,14 @@ func (s *Server) handleSearchTagValuesRequest(args [1]string, argsEscaped bool, Name: "tag_name", In: "path", }: params.TagName, + { + Name: "start", + In: "query", + }: params.Start, + { + Name: "end", + In: "query", + }: params.End, }, Raw: r, } @@ -399,12 +411,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 +476,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 +584,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 +609,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 +638,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 +673,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..91ba47be 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,11 @@ func decodeSearchParams(args [0]string, argsEscaped bool, r *http.Request) (para type SearchTagValuesParams struct { // Tag name. TagName string + // 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 +473,29 @@ func unpackSearchTagValuesParams(packed middleware.Parameters) (params SearchTag } params.TagName = packed[key].(string) } + { + 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 +541,152 @@ func decodeSearchTagValuesParams(args [1]string, argsEscaped bool, r *http.Reque 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 +698,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 +715,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 +726,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 From b11e9e2f2b13face05f389a698ae86dc90575f82 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 21 May 2024 07:48:24 +0300 Subject: [PATCH 03/22] chore(tempoproxy): update handler due to code changes --- internal/tempoproxy/tempoproxy.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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. From 4f5cf41603c630a42494989066d7cc07b76241e9 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 21 May 2024 15:30:38 +0300 Subject: [PATCH 04/22] feat(traceql): parse autocomplete queries --- internal/traceql/autocomplete.go | 91 +++++++++++++++ internal/traceql/autocomplete_test.go | 156 ++++++++++++++++++++++++++ internal/traceql/parser_field_expr.go | 148 +++++++++++++----------- 3 files changed, 329 insertions(+), 66 deletions(-) create mode 100644 internal/traceql/autocomplete.go create mode 100644 internal/traceql/autocomplete_test.go diff --git a/internal/traceql/autocomplete.go b/internal/traceql/autocomplete.go new file mode 100644 index 00000000..1ff5cd71 --- /dev/null +++ b/internal/traceql/autocomplete.go @@ -0,0 +1,91 @@ +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 + } + + switch a, ok, err := p.tryAttribute(); { + case err != nil: + return nil, false, err + case 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..5655bd6f 100644 --- a/internal/traceql/parser_field_expr.go +++ b/internal/traceql/parser_field_expr.go @@ -58,35 +58,20 @@ 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 + } + + switch a, ok, err := p.tryAttribute(); { + case err != nil: + return nil, err + case ok: + return a, nil + } return nil, p.unexpectedToken(t) } } @@ -179,65 +164,86 @@ 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) { +func (p *parser) tryAttribute() (a *Attribute, ok bool, _ error) { a = new(Attribute) - switch t := p.next(); t.Type { + switch t := p.peek(); t.Type { case lexer.SpanDuration: a.Prop = SpanDuration case lexer.ChildCount: @@ -256,34 +262,44 @@ 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, nil } + p.next() - return a, nil + return a, true, nil +} + +// ParseAttributeSelector parses attribute from given string. +func ParseAttributeSelector(attr string) (a Attribute) { + parseAttributeSelector(attr, &a) + return a +} + +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 + } + + 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 + } } From bf647e3d91bf5b3a31e25c051cdedcc14bb71e14 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 22 May 2024 13:01:33 +0300 Subject: [PATCH 05/22] chore: make ogen output less verbose --- internal/generate.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From d283fba92437aa93cb89c52523288347b2fc143d Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 22 May 2024 13:02:17 +0300 Subject: [PATCH 06/22] feat(oas): add autocomplete parameter to `searchTagValues` --- _oas/tempo.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/_oas/tempo.yml b/_oas/tempo.yml index df779515..e4604782 100644 --- a/_oas/tempo.yml +++ b/_oas/tempo.yml @@ -177,6 +177,19 @@ paths: 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: From ec05262bd247cb711a4a86aef6dc6567316379d1 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 22 May 2024 13:02:24 +0300 Subject: [PATCH 07/22] chore: commit generated files --- internal/tempoapi/oas_client_gen.go | 17 +++++++ internal/tempoapi/oas_handlers_gen.go | 4 ++ internal/tempoapi/oas_parameters_gen.go | 59 +++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/internal/tempoapi/oas_client_gen.go b/internal/tempoapi/oas_client_gen.go index 0cc4a595..979ab545 100644 --- a/internal/tempoapi/oas_client_gen.go +++ b/internal/tempoapi/oas_client_gen.go @@ -475,6 +475,23 @@ func (c *Client) sendSearchTagValues(ctx context.Context, params SearchTagValues 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{ diff --git a/internal/tempoapi/oas_handlers_gen.go b/internal/tempoapi/oas_handlers_gen.go index 89e13dd3..f51e3b26 100644 --- a/internal/tempoapi/oas_handlers_gen.go +++ b/internal/tempoapi/oas_handlers_gen.go @@ -347,6 +347,10 @@ 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", diff --git a/internal/tempoapi/oas_parameters_gen.go b/internal/tempoapi/oas_parameters_gen.go index 91ba47be..9150f34e 100644 --- a/internal/tempoapi/oas_parameters_gen.go +++ b/internal/tempoapi/oas_parameters_gen.go @@ -458,6 +458,15 @@ 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. @@ -473,6 +482,15 @@ 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", @@ -541,6 +559,47 @@ 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{ From 5d32a8632e3958ed33c8e3153efe4bf1b2d22278 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 22 May 2024 13:09:52 +0300 Subject: [PATCH 08/22] feat(traceql): add `ParseAttribute` function --- internal/traceql/attribute.go | 34 +++++++++++++++++++++++- internal/traceql/attribute_test.go | 37 +++++++++++++++++++++++++++ internal/traceql/autocomplete.go | 8 ++---- internal/traceql/parser_field_expr.go | 22 +++++----------- 4 files changed, 78 insertions(+), 23 deletions(-) create mode 100644 internal/traceql/attribute_test.go diff --git a/internal/traceql/attribute.go b/internal/traceql/attribute.go index a5f8a5bc..e97c62e5 100644 --- a/internal/traceql/attribute.go +++ b/internal/traceql/attribute.go @@ -1,6 +1,25 @@ package traceql -import "strings" +import ( + "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 +119,19 @@ const ( TraceDuration ) +var intrinsicNames = func() (r []string) { + r = make([]string, 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 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 index 1ff5cd71..8da5f15e 100644 --- a/internal/traceql/autocomplete.go +++ b/internal/traceql/autocomplete.go @@ -80,12 +80,8 @@ func parseSimpleFieldExpr(p *parser) (FieldExpr, bool, error) { return s, true, nil } - switch a, ok, err := p.tryAttribute(); { - case err != nil: - return nil, false, err - case ok: - return a, true, nil + if a, ok := p.tryAttribute(); ok { + return &a, true, nil } - return nil, false, nil } diff --git a/internal/traceql/parser_field_expr.go b/internal/traceql/parser_field_expr.go index 5655bd6f..2fc04b74 100644 --- a/internal/traceql/parser_field_expr.go +++ b/internal/traceql/parser_field_expr.go @@ -66,11 +66,8 @@ func (p *parser) parseFieldExpr1() (FieldExpr, error) { return s, nil } - switch a, ok, err := p.tryAttribute(); { - case err != nil: - return nil, err - case ok: - return a, nil + if a, ok := p.tryAttribute(); ok { + return &a, nil } return nil, p.unexpectedToken(t) } @@ -241,8 +238,7 @@ func (p *parser) tryStatic() (s *Static, ok bool, _ error) { return s, true, nil } -func (p *parser) tryAttribute() (a *Attribute, ok bool, _ error) { - a = new(Attribute) +func (p *parser) tryAttribute() (a Attribute, _ bool) { switch t := p.peek(); t.Type { case lexer.SpanDuration: a.Prop = SpanDuration @@ -263,19 +259,13 @@ func (p *parser) tryAttribute() (a *Attribute, ok bool, _ error) { case lexer.TraceDuration: a.Prop = TraceDuration case lexer.Ident: - parseAttributeSelector(t.Text, a) + parseAttributeSelector(t.Text, &a) default: - return a, false, nil + return a, false } p.next() - return a, true, nil -} - -// ParseAttributeSelector parses attribute from given string. -func ParseAttributeSelector(attr string) (a Attribute) { - parseAttributeSelector(attr, &a) - return a + return a, true } func parseAttributeSelector(attr string, a *Attribute) { From 0a617d7c1644adab25f844c2111728e3988ce2f4 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 22 May 2024 13:10:54 +0300 Subject: [PATCH 09/22] feat(tempohandler): update handler to latest API --- internal/tempohandler/tempohandler.go | 81 ++++++++++++++++++++++----- internal/tracestorage/tracestorage.go | 48 ++++++++++++++-- 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/internal/tempohandler/tempohandler.go b/internal/tempohandler/tempohandler.go index 1bbc3063..4521e793 100644 --- a/internal/tempohandler/tempohandler.go +++ b/internal/tempohandler/tempohandler.go @@ -9,17 +9,18 @@ import ( "strings" "time" + "github.com/go-faster/errors" + "github.com/go-faster/sdk/zctx" "github.com/go-logfmt/logfmt" + ht "github.com/ogen-go/ogen/http" "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" - "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" ) @@ -147,9 +148,16 @@ 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) + attr := traceql.Attribute{Name: params.TagName} + query := traceql.ParseAutocomplete(params.Q.Or(`{}`)) + + 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 +172,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 +186,23 @@ 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, err + } + query := traceql.ParseAutocomplete(params.Q.Or(`{}`)) + + 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() @@ -218,8 +237,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,20 +253,53 @@ 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") } - lg.Debug("Got tag names", zap.Int("count", len(names))) + + names := make([]string, len(tags)) + for i := range tags { + names = append(names, tags[i].Name) + } + lg.Debug("Got tag names", zap.Int("count", len(tags))) return &tempoapi.TagNames{ TagNames: 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) { + return nil, ht.ErrNotImplemented +} + // TraceByID implements traceByID operation. // // Querying traces by id. 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. From 91c13d20503861d84174c967dc8e90bfcce1187f Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 22 May 2024 13:12:17 +0300 Subject: [PATCH 10/22] test(tempoe2e): update tests to latest API --- integration/tempoe2e/common_test.go | 20 +++++++++++++++++--- integration/tempoe2e/tempo_e2e.go | 16 ++++++++++++++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/integration/tempoe2e/common_test.go b/integration/tempoe2e/common_test.go index 15642f87..83a0fe25 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" @@ -80,11 +81,16 @@ 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 { @@ -100,7 +106,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 { @@ -117,7 +127,11 @@ func runTest( tagValues[t.Value] = struct{}{} } - r, err := c.SearchTagValuesV2(ctx, tempoapi.SearchTagValuesV2Params{TagName: tagName}) + 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 { diff --git a/integration/tempoe2e/tempo_e2e.go b/integration/tempoe2e/tempo_e2e.go index 3a1e6a6e..e4abc83d 100644 --- a/integration/tempoe2e/tempo_e2e.go +++ b/integration/tempoe2e/tempo_e2e.go @@ -10,6 +10,7 @@ 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/traceqlengine" "github.com/go-faster/oteldb/internal/tracestorage" ) @@ -19,8 +20,12 @@ type BatchSet struct { Batches []ptrace.Traces Tags map[string][]tracestorage.Tag Traces map[pcommon.TraceID]Trace - Engine *traceqlengine.Engine - mq traceqlengine.MemoryQuerier + + Start otelstorage.Timestamp + End otelstorage.Timestamp + + Engine *traceqlengine.Engine + mq traceqlengine.MemoryQuerier } // ParseBatchSet parses JSON batches from given reader. @@ -80,6 +85,13 @@ func (s *BatchSet) addSpan(span ptrace.Span) { 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() t, ok := s.Traces[traceID] if !ok { From 4b1d48dc75d58f50c094eb5536d32cb34f66606f Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 07:10:28 +0300 Subject: [PATCH 11/22] feat(traceql): implement stringer for `AttributeScope` --- internal/traceql/attribute.go | 18 ++++++++++++++++++ internal/traceql/autocomplete.go | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/traceql/attribute.go b/internal/traceql/attribute.go index e97c62e5..03382e33 100644 --- a/internal/traceql/attribute.go +++ b/internal/traceql/attribute.go @@ -1,6 +1,7 @@ package traceql import ( + "fmt" "strings" "github.com/go-faster/errors" @@ -139,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/autocomplete.go b/internal/traceql/autocomplete.go index 8da5f15e..44c058e5 100644 --- a/internal/traceql/autocomplete.go +++ b/internal/traceql/autocomplete.go @@ -37,7 +37,7 @@ func ParseAutocomplete(input string) (c Autocomplete) { return c case !ok: // Handle cases like `{ .foo = && .bar = 10 }`. - op, ok := p.peekBinaryOp() + op, ok = p.peekBinaryOp() if !ok { return c } From bd41c670fdd35dd6c650e6847cc636db82a714d7 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 07:11:14 +0300 Subject: [PATCH 12/22] feat(tracestorage): collect tag scope --- internal/tracestorage/consumer.go | 17 +++++++---------- internal/tracestorage/schema.go | 8 +++++--- 2 files changed, 12 insertions(+), 13 deletions(-) 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"` } From 95dc15b7d1e1dd5f2de555aab342d127dea270ec Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 07:11:37 +0300 Subject: [PATCH 13/22] refactor(chstorage): update code due to API changes --- internal/chstorage/inserter_traces.go | 3 + internal/chstorage/querier_traces.go | 172 ++++++++++++++++++++++++-- internal/chstorage/schema_traces.go | 4 +- 3 files changed, 165 insertions(+), 14 deletions(-) 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..35248fbd 100644 --- a/internal/chstorage/querier_traces.go +++ b/internal/chstorage/querier_traces.go @@ -10,6 +10,8 @@ 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/collector/pdata/ptrace" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" @@ -88,11 +90,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 +108,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 +148,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: ptrace.StatusCodeUnset.String(), Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: ptrace.StatusCodeOk.String(), Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: ptrace.StatusCodeError.String(), 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: ptrace.SpanKindUnspecified.String(), Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: ptrace.SpanKindInternal.String(), Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: ptrace.SpanKindServer.String(), Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: ptrace.SpanKindClient.String(), Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: ptrace.SpanKindProducer.String(), Type: int32(pcommon.ValueTypeStr)}, + {Name: name, Value: ptrace.SpanKindConsumer.String(), 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 +271,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 +302,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` ) From d6ef0ec60ce287ea57e5504eea16e50990ca0cd9 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 07:47:29 +0300 Subject: [PATCH 14/22] feat(integration): filter ch-go logs --- integration/logger.go | 49 +++++++++++++++++++++++++++++++++ integration/lokie2e/ch_test.go | 3 +- integration/prome2e/ch_test.go | 2 ++ integration/tempoe2e/ch_test.go | 2 ++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 integration/logger.go 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) } From 2b314de0cedbb04081b6800b0407cd211852c822 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 08:42:56 +0300 Subject: [PATCH 15/22] fix(tempohandler): deduplicate tag names --- internal/tempohandler/tempohandler.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/tempohandler/tempohandler.go b/internal/tempohandler/tempohandler.go index 4521e793..11c1c1a8 100644 --- a/internal/tempohandler/tempohandler.go +++ b/internal/tempohandler/tempohandler.go @@ -16,6 +16,7 @@ import ( "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.uber.org/zap" + "golang.org/x/exp/maps" "github.com/go-faster/oteldb/internal/iterators" "github.com/go-faster/oteldb/internal/otelstorage" @@ -280,14 +281,14 @@ func (h *TempoAPI) SearchTags(ctx context.Context, params tempoapi.SearchTagsPar return nil, errors.Wrap(err, "get tag names") } - names := make([]string, len(tags)) - for i := range tags { - names = append(names, tags[i].Name) + names := make(map[string]struct{}, len(tags)) + for _, tag := range tags { + names[tag.Name] = struct{}{} } - lg.Debug("Got tag names", zap.Int("count", len(tags))) + lg.Debug("Got tag names", zap.Int("count", len(names))) return &tempoapi.TagNames{ - TagNames: names, + TagNames: maps.Keys(names), }, nil } From b19f545a2e7ece37e9be64153ca6b78be61fa18f Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 08:43:16 +0300 Subject: [PATCH 16/22] fix(tempoe2e): do not add span name to the tag set --- integration/tempoe2e/tempo_e2e.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/integration/tempoe2e/tempo_e2e.go b/integration/tempoe2e/tempo_e2e.go index e4abc83d..411e8ab2 100644 --- a/integration/tempoe2e/tempo_e2e.go +++ b/integration/tempoe2e/tempo_e2e.go @@ -11,6 +11,7 @@ import ( "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" ) @@ -59,20 +60,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)) } @@ -109,15 +109,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: @@ -126,6 +118,7 @@ func (s *BatchSet) addTags(m pcommon.Map) { Name: k, Value: v.AsString(), Type: int32(t), + Scope: scope, }) } return true From 6c570070de64ae0621cd5139316466d2bb798666 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 11:24:34 +0300 Subject: [PATCH 17/22] chore: formatting --- internal/lokihandler/lokihandler.go | 20 ++++++++++---------- internal/promhandler/promhandler.go | 4 ++-- internal/tempohandler/tempohandler.go | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) 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/tempohandler/tempohandler.go b/internal/tempohandler/tempohandler.go index 11c1c1a8..e98de179 100644 --- a/internal/tempohandler/tempohandler.go +++ b/internal/tempohandler/tempohandler.go @@ -26,14 +26,14 @@ import ( "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 } +var _ tempoapi.Handler = (*TempoAPI)(nil) + // NewTempoAPI creates new TempoAPI. func NewTempoAPI(q tracestorage.Querier, engine *traceqlengine.Engine) *TempoAPI { return &TempoAPI{ From 36bd388f544f81a60f7835ba0e623980435ad02a Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 11:29:12 +0300 Subject: [PATCH 18/22] fix(tempohandler): make autocomplete queries opt-in --- cmd/oteldb/app.go | 2 +- integration/tempoe2e/common_test.go | 2 +- internal/tempohandler/options.go | 12 +++++++++++ internal/tempohandler/tempohandler.go | 29 +++++++++++++++++++++------ 4 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 internal/tempohandler/options.go 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/tempoe2e/common_test.go b/integration/tempoe2e/common_test.go index 83a0fe25..5d4f1978 100644 --- a/integration/tempoe2e/common_test.go +++ b/integration/tempoe2e/common_test.go @@ -55,7 +55,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) 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 e98de179..537797cf 100644 --- a/internal/tempohandler/tempohandler.go +++ b/internal/tempohandler/tempohandler.go @@ -30,15 +30,24 @@ import ( 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, } } @@ -149,8 +158,13 @@ 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) - attr := traceql.Attribute{Name: params.TagName} - query := traceql.ParseAutocomplete(params.Q.Or(`{}`)) + 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, @@ -195,7 +209,10 @@ func (h *TempoAPI) SearchTagValuesV2(ctx context.Context, params tempoapi.Search if err != nil { return nil, err } - query := traceql.ParseAutocomplete(params.Q.Or(`{}`)) + 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, From c377a6c7c558fcb8697d3a7c94f4d0d106814386 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 13:39:43 +0300 Subject: [PATCH 19/22] fix(traceql): do not return empty intrinsic names --- internal/traceql/attribute.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/traceql/attribute.go b/internal/traceql/attribute.go index 03382e33..382a6a5b 100644 --- a/internal/traceql/attribute.go +++ b/internal/traceql/attribute.go @@ -121,7 +121,7 @@ const ( ) var intrinsicNames = func() (r []string) { - r = make([]string, TraceDuration) + r = make([]string, 0, TraceDuration) for i := SpanDuration; i <= TraceDuration; i++ { r = append(r, Attribute{Prop: i}.String()) } From 392c83ca1e551dd4c222be0efae6cd82f90b4cbc Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 13:40:25 +0300 Subject: [PATCH 20/22] fix(chstorage): return valid intrinsic enum values --- internal/chstorage/querier_traces.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/internal/chstorage/querier_traces.go b/internal/chstorage/querier_traces.go index 35248fbd..f66e699d 100644 --- a/internal/chstorage/querier_traces.go +++ b/internal/chstorage/querier_traces.go @@ -11,7 +11,6 @@ import ( "github.com/go-faster/errors" "github.com/go-faster/sdk/zctx" "go.opentelemetry.io/collector/pdata/pcommon" - "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" @@ -170,21 +169,21 @@ func (q *Querier) TagValues(ctx context.Context, tag traceql.Attribute, opts tra // TODO(tdakkota): probably we should do a proper query. name := tag.String() statuses := []tracestorage.Tag{ - {Name: name, Value: ptrace.StatusCodeUnset.String(), Type: int32(pcommon.ValueTypeStr)}, - {Name: name, Value: ptrace.StatusCodeOk.String(), Type: int32(pcommon.ValueTypeStr)}, - {Name: name, Value: ptrace.StatusCodeError.String(), Type: int32(pcommon.ValueTypeStr)}, + {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: ptrace.SpanKindUnspecified.String(), Type: int32(pcommon.ValueTypeStr)}, - {Name: name, Value: ptrace.SpanKindInternal.String(), Type: int32(pcommon.ValueTypeStr)}, - {Name: name, Value: ptrace.SpanKindServer.String(), Type: int32(pcommon.ValueTypeStr)}, - {Name: name, Value: ptrace.SpanKindClient.String(), Type: int32(pcommon.ValueTypeStr)}, - {Name: name, Value: ptrace.SpanKindProducer.String(), Type: int32(pcommon.ValueTypeStr)}, - {Name: name, Value: ptrace.SpanKindConsumer.String(), Type: int32(pcommon.ValueTypeStr)}, + {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: From 1005db639e6c5412bf5d9b6022a58ca846c192da Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 13:40:39 +0300 Subject: [PATCH 21/22] feat(tempohandler): implement `SearchTagsV2` --- internal/tempohandler/tempohandler.go | 67 ++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/internal/tempohandler/tempohandler.go b/internal/tempohandler/tempohandler.go index 537797cf..870c6cfc 100644 --- a/internal/tempohandler/tempohandler.go +++ b/internal/tempohandler/tempohandler.go @@ -12,7 +12,6 @@ import ( "github.com/go-faster/errors" "github.com/go-faster/sdk/zctx" "github.com/go-logfmt/logfmt" - ht "github.com/ogen-go/ogen/http" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" "go.uber.org/zap" @@ -315,7 +314,71 @@ func (h *TempoAPI) SearchTags(ctx context.Context, params tempoapi.SearchTagsPar // // GET /api/v2/search/tags func (h *TempoAPI) SearchTagsV2(ctx context.Context, params tempoapi.SearchTagsV2Params) (*tempoapi.TagNamesV2, error) { - return nil, ht.ErrNotImplemented + 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 } // TraceByID implements traceByID operation. From 9d60e6dde537df0d8d674438dc7e63d8eed399f3 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Thu, 23 May 2024 13:40:52 +0300 Subject: [PATCH 22/22] test(tempoe2e): add test for `SearchTagsV2` --- integration/tempoe2e/common_test.go | 93 +++++++++++++++++++++++++---- integration/tempoe2e/tempo_e2e.go | 19 +++--- 2 files changed, 95 insertions(+), 17 deletions(-) diff --git a/integration/tempoe2e/common_test.go b/integration/tempoe2e/common_test.go index 5d4f1978..4a636d8f 100644 --- a/integration/tempoe2e/common_test.go +++ b/integration/tempoe2e/common_test.go @@ -21,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" ) @@ -97,6 +98,42 @@ func runTest( 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) @@ -119,25 +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{ - AttributeSelector: "." + tagName, + 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 411e8ab2..a6a7d5b7 100644 --- a/integration/tempoe2e/tempo_e2e.go +++ b/integration/tempoe2e/tempo_e2e.go @@ -18,9 +18,10 @@ import ( // BatchSet is a set of batches. type BatchSet struct { - Batches []ptrace.Traces - Tags map[string][]tracestorage.Tag - Traces map[pcommon.TraceID]Trace + Batches []ptrace.Traces + Tags map[string][]tracestorage.Tag + Traces map[pcommon.TraceID]Trace + SpanNames map[string]struct{} Start otelstorage.Timestamp End otelstorage.Timestamp @@ -81,10 +82,6 @@ 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 } @@ -93,6 +90,9 @@ func (s *BatchSet) addSpan(span ptrace.Span) { } traceID := span.TraceID() + if s.Traces == nil { + s.Traces = map[pcommon.TraceID]Trace{} + } t, ok := s.Traces[traceID] if !ok { t = Trace{ @@ -102,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.