diff --git a/pkg/tsdb/jaeger/client.go b/pkg/tsdb/jaeger/client.go index 51c3ceb9d42..3e5ca5f5daf 100644 --- a/pkg/tsdb/jaeger/client.go +++ b/pkg/tsdb/jaeger/client.go @@ -1,6 +1,7 @@ package jaeger import ( + "context" "encoding/json" "fmt" "net/http" @@ -11,9 +12,10 @@ import ( ) type JaegerClient struct { - logger log.Logger - url string - httpClient *http.Client + logger log.Logger + url string + httpClient *http.Client + traceIdTimeEnabled bool } type ServicesResponse struct { @@ -24,11 +26,12 @@ type ServicesResponse struct { Total int `json:"total"` } -func New(url string, hc *http.Client, logger log.Logger) (JaegerClient, error) { +func New(url string, hc *http.Client, logger log.Logger, traceIdTimeEnabled bool) (JaegerClient, error) { client := JaegerClient{ - logger: logger, - url: url, - httpClient: hc, + logger: logger, + url: url, + httpClient: hc, + traceIdTimeEnabled: traceIdTimeEnabled, } return client, nil } @@ -88,3 +91,70 @@ func (j *JaegerClient) Operations(s string) ([]string, error) { operations = response.Data return operations, err } + +func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int64) (TraceResponse, error) { + logger := j.logger.FromContext(ctx) + var response TracesResponse + trace := TraceResponse{} + + if traceID == "" { + return trace, backend.DownstreamError(fmt.Errorf("traceID is empty")) + } + + traceUrl, err := url.JoinPath(j.url, "/api/traces", url.QueryEscape(traceID)) + if err != nil { + return trace, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err)) + } + + // Add time parameters if provided and traceIdTimeEnabled is true + if j.traceIdTimeEnabled { + if start > 0 || end > 0 { + parsedURL, err := url.Parse(traceUrl) + if err != nil { + return trace, backend.DownstreamError(fmt.Errorf("failed to parse url: %w", err)) + } + + query := parsedURL.Query() + if start > 0 { + query.Set("start", fmt.Sprintf("%d", start)) + } + if end > 0 { + query.Set("end", fmt.Sprintf("%d", end)) + } + + parsedURL.RawQuery = query.Encode() + traceUrl = parsedURL.String() + } + } + + res, err := j.httpClient.Get(traceUrl) + if err != nil { + if backend.IsDownstreamHTTPError(err) { + return trace, backend.DownstreamError(err) + } + return trace, err + } + + defer func() { + if err = res.Body.Close(); err != nil { + logger.Error("Failed to close response body", "error", err) + } + }() + + if res != nil && res.StatusCode/100 != 2 { + err := backend.DownstreamError(fmt.Errorf("request failed: %s", res.Status)) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return trace, backend.DownstreamError(err) + } + return trace, err + } + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return trace, err + } + + // We only support one trace at a time + // this is how it was implemented in the frontend before + trace = response.Data[0] + return trace, err +} diff --git a/pkg/tsdb/jaeger/client_test.go b/pkg/tsdb/jaeger/client_test.go index 9351cd07dfa..fc34fa61379 100644 --- a/pkg/tsdb/jaeger/client_test.go +++ b/pkg/tsdb/jaeger/client_test.go @@ -1,12 +1,14 @@ package jaeger import ( + "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/stretchr/testify/assert" ) @@ -58,7 +60,7 @@ func TestJaegerClient_Services(t *testing.T) { })) defer server.Close() - client, err := New(server.URL, server.Client(), log.NewNullLogger()) + client, err := New(server.URL, server.Client(), log.NewNullLogger(), false) assert.NoError(t, err) services, err := client.Services() @@ -147,7 +149,7 @@ func TestJaegerClient_Operations(t *testing.T) { })) defer server.Close() - client, err := New(server.URL, server.Client(), log.NewNullLogger()) + client, err := New(server.URL, server.Client(), log.NewNullLogger(), false) assert.NoError(t, err) operations, err := client.Operations(tt.service) @@ -164,3 +166,113 @@ func TestJaegerClient_Operations(t *testing.T) { }) } } + +func TestJaegerClient_Trace(t *testing.T) { + tests := []struct { + name string + traceId string + traceIdTimeEnabled bool + start int64 + end int64 + mockResponse string + mockStatusCode int + mockStatus string + expectedURL string + expectError bool + expectedError error + }{ + { + name: "Successful response with time params enabled", + traceId: "abc123", + traceIdTimeEnabled: true, + start: 1000, + end: 2000, + mockResponse: `{"data":[{"traceID":"abc123"}]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "/api/traces/abc123?end=2000&start=1000", + expectError: false, + expectedError: nil, + }, + { + name: "Successful response with time params disabled", + traceId: "abc123", + traceIdTimeEnabled: false, + start: 1000, + end: 2000, + mockResponse: `{"data":[{"traceID":"abc123"}]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "/api/traces/abc123", + expectError: false, + expectedError: nil, + }, + { + name: "Non-200 response", + traceId: "abc123", + traceIdTimeEnabled: true, + start: 1000, + end: 2000, + mockResponse: "", + mockStatusCode: http.StatusInternalServerError, + mockStatus: "Internal Server Error", + expectedURL: "/api/traces/abc123?end=2000&start=1000", + expectError: true, + expectedError: backend.PluginError(errors.New("Internal Server Error")), + }, + { + name: "Invalid JSON response", + traceId: "abc123", + traceIdTimeEnabled: true, + start: 1000, + end: 2000, + mockResponse: `{invalid json`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "/api/traces/abc123?end=2000&start=1000", + expectError: true, + expectedError: &json.SyntaxError{}, + }, + { + name: "Empty trace ID", + traceId: "", + traceIdTimeEnabled: true, + start: 1000, + end: 2000, + mockResponse: `{"data":[]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "", + expectError: true, + expectedError: backend.DownstreamError(errors.New("traceID is empty")), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var actualURL string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + actualURL = r.URL.String() + w.WriteHeader(tt.mockStatusCode) + _, _ = w.Write([]byte(tt.mockResponse)) + })) + defer server.Close() + + client, err := New(server.URL, server.Client(), log.NewNullLogger(), tt.traceIdTimeEnabled) + assert.NoError(t, err) + + trace, err := client.Trace(context.Background(), tt.traceId, tt.start, tt.end) + + if tt.expectError { + assert.Error(t, err) + if tt.expectedError != nil { + assert.IsType(t, tt.expectedError, err) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, trace) + } + assert.Equal(t, tt.expectedURL, actualURL) + }) + } +} diff --git a/pkg/tsdb/jaeger/jaeger.go b/pkg/tsdb/jaeger/jaeger.go index 1128192db1d..ec1144f5178 100644 --- a/pkg/tsdb/jaeger/jaeger.go +++ b/pkg/tsdb/jaeger/jaeger.go @@ -2,6 +2,7 @@ package jaeger import ( "context" + "encoding/json" "errors" "fmt" @@ -29,6 +30,12 @@ type datasourceInfo struct { JaegerClient JaegerClient } +type datasourceJSONData struct { + TraceIdTimeParams struct { + Enabled bool `json:"enabled"` + } `json:"traceIdTimeParams"` +} + func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { httpClientOptions, err := settings.HTTPClientOptions(ctx) @@ -45,8 +52,14 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst return nil, backend.DownstreamError(errors.New("error reading settings: url is empty")) } + var jsonData datasourceJSONData + err = json.Unmarshal(settings.JSONData, &jsonData) + if err != nil { + return nil, fmt.Errorf("error reading settings: %w", err) + } + logger := logger.FromContext(ctx) - jaegerClient, err := New(settings.URL, httpClient, logger) + jaegerClient, err := New(settings.URL, httpClient, logger, jsonData.TraceIdTimeParams.Enabled) return &datasourceInfo{JaegerClient: jaegerClient}, err } } @@ -91,3 +104,12 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq handler := httpadapter.New(s.registerResourceRoutes()) return handler.CallResource(ctx, req, sender) } + +func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + dsInfo, err := s.getDSInfo(ctx, req.PluginContext) + if err != nil { + return nil, err + } + + return queryData(ctx, dsInfo, req) +} diff --git a/pkg/tsdb/jaeger/jaeger_test.go b/pkg/tsdb/jaeger/jaeger_test.go new file mode 100644 index 00000000000..72214bf6ed3 --- /dev/null +++ b/pkg/tsdb/jaeger/jaeger_test.go @@ -0,0 +1,89 @@ +package jaeger + +import ( + "context" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDataSourceInstanceSettings_TraceIdTimeEnabled(t *testing.T) { + tests := []struct { + name string + jsonData string + expectedEnabled bool + expectError bool + }{ + { + name: "traceIdTimeParams enabled", + jsonData: `{ + "traceIdTimeParams": { + "enabled": true + } + }`, + expectedEnabled: true, + expectError: false, + }, + { + name: "traceIdTimeParams disabled", + jsonData: `{ + "traceIdTimeParams": { + "enabled": false + } + }`, + expectedEnabled: false, + expectError: false, + }, + { + name: "traceIdTimeParams not specified", + jsonData: `{}`, + expectedEnabled: false, + expectError: false, + }, + { + name: "traceIdTimeParams without enabled", + jsonData: `{"traceIdTimeParams":{}}`, + expectedEnabled: false, + expectError: false, + }, + { + name: "Invalid JSON", + jsonData: `{invalid json`, + expectedEnabled: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create instance settings + settings := backend.DataSourceInstanceSettings{ + JSONData: []byte(tt.jsonData), + URL: "http://localhost:16686", + } + + // Create instance factory + factory := newInstanceSettings(httpclient.NewProvider()) + instance, err := factory(context.Background(), settings) + + if tt.expectError { + assert.Error(t, err) + return + } + + require.NoError(t, err) + require.NotNil(t, instance) + + // Get the datasource info + dsInfo, ok := instance.(*datasourceInfo) + require.True(t, ok) + require.NotNil(t, dsInfo) + + // Verify the client's traceIdTimeEnabled parameter + assert.Equal(t, tt.expectedEnabled, dsInfo.JaegerClient.traceIdTimeEnabled) + }) + } +} diff --git a/pkg/tsdb/jaeger/querydata.go b/pkg/tsdb/jaeger/querydata.go new file mode 100644 index 00000000000..85aed936bff --- /dev/null +++ b/pkg/tsdb/jaeger/querydata.go @@ -0,0 +1,217 @@ +package jaeger + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +type jaegerQuery struct { + QueryType string `json:"queryType"` + Service string `json:"service"` + Operation string `json:"operation"` + Query string `json:"query"` + Tags string `json:"tags"` + MinDuration string `json:"minDuration"` + MaxDuration string `json:"maxDuration"` + Limit int `json:"limit"` +} + +func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + response := backend.NewQueryDataResponse() + + for _, q := range req.Queries { + var query jaegerQuery + + err := json.Unmarshal(q.JSON, &query) + if err != nil { + err = backend.DownstreamError(fmt.Errorf("error while parsing the query json. %w", err)) + response.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(err) + continue + } + + // No query type means traceID query + if query.QueryType == "" { + traces, err := dsInfo.JaegerClient.Trace(ctx, query.Query, q.TimeRange.From.UnixMilli(), q.TimeRange.To.UnixMilli()) + if err != nil { + response.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(err) + continue + } + frame := transformTraceResponse(traces, q.RefID) + response.Responses[q.RefID] = backend.DataResponse{ + Frames: []*data.Frame{frame}, + } + } + } + + return response, nil +} + +// transformTraceResponse converts Jaeger trace data to a Data frame +func transformTraceResponse(trace TraceResponse, refID string) *data.Frame { + frame := data.NewFrame(refID, + data.NewField("traceID", nil, []string{}), + data.NewField("spanID", nil, []string{}), + data.NewField("parentSpanID", nil, []*string{}), + data.NewField("operationName", nil, []string{}), + data.NewField("serviceName", nil, []string{}), + data.NewField("serviceTags", nil, []json.RawMessage{}), + data.NewField("startTime", nil, []float64{}), + data.NewField("duration", nil, []float64{}), + data.NewField("logs", nil, []json.RawMessage{}), + data.NewField("references", nil, []json.RawMessage{}), + data.NewField("tags", nil, []json.RawMessage{}), + data.NewField("warnings", nil, []json.RawMessage{}), + data.NewField("stackTraces", nil, []json.RawMessage{}), + ) + + // Set metadata for trace visualization + frame.Meta = &data.FrameMeta{ + PreferredVisualization: "trace", + Custom: map[string]interface{}{ + "traceFormat": "jaeger", + }, + } + + // Process each span in the trace + for _, span := range trace.Spans { + // Find parent span ID + var parentSpanID *string + for _, ref := range span.References { + if ref.RefType == "CHILD_OF" { + s := ref.SpanID + parentSpanID = &s + break + } + } + + // Get service name and tags + serviceName := "" + serviceTags := json.RawMessage{} + if process, ok := trace.Processes[span.ProcessID]; ok { + serviceName = process.ServiceName + tagsMarshaled, err := json.Marshal(process.Tags) + if err == nil { + serviceTags = json.RawMessage(tagsMarshaled) + } + } + + // Convert logs + logs := json.RawMessage{} + logsMarshaled, err := json.Marshal(span.Logs) + if err == nil { + logs = json.RawMessage(logsMarshaled) + } + + // Convert references (excluding parent) + references := json.RawMessage{} + filteredRefs := []TraceSpanReference{} + for _, ref := range span.References { + if parentSpanID == nil || ref.SpanID != *parentSpanID { + filteredRefs = append(filteredRefs, ref) + } + } + refsMarshaled, err := json.Marshal(filteredRefs) + if err == nil { + references = json.RawMessage(refsMarshaled) + } + + // Convert tags + tags := json.RawMessage{} + tagsMarshaled, err := json.Marshal(span.Tags) + if err == nil { + tags = json.RawMessage(tagsMarshaled) + } + + // Convert warnings + warnings := json.RawMessage{} + warningsMarshaled, err := json.Marshal(span.Warnings) + if err == nil { + warnings = json.RawMessage(warningsMarshaled) + } + + // Convert stack traces + stackTraces := json.RawMessage{} + stackTracesMarshaled, err := json.Marshal(span.StackTraces) + if err == nil { + stackTraces = json.RawMessage(stackTracesMarshaled) + } + + // Add span to frame + frame.AppendRow( + span.TraceID, + span.SpanID, + parentSpanID, + span.OperationName, + serviceName, + serviceTags, + float64(span.StartTime)/1000, // Convert microseconds to milliseconds + float64(span.Duration)/1000, // Convert microseconds to milliseconds + logs, + references, + tags, + warnings, + stackTraces, + ) + } + + return frame +} + +type TraceKeyValuePair struct { + Key string `json:"key"` + Type string `json:"type"` + Value interface{} `json:"value"` +} + +type TraceProcess struct { + ServiceName string `json:"serviceName"` + Tags []TraceKeyValuePair `json:"tags"` +} + +type TraceSpanReference struct { + RefType string `json:"refType"` + SpanID string `json:"spanID"` + TraceID string `json:"traceID"` +} + +type TraceLog struct { + // Millisecond epoch time + Timestamp int64 `json:"timestamp"` + Fields []TraceKeyValuePair `json:"fields"` + Name string `json:"name"` +} + +type Span struct { + TraceID string `json:"traceID"` + SpanID string `json:"spanID"` + ProcessID string `json:"processID"` + OperationName string `json:"operationName"` + // Times are in microseconds + StartTime int64 `json:"startTime"` + Duration int64 `json:"duration"` + Logs []TraceLog `json:"logs"` + References []TraceSpanReference `json:"references"` + Tags []TraceKeyValuePair `json:"tags"` + Warnings []string `json:"warnings"` + Flags int `json:"flags"` + StackTraces []string `json:"stackTraces"` +} + +type TraceResponse struct { + Processes map[string]TraceProcess `json:"processes"` + TraceID string `json:"traceID"` + Warnings []string `json:"warnings"` + Spans []Span `json:"spans"` +} + +type TracesResponse struct { + Data []TraceResponse `json:"data"` + Errors interface{} `json:"errors"` // TODO: Handle errors, but we were not using them in the frontend either + Limit int `json:"limit"` + Offset int `json:"offset"` + Total int `json:"total"` +} diff --git a/pkg/tsdb/jaeger/querydata_test.go b/pkg/tsdb/jaeger/querydata_test.go new file mode 100644 index 00000000000..8dc6dc3b0ad --- /dev/null +++ b/pkg/tsdb/jaeger/querydata_test.go @@ -0,0 +1,186 @@ +package jaeger + +import ( + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/experimental" +) + +func TestTransformTraceResponse(t *testing.T) { + t.Run("simple_trace", func(t *testing.T) { + trace := TraceResponse{ + TraceID: "3fa414edcef6ad90", + Spans: []Span{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + OperationName: "HTTP GET - api_traces_traceid", + StartTime: 1605873894680409, + Duration: 1049141, + Tags: []TraceKeyValuePair{ + {Key: "sampler.type", Type: "string", Value: "probabilistic"}, + {Key: "sampler.param", Type: "float64", Value: 1}, + }, + Logs: []TraceLog{}, + ProcessID: "p1", + Warnings: nil, + Flags: 0, + }, + { + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + OperationName: "/tempopb.Querier/FindTraceByID", + References: []TraceSpanReference{ + { + RefType: "CHILD_OF", + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + }, + }, + StartTime: 1605873894680587, + Duration: 1847, + Tags: []TraceKeyValuePair{ + {Key: "component", Type: "string", Value: "gRPC"}, + {Key: "span.kind", Type: "string", Value: "client"}, + }, + Logs: []TraceLog{}, + ProcessID: "p1", + Warnings: nil, + Flags: 0, + }, + }, + Processes: map[string]TraceProcess{ + "p1": { + ServiceName: "tempo-querier", + Tags: []TraceKeyValuePair{ + {Key: "cluster", Type: "string", Value: "ops-tools1"}, + {Key: "container", Type: "string", Value: "tempo-query"}, + }, + }, + }, + Warnings: nil, + } + + frame := transformTraceResponse(trace, "test") + experimental.CheckGoldenJSONFrame(t, "./testdata", "simple_trace.golden", frame, false) + }) + + t.Run("complex_trace", func(t *testing.T) { + trace := TraceResponse{ + TraceID: "3fa414edcef6ad90", + Spans: []Span{ + { + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + OperationName: "HTTP GET - api_traces_traceid", + References: []TraceSpanReference{}, + StartTime: 1605873894680409, + Duration: 1049141, + Tags: []TraceKeyValuePair{ + {Key: "sampler.type", Type: "string", Value: "probabilistic"}, + {Key: "sampler.param", Type: "float64", Value: 1}, + {Key: "error", Type: "bool", Value: true}, + {Key: "http.status_code", Type: "int", Value: 500}, + }, + Logs: []TraceLog{ + { + Timestamp: 1605873894681000, + Fields: []TraceKeyValuePair{ + {Key: "event", Type: "string", Value: "error"}, + {Key: "message", Type: "string", Value: "Internal server error"}, + }, + }, + }, + ProcessID: "p1", + Warnings: []string{"High latency detected", "Error rate above threshold"}, + Flags: 0, + }, + { + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + OperationName: "/tempopb.Querier/FindTraceByID", + References: []TraceSpanReference{ + { + RefType: "CHILD_OF", + TraceID: "3fa414edcef6ad90", + SpanID: "3fa414edcef6ad90", + }, + }, + StartTime: 1605873894680587, + Duration: 1847, + Tags: []TraceKeyValuePair{ + {Key: "component", Type: "string", Value: "gRPC"}, + {Key: "span.kind", Type: "string", Value: "client"}, + {Key: "error", Type: "bool", Value: true}, + {Key: "grpc.status_code", Type: "int", Value: 13}, + }, + Logs: []TraceLog{ + { + Timestamp: 1605873894680700, + Fields: []TraceKeyValuePair{ + {Key: "event", Type: "string", Value: "error"}, + {Key: "message", Type: "string", Value: "gRPC error: INTERNAL"}, + }, + }, + }, + ProcessID: "p1", + Warnings: []string{"gRPC call failed", "Retry attempt 3"}, + Flags: 0, + }, + { + TraceID: "3fa414edcef6ad90", + SpanID: "1a2b3c4d5e6f7g8h", + OperationName: "db.query", + References: []TraceSpanReference{ + { + RefType: "CHILD_OF", + TraceID: "3fa414edcef6ad90", + SpanID: "0f5c1808567e4403", + }, + }, + StartTime: 1605873894680800, + Duration: 500, + Tags: []TraceKeyValuePair{ + {Key: "db.type", Type: "string", Value: "postgresql"}, + {Key: "db.statement", Type: "string", Value: "SELECT * FROM traces WHERE id = $1"}, + {Key: "error", Type: "bool", Value: true}, + }, + Logs: []TraceLog{ + { + Timestamp: 1605873894680850, + Fields: []TraceKeyValuePair{ + {Key: "event", Type: "string", Value: "error"}, + {Key: "message", Type: "string", Value: "Database connection timeout"}, + }, + }, + }, + ProcessID: "p2", + Warnings: []string{"Database connection slow", "Query timeout"}, + Flags: 0, + }, + }, + Processes: map[string]TraceProcess{ + "p1": { + ServiceName: "tempo-querier", + Tags: []TraceKeyValuePair{ + {Key: "cluster", Type: "string", Value: "ops-tools1"}, + {Key: "container", Type: "string", Value: "tempo-query"}, + {Key: "version", Type: "string", Value: "1.2.3"}, + }, + }, + "p2": { + ServiceName: "tempo-storage", + Tags: []TraceKeyValuePair{ + {Key: "cluster", Type: "string", Value: "ops-tools1"}, + {Key: "container", Type: "string", Value: "tempo-storage"}, + {Key: "version", Type: "string", Value: "2.0.1"}, + }, + }, + }, + Warnings: []string{"Trace contains errors", "Multiple service failures"}, + } + + frame := transformTraceResponse(trace, "test") + experimental.CheckGoldenJSONFrame(t, "./testdata", "complex_trace.golden", frame, false) + }) +} diff --git a/pkg/tsdb/jaeger/testdata/complex_trace.golden.jsonc b/pkg/tsdb/jaeger/testdata/complex_trace.golden.jsonc new file mode 100644 index 00000000000..14a4df4ef37 --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/complex_trace.golden.jsonc @@ -0,0 +1,375 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "traceFormat": "jaeger" +// }, +// "preferredVisualisationType": "trace" +// } +// Name: test +// Dimensions: 13 Fields by 3 Rows +// +------------------+------------------+--------------------+--------------------------------+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------+-------------------------+ +// | Name: traceID | Name: spanID | Name: parentSpanID | Name: operationName | Name: serviceName | Name: serviceTags | Name: startTime | Name: duration | Name: logs | Name: references | Name: tags | Name: warnings | Name: stackTraces | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []*string | Type: []string | Type: []string | Type: []json.RawMessage | Type: []float64 | Type: []float64 | Type: []json.RawMessage | Type: []json.RawMessage | Type: []json.RawMessage | Type: []json.RawMessage | Type: []json.RawMessage | +// +------------------+------------------+--------------------+--------------------------------+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------+-------------------------+ +// | 3fa414edcef6ad90 | 3fa414edcef6ad90 | null | HTTP GET - api_traces_traceid | tempo-querier | [{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-query"},{"key":"version","type":"string","value":"1.2.3"}] | 1.605873894680409e+12 | 1049.141 | [{"timestamp":1605873894681000,"fields":[{"key":"event","type":"string","value":"error"},{"key":"message","type":"string","value":"Internal server error"}],"name":""}] | [] | [{"key":"sampler.type","type":"string","value":"probabilistic"},{"key":"sampler.param","type":"float64","value":1},{"key":"error","type":"bool","value":true},{"key":"http.status_code","type":"int","value":500}] | ["High latency detected","Error rate above threshold"] | null | +// | 3fa414edcef6ad90 | 0f5c1808567e4403 | 3fa414edcef6ad90 | /tempopb.Querier/FindTraceByID | tempo-querier | [{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-query"},{"key":"version","type":"string","value":"1.2.3"}] | 1.605873894680587e+12 | 1.847 | [{"timestamp":1605873894680700,"fields":[{"key":"event","type":"string","value":"error"},{"key":"message","type":"string","value":"gRPC error: INTERNAL"}],"name":""}] | [] | [{"key":"component","type":"string","value":"gRPC"},{"key":"span.kind","type":"string","value":"client"},{"key":"error","type":"bool","value":true},{"key":"grpc.status_code","type":"int","value":13}] | ["gRPC call failed","Retry attempt 3"] | null | +// | 3fa414edcef6ad90 | 1a2b3c4d5e6f7g8h | 0f5c1808567e4403 | db.query | tempo-storage | [{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-storage"},{"key":"version","type":"string","value":"2.0.1"}] | 1.6058738946808e+12 | 0.5 | [{"timestamp":1605873894680850,"fields":[{"key":"event","type":"string","value":"error"},{"key":"message","type":"string","value":"Database connection timeout"}],"name":""}] | [] | [{"key":"db.type","type":"string","value":"postgresql"},{"key":"db.statement","type":"string","value":"SELECT * FROM traces WHERE id = $1"},{"key":"error","type":"bool","value":true}] | ["Database connection slow","Query timeout"] | null | +// +------------------+------------------+--------------------+--------------------------------+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------+-------------------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "custom": { + "traceFormat": "jaeger" + }, + "preferredVisualisationType": "trace" + }, + "fields": [ + { + "name": "traceID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "spanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "parentSpanID", + "type": "string", + "typeInfo": { + "frame": "string", + "nullable": true + } + }, + { + "name": "operationName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceTags", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "startTime", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "duration", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "logs", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "references", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "tags", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "warnings", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "stackTraces", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + } + ] + }, + "data": { + "values": [ + [ + "3fa414edcef6ad90", + "3fa414edcef6ad90", + "3fa414edcef6ad90" + ], + [ + "3fa414edcef6ad90", + "0f5c1808567e4403", + "1a2b3c4d5e6f7g8h" + ], + [ + null, + "3fa414edcef6ad90", + "0f5c1808567e4403" + ], + [ + "HTTP GET - api_traces_traceid", + "/tempopb.Querier/FindTraceByID", + "db.query" + ], + [ + "tempo-querier", + "tempo-querier", + "tempo-storage" + ], + [ + [ + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-query" + }, + { + "key": "version", + "type": "string", + "value": "1.2.3" + } + ], + [ + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-query" + }, + { + "key": "version", + "type": "string", + "value": "1.2.3" + } + ], + [ + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-storage" + }, + { + "key": "version", + "type": "string", + "value": "2.0.1" + } + ] + ], + [ + 1605873894680.409, + 1605873894680.587, + 1605873894680.8 + ], + [ + 1049.141, + 1.847, + 0.5 + ], + [ + [ + { + "timestamp": 1605873894681000, + "fields": [ + { + "key": "event", + "type": "string", + "value": "error" + }, + { + "key": "message", + "type": "string", + "value": "Internal server error" + } + ], + "name": "" + } + ], + [ + { + "timestamp": 1605873894680700, + "fields": [ + { + "key": "event", + "type": "string", + "value": "error" + }, + { + "key": "message", + "type": "string", + "value": "gRPC error: INTERNAL" + } + ], + "name": "" + } + ], + [ + { + "timestamp": 1605873894680850, + "fields": [ + { + "key": "event", + "type": "string", + "value": "error" + }, + { + "key": "message", + "type": "string", + "value": "Database connection timeout" + } + ], + "name": "" + } + ] + ], + [ + [], + [], + [] + ], + [ + [ + { + "key": "sampler.type", + "type": "string", + "value": "probabilistic" + }, + { + "key": "sampler.param", + "type": "float64", + "value": 1 + }, + { + "key": "error", + "type": "bool", + "value": true + }, + { + "key": "http.status_code", + "type": "int", + "value": 500 + } + ], + [ + { + "key": "component", + "type": "string", + "value": "gRPC" + }, + { + "key": "span.kind", + "type": "string", + "value": "client" + }, + { + "key": "error", + "type": "bool", + "value": true + }, + { + "key": "grpc.status_code", + "type": "int", + "value": 13 + } + ], + [ + { + "key": "db.type", + "type": "string", + "value": "postgresql" + }, + { + "key": "db.statement", + "type": "string", + "value": "SELECT * FROM traces WHERE id = $1" + }, + { + "key": "error", + "type": "bool", + "value": true + } + ] + ], + [ + [ + "High latency detected", + "Error rate above threshold" + ], + [ + "gRPC call failed", + "Retry attempt 3" + ], + [ + "Database connection slow", + "Query timeout" + ] + ], + [ + null, + null, + null + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/jaeger/testdata/simple_trace.golden.jsonc b/pkg/tsdb/jaeger/testdata/simple_trace.golden.jsonc new file mode 100644 index 00000000000..1eb3234f7e2 --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/simple_trace.golden.jsonc @@ -0,0 +1,239 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "traceFormat": "jaeger" +// }, +// "preferredVisualisationType": "trace" +// } +// Name: test +// Dimensions: 13 Fields by 2 Rows +// +------------------+------------------+--------------------+--------------------------------+-------------------+--------------------------------------------------------------------------------------------------------------------+-----------------------+-----------------+-------------------------+-------------------------+---------------------------------------------------------------------------------------------------------------------+-------------------------+-------------------------+ +// | Name: traceID | Name: spanID | Name: parentSpanID | Name: operationName | Name: serviceName | Name: serviceTags | Name: startTime | Name: duration | Name: logs | Name: references | Name: tags | Name: warnings | Name: stackTraces | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []*string | Type: []string | Type: []string | Type: []json.RawMessage | Type: []float64 | Type: []float64 | Type: []json.RawMessage | Type: []json.RawMessage | Type: []json.RawMessage | Type: []json.RawMessage | Type: []json.RawMessage | +// +------------------+------------------+--------------------+--------------------------------+-------------------+--------------------------------------------------------------------------------------------------------------------+-----------------------+-----------------+-------------------------+-------------------------+---------------------------------------------------------------------------------------------------------------------+-------------------------+-------------------------+ +// | 3fa414edcef6ad90 | 3fa414edcef6ad90 | null | HTTP GET - api_traces_traceid | tempo-querier | [{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-query"}] | 1.605873894680409e+12 | 1049.141 | [] | [] | [{"key":"sampler.type","type":"string","value":"probabilistic"},{"key":"sampler.param","type":"float64","value":1}] | null | null | +// | 3fa414edcef6ad90 | 0f5c1808567e4403 | 3fa414edcef6ad90 | /tempopb.Querier/FindTraceByID | tempo-querier | [{"key":"cluster","type":"string","value":"ops-tools1"},{"key":"container","type":"string","value":"tempo-query"}] | 1.605873894680587e+12 | 1.847 | [] | [] | [{"key":"component","type":"string","value":"gRPC"},{"key":"span.kind","type":"string","value":"client"}] | null | null | +// +------------------+------------------+--------------------+--------------------------------+-------------------+--------------------------------------------------------------------------------------------------------------------+-----------------------+-----------------+-------------------------+-------------------------+---------------------------------------------------------------------------------------------------------------------+-------------------------+-------------------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "custom": { + "traceFormat": "jaeger" + }, + "preferredVisualisationType": "trace" + }, + "fields": [ + { + "name": "traceID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "spanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "parentSpanID", + "type": "string", + "typeInfo": { + "frame": "string", + "nullable": true + } + }, + { + "name": "operationName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceTags", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "startTime", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "duration", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "logs", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "references", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "tags", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "warnings", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + }, + { + "name": "stackTraces", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + } + ] + }, + "data": { + "values": [ + [ + "3fa414edcef6ad90", + "3fa414edcef6ad90" + ], + [ + "3fa414edcef6ad90", + "0f5c1808567e4403" + ], + [ + null, + "3fa414edcef6ad90" + ], + [ + "HTTP GET - api_traces_traceid", + "/tempopb.Querier/FindTraceByID" + ], + [ + "tempo-querier", + "tempo-querier" + ], + [ + [ + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-query" + } + ], + [ + { + "key": "cluster", + "type": "string", + "value": "ops-tools1" + }, + { + "key": "container", + "type": "string", + "value": "tempo-query" + } + ] + ], + [ + 1605873894680.409, + 1605873894680.587 + ], + [ + 1049.141, + 1.847 + ], + [ + [], + [] + ], + [ + [], + [] + ], + [ + [ + { + "key": "sampler.type", + "type": "string", + "value": "probabilistic" + }, + { + "key": "sampler.param", + "type": "float64", + "value": 1 + } + ], + [ + { + "key": "component", + "type": "string", + "value": "gRPC" + }, + { + "key": "span.kind", + "type": "string", + "value": "client" + } + ] + ], + [ + null, + null + ], + [ + null, + null + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index bb8ccf2d932..3c7b33c2109 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -69,6 +69,16 @@ export class JaegerDatasource extends DataSourceWithBackend): Observable { + // No query type means that the query is a trace ID query + // If all targets are trace ID queries, we can use the backend querying + const allTargetsTraceIdQuery = options.targets.every((target) => !target.queryType); + // We have not migrated the node graph to the backend + // If the node graph is disabled, we can use the backend migration + const nodeGraphDisabled = !this.nodeGraph?.enabled; + if (config.featureToggles.jaegerBackendMigration && allTargetsTraceIdQuery && nodeGraphDisabled) { + return super.query(options); + } + // At this moment we expect only one target. In case we somehow change the UI to be able to show multiple // traces at one we need to change this. const target: JaegerQuery = options.targets[0]; @@ -131,7 +141,7 @@ export class JaegerDatasource extends DataSourceWithBackend