d0ea82633f
* Jaeger: Migrate Services and Operations to the gRPC Jaeger endpoint (#112384) * add grpc feature toggle * move types into types.go * creates grpc client functions for services and operations * Call grpc services function when feature flag is enabled for health check * remove unnecessary double encoding * check for successful status code before decoding response and return nil in case of successful response * remove duplicate code * use variable * fix error type in testsz * Jaeger: Migrate search and Trace Search calls to use gRPC endpoint (#112610) * move all types into types package except for JagerClient * move all helper functions into utils package * change return type of search function to be frames and add grpc search functionality * fix tests * fix types and the way we check error response from grpc * change trace name and duration unit conversion * fix types and add tests * support queryAttributes * quick limit implementation in post processing * add todo for attributes / tags * make trace functionality ready to support grpc flow * add functions to process search response for a specific trace and create the Trace frame * tests for helper funtions * remove grpc querying for now! * change logic to be able to process and support multiple resource spans * remove logic for gRPC from grpc_client.go * add equivalent fields for logs and references * add tests for grpcTraceResponse function * fix types after merge with main * fix status code checks and return nil for error on successful responses * enable reading through config flag for trace search * create sigle key value type since they are similar for OTLP and non OTLP based formats * reference right type * convert events and links into references and logs * add status code, status message and kind to data frame * fix tests to accomodate new format * remove unused function and add more tests * remove edit flag for jsonc golden test files * add clarifying comment * fix tests and linting * fix golden files for testing * fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add clarifying comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove unnecessary logging statement * fix downstream errors --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * use downstreamerrorf where applicable and add missing downstream eror sources. * tests --------- Co-authored-by: ismail simsek <ismailsimsek09@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
273 lines
8.6 KiB
Go
273 lines
8.6 KiB
Go
package jaeger
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-logfmt/logfmt"
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
"github.com/grafana/grafana/pkg/tsdb/jaeger/types"
|
|
"github.com/grafana/grafana/pkg/tsdb/jaeger/utils"
|
|
)
|
|
|
|
func (j *JaegerClient) GrpcServices() ([]string, error) {
|
|
var response types.GrpcServicesResponse
|
|
services := []string{}
|
|
|
|
u, err := url.JoinPath(j.url, "/api/v3/services")
|
|
if err != nil {
|
|
return services, backend.DownstreamErrorf("failed to join url: %w", err)
|
|
}
|
|
|
|
res, err := j.httpClient.Get(u)
|
|
if err != nil {
|
|
if backend.IsDownstreamHTTPError(err) {
|
|
return services, backend.DownstreamError(err)
|
|
}
|
|
return services, err
|
|
}
|
|
|
|
defer func() {
|
|
if err = res.Body.Close(); err != nil {
|
|
j.logger.Error("Failed to close response body", "error", err)
|
|
}
|
|
}()
|
|
|
|
if res != nil && res.StatusCode != http.StatusOK {
|
|
err := fmt.Errorf("request failed: %s", res.Status)
|
|
if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream {
|
|
return services, backend.DownstreamError(err)
|
|
}
|
|
return services, err
|
|
}
|
|
|
|
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
|
|
return services, backend.DownstreamError(err)
|
|
}
|
|
|
|
services = response.Services
|
|
return services, nil
|
|
}
|
|
|
|
func (j *JaegerClient) GrpcOperations(s string) ([]string, error) {
|
|
var response types.GrpcOperationsResponse
|
|
operations := []string{}
|
|
|
|
u, err := url.JoinPath(j.url, "/api/v3/operations")
|
|
if err != nil {
|
|
return operations, backend.DownstreamErrorf("failed to join url: %w", err)
|
|
}
|
|
|
|
jaegerURL, err := url.Parse(u)
|
|
if err != nil {
|
|
return operations, backend.DownstreamErrorf("failed to parse Jaeger URL: %w", err)
|
|
}
|
|
|
|
urlQuery := jaegerURL.Query()
|
|
urlQuery.Set("service", s)
|
|
jaegerURL.RawQuery = urlQuery.Encode()
|
|
|
|
res, err := j.httpClient.Get(jaegerURL.String())
|
|
if err != nil {
|
|
if backend.IsDownstreamHTTPError(err) {
|
|
return operations, backend.DownstreamError(err)
|
|
}
|
|
return operations, err
|
|
}
|
|
|
|
defer func() {
|
|
if err = res.Body.Close(); err != nil {
|
|
j.logger.Error("Failed to close response body", "error", err)
|
|
}
|
|
}()
|
|
|
|
if res != nil && res.StatusCode != http.StatusOK {
|
|
err := fmt.Errorf("request failed: %s", res.Status)
|
|
if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream {
|
|
return operations, backend.DownstreamError(err)
|
|
}
|
|
return operations, err
|
|
}
|
|
|
|
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
|
|
return operations, backend.DownstreamError(err)
|
|
}
|
|
|
|
// extract name from operations response
|
|
for _, op := range response.Operations {
|
|
operations = append(operations, op.Name)
|
|
}
|
|
|
|
return operations, nil
|
|
}
|
|
|
|
// Note that this and all functionality around search is not yet being used. Once Jaeger adds support for attributes and limit parameters
|
|
// we will be able to start using this and routing traffic to the new API based on the feature flag.
|
|
func (j *JaegerClient) GrpcSearch(query *JaegerQuery, start, end time.Time) (*data.Frame, error) {
|
|
u, err := url.JoinPath(j.url, "/api/v3/traces")
|
|
if err != nil {
|
|
return nil, backend.DownstreamErrorf("failed to join url path: %w", err)
|
|
}
|
|
|
|
jaegerURL, err := url.Parse(u)
|
|
if err != nil {
|
|
return nil, backend.DownstreamErrorf("failed to parse Jaeger URL: %w", err)
|
|
}
|
|
|
|
var queryTags string
|
|
if query.Tags != "" {
|
|
tagMap := make(map[string]string)
|
|
decoder := logfmt.NewDecoder(strings.NewReader(query.Tags))
|
|
for decoder.ScanRecord() {
|
|
for decoder.ScanKeyval() {
|
|
key := decoder.Key()
|
|
value := decoder.Value()
|
|
tagMap[string(key)] = string(value)
|
|
}
|
|
}
|
|
|
|
marshaledTags, err := json.Marshal(tagMap)
|
|
if err != nil {
|
|
return nil, backend.DownstreamErrorf("failed to convert tags to JSON: %w", err)
|
|
}
|
|
|
|
queryTags = string(marshaledTags)
|
|
}
|
|
|
|
queryParams := map[string]string{
|
|
"query.service_name": query.Service,
|
|
"query.operation_name": query.Operation,
|
|
"query.attributes": queryTags, // TODO: no native support of attributes/tags figure out if we want to do it in post processing.
|
|
"query.duration_min": query.MinDuration,
|
|
"query.duration_max": query.MaxDuration,
|
|
}
|
|
|
|
urlQuery := jaegerURL.Query()
|
|
|
|
for key, value := range queryParams {
|
|
if value != "" {
|
|
urlQuery.Set(key, value)
|
|
}
|
|
}
|
|
|
|
jaegerURL.RawQuery = urlQuery.Encode()
|
|
// jaeger will not be able to process the request if the time is encoded, all other parameters are encoded except for the start and end time
|
|
jaegerURL.RawQuery += fmt.Sprintf("&query.start_time_min=%s&query.start_time_max=%s", start.Format(time.RFC3339Nano), end.Format(time.RFC3339Nano))
|
|
resp, err := j.httpClient.Get(jaegerURL.String())
|
|
if err != nil {
|
|
if backend.IsDownstreamHTTPError(err) {
|
|
return nil, backend.DownstreamError(err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if err = resp.Body.Close(); err != nil {
|
|
j.logger.Error("Failed to close response body", "error", err)
|
|
}
|
|
}()
|
|
|
|
if resp != nil && resp.StatusCode != http.StatusOK {
|
|
err := fmt.Errorf("request failed: %s", resp.Status)
|
|
if backend.ErrorSourceFromHTTPStatus(resp.StatusCode) == backend.ErrorSourceDownstream {
|
|
return nil, backend.DownstreamError(err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var response types.GrpcTracesResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
|
return nil, backend.DownstreamErrorf("failed to decode Jaeger response: %w", err)
|
|
}
|
|
|
|
// for search call, an unsuccessful response is exposed through the error attribute
|
|
// see https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L77C17-L79C18
|
|
if response.Error.HttpCode != 0 && response.Error.HttpCode != http.StatusOK {
|
|
err := fmt.Errorf("request failed %s", response.Error.Message)
|
|
if backend.ErrorSourceFromHTTPStatus(response.Error.HttpCode) == backend.ErrorSourceDownstream {
|
|
return nil, backend.DownstreamError(err)
|
|
}
|
|
return nil, err
|
|
}
|
|
frames := utils.TransformGrpcSearchResponse(response.Result, j.settings.UID, j.settings.Name, query.Limit)
|
|
return frames, nil
|
|
}
|
|
|
|
func (j *JaegerClient) GrpcTrace(ctx context.Context, traceID string, start, end time.Time, refID string) (*data.Frame, error) {
|
|
logger := j.logger.FromContext(ctx)
|
|
var response types.GrpcTracesResponse
|
|
|
|
if traceID == "" {
|
|
return nil, backend.DownstreamErrorf("traceID is empty")
|
|
}
|
|
|
|
traceUrl, err := url.JoinPath(j.url, "/api/v3/traces", url.QueryEscape(traceID))
|
|
if err != nil {
|
|
return nil, backend.DownstreamErrorf("failed to join url: %w", err)
|
|
}
|
|
|
|
var jsonData types.SettingsJSONData
|
|
if err := json.Unmarshal(j.settings.JSONData, &jsonData); err != nil {
|
|
return nil, backend.DownstreamErrorf("failed to parse settings JSON data: %w", err)
|
|
}
|
|
|
|
// Add time parameters if trace ID time is enabled and time range is provided
|
|
if jsonData.TraceIdTimeParams.Enabled {
|
|
if start.UnixMicro() > 0 || end.UnixMicro() > 0 {
|
|
parsedURL, err := url.Parse(traceUrl)
|
|
if err != nil {
|
|
return nil, backend.DownstreamErrorf("failed to parse url: %w", err)
|
|
}
|
|
|
|
// jaeger will not be able to process the request if the time is encoded, all other parameters are encoded except for the start and end time
|
|
parsedURL.RawQuery += fmt.Sprintf("start_time=%s&end_time=%s", start.Format(time.RFC3339Nano), end.Format(time.RFC3339Nano))
|
|
traceUrl = parsedURL.String()
|
|
}
|
|
}
|
|
|
|
res, err := j.httpClient.Get(traceUrl)
|
|
if err != nil {
|
|
if backend.IsDownstreamHTTPError(err) {
|
|
return nil, backend.DownstreamError(err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if err = res.Body.Close(); err != nil {
|
|
logger.Error("Failed to close response body", "error", err)
|
|
}
|
|
}()
|
|
|
|
if res != nil && res.StatusCode != http.StatusOK {
|
|
err := fmt.Errorf("request failed: %s", res.Status)
|
|
if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream {
|
|
return nil, backend.DownstreamError(err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// for trace search call, an unsuccessful response is exposed through the error attribute
|
|
// see https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L77C17-L79C18
|
|
if response.Error.HttpCode != 0 && response.Error.HttpCode != http.StatusOK {
|
|
err := fmt.Errorf("request failed %s", response.Error.Message)
|
|
if backend.ErrorSourceFromHTTPStatus(response.Error.HttpCode) == backend.ErrorSourceDownstream {
|
|
return nil, backend.DownstreamError(err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
frame := utils.TransformGrpcTraceResponse(response.Result.ResourceSpans, refID)
|
|
return frame, nil
|
|
}
|