Jaeger: Migrate "search" query type to backend (#103399)

* jaeger backend migration

* move processing to JaegerClient.Search

* fix upload query error source type

* suggestions

* lint

* fix link to traceid query

* fix tests

* fix tests

* use consistent types

* add tests for TransformSearchResponse fn

* test search function

* fix filtering using tags

* suggestion

* remove unnecessary arguments

* use logfmt parser for tags

* test

* test

* use logfmt for query tags

* update

* go fmt

* run backend for all queryTypes

* run make update-workspace

* assign owner to logfmt pkg in go.mod

* apply suggestions

* update tests

* trigger workflows?
pull/104996/head^2
Gareth Dawson 2 months ago committed by GitHub
parent a4efb73774
commit 977e923555
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      go.mod
  2. 119
      pkg/tsdb/jaeger/client.go
  3. 243
      pkg/tsdb/jaeger/client_test.go
  4. 2
      pkg/tsdb/jaeger/jaeger.go
  5. 9
      pkg/tsdb/jaeger/jaeger_test.go
  6. 232
      pkg/tsdb/jaeger/querydata.go
  7. 102
      pkg/tsdb/jaeger/querydata_test.go
  8. 100
      pkg/tsdb/jaeger/testdata/search_empty_response.golden.jsonc
  9. 123
      pkg/tsdb/jaeger/testdata/search_multiple_response.golden.jsonc
  10. 117
      pkg/tsdb/jaeger/testdata/search_single_response.golden.jsonc
  11. 9
      public/app/plugins/datasource/jaeger/datasource.ts

@ -354,7 +354,7 @@ require (
github.com/gammazero/deque v0.2.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // @grafana/oss-big-tent
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect

@ -6,16 +6,18 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/go-logfmt/logfmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)
type JaegerClient struct {
logger log.Logger
url string
httpClient *http.Client
traceIdTimeEnabled bool
logger log.Logger
url string
httpClient *http.Client
settings backend.DataSourceInstanceSettings
}
type ServicesResponse struct {
@ -26,6 +28,12 @@ type ServicesResponse struct {
Total int `json:"total"`
}
type SettingsJSONData struct {
TraceIdTimeParams struct {
Enabled bool `json:"enabled"`
} `json:"traceIdTimeParams"`
}
type DependenciesResponse struct {
Data []ServiceDependency `json:"data"`
Errors []struct {
@ -40,12 +48,12 @@ type ServiceDependency struct {
CallCount int `json:"callCount"`
}
func New(url string, hc *http.Client, logger log.Logger, traceIdTimeEnabled bool) (JaegerClient, error) {
func New(hc *http.Client, logger log.Logger, settings backend.DataSourceInstanceSettings) (JaegerClient, error) {
client := JaegerClient{
logger: logger,
url: url,
httpClient: hc,
traceIdTimeEnabled: traceIdTimeEnabled,
logger: logger,
url: settings.URL,
httpClient: hc,
settings: settings,
}
return client, nil
}
@ -106,6 +114,90 @@ func (j *JaegerClient) Operations(s string) ([]string, error) {
return operations, err
}
func (j *JaegerClient) Search(query *JaegerQuery, start, end int64) ([]TraceResponse, error) {
jaegerURL, err := url.Parse(j.url)
if err != nil {
return []TraceResponse{}, fmt.Errorf("failed to parse Jaeger URL: %w", err)
}
jaegerURL.Path = "/api/traces"
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 []TraceResponse{}, fmt.Errorf("failed to convert tags to JSON: %w", err)
}
queryTags = string(marshaledTags)
}
queryParams := map[string]string{
"service": query.Service,
"operation": query.Operation,
"tags": queryTags,
"minDuration": query.MinDuration,
"maxDuration": query.MaxDuration,
}
urlQuery := jaegerURL.Query()
if query.Limit > 0 {
urlQuery.Set("limit", fmt.Sprintf("%d", query.Limit))
}
if start > 0 {
urlQuery.Set("start", fmt.Sprintf("%d", start))
}
if end > 0 {
urlQuery.Set("end", fmt.Sprintf("%d", end))
}
for key, value := range queryParams {
if value != "" {
urlQuery.Set(key, value)
}
}
jaegerURL.RawQuery = urlQuery.Encode()
resp, err := j.httpClient.Get(jaegerURL.String())
if err != nil {
if backend.IsDownstreamHTTPError(err) {
return []TraceResponse{}, backend.DownstreamError(err)
}
return []TraceResponse{}, err
}
defer func() {
if err = resp.Body.Close(); err != nil {
j.logger.Error("Failed to close response body", "error", err)
}
}()
if resp.StatusCode != http.StatusOK {
err := backend.DownstreamError(fmt.Errorf("request failed: %s", resp.Status))
if backend.ErrorSourceFromHTTPStatus(resp.StatusCode) == backend.ErrorSourceDownstream {
return []TraceResponse{}, backend.DownstreamError(err)
}
return []TraceResponse{}, err
}
var result TracesResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return []TraceResponse{}, fmt.Errorf("failed to decode Jaeger response: %w", err)
}
return result.Data, nil
}
func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int64) (TraceResponse, error) {
logger := j.logger.FromContext(ctx)
var response TracesResponse
@ -120,8 +212,13 @@ func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int
return trace, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err))
}
// Add time parameters if provided and traceIdTimeEnabled is true
if j.traceIdTimeEnabled {
var jsonData SettingsJSONData
if err := json.Unmarshal(j.settings.JSONData, &jsonData); err != nil {
return trace, backend.DownstreamError(fmt.Errorf("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 > 0 || end > 0 {
parsedURL, err := url.Parse(traceUrl)
if err != nil {

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
@ -60,7 +61,10 @@ func TestJaegerClient_Services(t *testing.T) {
}))
defer server.Close()
client, err := New(server.URL, server.Client(), log.NewNullLogger(), false)
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
services, err := client.Services()
@ -149,7 +153,10 @@ func TestJaegerClient_Operations(t *testing.T) {
}))
defer server.Close()
client, err := New(server.URL, server.Client(), log.NewNullLogger(), false)
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
operations, err := client.Operations(tt.service)
@ -167,84 +174,173 @@ func TestJaegerClient_Operations(t *testing.T) {
}
}
func TestJaegerClient_Search(t *testing.T) {
tests := []struct {
name string
query *JaegerQuery
start int64
end int64
mockResponse string
mockStatusCode int
expectedURL string
expectError bool
expectedError error
}{
{
name: "Successful search with all parameters",
query: &JaegerQuery{
Service: "test-service",
Operation: "test-operation",
Tags: "error=true",
MinDuration: "1s",
MaxDuration: "5s",
Limit: 10,
},
start: 1735689600000000,
end: 1738368000000000,
mockResponse: `{"data":[{"traceID":"test-trace-id"}]}`,
mockStatusCode: http.StatusOK,
expectedURL: "/api/traces?end=1738368000000000&limit=10&maxDuration=5s&minDuration=1s&operation=test-operation&service=test-service&start=1735689600000000&tags=%7B%22error%22%3A%22true%22%7D",
expectError: false,
expectedError: nil,
},
{
name: "Successful search with minimal parameters",
query: &JaegerQuery{
Service: "test-service",
},
start: 1735689600000000,
end: 1738368000000000,
mockResponse: `{"data":[{"traceID":"test-trace-id"}]}`,
mockStatusCode: http.StatusOK,
expectedURL: "/api/traces?end=1738368000000000&service=test-service&start=1735689600000000",
expectError: false,
expectedError: nil,
},
{
name: "Server error",
query: &JaegerQuery{
Service: "test-service",
},
start: 1735689600000000,
end: 1738368000000000,
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
expectedURL: "/api/traces?end=1738368000000000&service=test-service&start=1735689600000000",
expectError: true,
expectedError: backend.DownstreamError(fmt.Errorf("request failed: %s", http.StatusText(http.StatusInternalServerError))),
},
}
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()
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
traces, err := client.Search(tt.query, 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, traces)
assert.Equal(t, tt.expectedURL, actualURL)
}
})
}
}
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 string
traceId string
jsonData string
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 enabled",
traceId: "abc123",
jsonData: `{"traceIdTimeParams": {"enabled": 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: "Successful response with time params disabled",
traceId: "abc123",
jsonData: `{"traceIdTimeParams": {"enabled": 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: "Non-200 response",
traceId: "abc123",
jsonData: `{"traceIdTimeParams": {"enabled": 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: "Invalid JSON response",
traceId: "abc123",
jsonData: `{"traceIdTimeParams": {"enabled": 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")),
name: "Empty trace ID",
traceId: "",
jsonData: `{"traceIdTimeParams": {"enabled": true}}`,
start: 1000,
end: 2000,
mockResponse: `{"data":[]}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedURL: "",
expectError: true,
expectedError: backend.DownstreamError(errors.New("traceID is empty")),
},
}
@ -258,7 +354,11 @@ func TestJaegerClient_Trace(t *testing.T) {
}))
defer server.Close()
client, err := New(server.URL, server.Client(), log.NewNullLogger(), tt.traceIdTimeEnabled)
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
JSONData: []byte(tt.jsonData),
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
trace, err := client.Trace(context.Background(), tt.traceId, tt.start, tt.end)
@ -367,7 +467,10 @@ func TestJaegerClient_Dependencies(t *testing.T) {
}))
defer server.Close()
client, err := New(server.URL, server.Client(), log.NewNullLogger(), false)
settings := backend.DataSourceInstanceSettings{
URL: server.URL,
}
client, err := New(server.Client(), log.NewNullLogger(), settings)
assert.NoError(t, err)
dependencies, err := client.Dependencies(context.Background(), tt.start, tt.end)

@ -59,7 +59,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
}
logger := logger.FromContext(ctx)
jaegerClient, err := New(settings.URL, httpClient, logger, jsonData.TraceIdTimeParams.Enabled)
jaegerClient, err := New(httpClient, logger, settings)
return &datasourceInfo{JaegerClient: jaegerClient}, err
}
}

@ -2,6 +2,7 @@ package jaeger
import (
"context"
"encoding/json"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@ -83,7 +84,13 @@ func TestDataSourceInstanceSettings_TraceIdTimeEnabled(t *testing.T) {
require.NotNil(t, dsInfo)
// Verify the client's traceIdTimeEnabled parameter
assert.Equal(t, tt.expectedEnabled, dsInfo.JaegerClient.traceIdTimeEnabled)
var jsonData SettingsJSONData
if err := json.Unmarshal(dsInfo.JaegerClient.settings.JSONData, &jsonData); err != nil {
t.Fatalf("failed to parse settings JSON data: %v", err)
}
assert.Equal(t, tt.expectedEnabled, jsonData.TraceIdTimeParams.Enabled)
})
}
}

@ -5,12 +5,13 @@ import (
"encoding/json"
"fmt"
"sort"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
type jaegerQuery struct {
type JaegerQuery struct {
QueryType string `json:"queryType"`
Service string `json:"service"`
Operation string `json:"operation"`
@ -23,9 +24,10 @@ type jaegerQuery struct {
func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
response := backend.NewQueryDataResponse()
logger := dsInfo.JaegerClient.logger.FromContext(ctx)
for _, q := range req.Queries {
var query jaegerQuery
var query JaegerQuery
err := json.Unmarshal(q.JSON, &query)
if err != nil {
@ -34,6 +36,29 @@ func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDa
continue
}
// Handle "Upload" query type
if query.QueryType == "upload" {
logger.Debug("upload query type is not supported in backend mode")
response.Responses[q.RefID] = backend.DataResponse{
Error: fmt.Errorf("unsupported query type %s. only available in frontend mode", query.QueryType),
ErrorSource: backend.ErrorSourceDownstream,
}
continue
}
// Handle "Search" query type
if query.QueryType == "search" {
traces, err := dsInfo.JaegerClient.Search(&query, q.TimeRange.From.UnixMicro(), q.TimeRange.To.UnixMicro())
if err != nil {
response.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(err)
continue
}
frames := transformSearchResponse(traces, dsInfo)
response.Responses[q.RefID] = backend.DataResponse{
Frames: data.Frames{frames},
}
}
// 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())
@ -68,7 +93,98 @@ func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDa
return response, nil
}
// transformTraceResponse converts Jaeger trace data to a Data frame
func transformSearchResponse(response []TraceResponse, dsInfo *datasourceInfo) *data.Frame {
// Create a frame for the traces
frame := data.NewFrame("traces",
data.NewField("traceID", nil, []string{}).SetConfig(&data.FieldConfig{
DisplayName: "Trace ID",
Links: []data.DataLink{
{
Title: "Trace: ${__value.raw}",
URL: "",
Internal: &data.InternalDataLink{
DatasourceUID: dsInfo.JaegerClient.settings.UID,
DatasourceName: dsInfo.JaegerClient.settings.Name,
Query: map[string]interface{}{
"query": "${__value.raw}",
},
},
},
},
}),
data.NewField("traceName", nil, []string{}).SetConfig(&data.FieldConfig{
DisplayName: "Trace name",
}),
data.NewField("startTime", nil, []time.Time{}).SetConfig(&data.FieldConfig{
DisplayName: "Start time",
}),
data.NewField("duration", nil, []int64{}).SetConfig(&data.FieldConfig{
DisplayName: "Duration",
Unit: "µs",
}),
)
// Set the visualization type to table
frame.Meta = &data.FrameMeta{
PreferredVisualization: "table",
}
// Sort traces by start time in descending order (newest first)
sort.Slice(response, func(i, j int) bool {
rootSpanI := response[i].Spans[0]
rootSpanJ := response[j].Spans[0]
for _, span := range response[i].Spans {
if span.StartTime < rootSpanI.StartTime {
rootSpanI = span
}
}
for _, span := range response[j].Spans {
if span.StartTime < rootSpanJ.StartTime {
rootSpanJ = span
}
}
return rootSpanI.StartTime > rootSpanJ.StartTime
})
// Process each trace
for _, trace := range response {
if len(trace.Spans) == 0 {
continue
}
// Get the root span
rootSpan := trace.Spans[0]
for _, span := range trace.Spans {
if span.StartTime < rootSpan.StartTime {
rootSpan = span
}
}
// Get the service name for the trace
serviceName := ""
if process, ok := trace.Processes[rootSpan.ProcessID]; ok {
serviceName = process.ServiceName
}
// Get the trace name and start time
traceName := fmt.Sprintf("%s: %s", serviceName, rootSpan.OperationName)
startTime := time.Unix(0, rootSpan.StartTime*1000)
// Append the row to the frame
frame.AppendRow(
trace.TraceID,
traceName,
startTime,
rootSpan.Duration,
)
}
return frame
}
func transformTraceResponse(trace TraceResponse, refID string) *data.Frame {
frame := data.NewFrame(refID,
data.NewField("traceID", nil, []string{}),
@ -179,61 +295,6 @@ func transformTraceResponse(trace TraceResponse, refID string) *data.Frame {
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"`
}
func transformDependenciesResponse(dependencies DependenciesResponse, refID string) []*data.Frame {
// Create nodes frame
nodesFrame := data.NewFrame(refID+"_nodes",
@ -300,3 +361,58 @@ func transformDependenciesResponse(dependencies DependenciesResponse, refID stri
return []*data.Frame{nodesFrame, edgesFrame}
}
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"`
}

@ -3,9 +3,111 @@ package jaeger
import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
)
func TestTransformSearchResponse(t *testing.T) {
t.Run("empty_response", func(t *testing.T) {
dsInfo := &datasourceInfo{
JaegerClient: JaegerClient{
settings: backend.DataSourceInstanceSettings{
UID: "test-uid",
Name: "test-name",
},
},
}
frame := transformSearchResponse([]TraceResponse{}, dsInfo)
experimental.CheckGoldenJSONFrame(t, "./testdata", "search_empty_response.golden", frame, false)
})
t.Run("single_trace", func(t *testing.T) {
dsInfo := &datasourceInfo{
JaegerClient: JaegerClient{
settings: backend.DataSourceInstanceSettings{
UID: "test-uid",
Name: "test-name",
},
},
}
response := []TraceResponse{
{
TraceID: "test-trace-id",
Spans: []Span{
{
TraceID: "test-trace-id",
ProcessID: "p1",
OperationName: "test-operation",
StartTime: 1605873894680409,
Duration: 1000,
},
},
Processes: map[string]TraceProcess{
"p1": {
ServiceName: "test-service",
},
},
},
}
frame := transformSearchResponse(response, dsInfo)
experimental.CheckGoldenJSONFrame(t, "./testdata", "search_single_response.golden", frame, false)
})
t.Run("multiple_traces", func(t *testing.T) {
dsInfo := &datasourceInfo{
JaegerClient: JaegerClient{
settings: backend.DataSourceInstanceSettings{
UID: "test-uid",
Name: "test-name",
},
},
}
response := []TraceResponse{
{
TraceID: "trace-1",
Spans: []Span{
{
TraceID: "trace-1",
ProcessID: "p1",
OperationName: "op1",
StartTime: 1605873894680409,
Duration: 1000,
},
},
Processes: map[string]TraceProcess{
"p1": {
ServiceName: "service-1",
},
},
},
{
TraceID: "trace-2",
Spans: []Span{
{
TraceID: "trace-2",
ProcessID: "p2",
OperationName: "op2",
StartTime: 1605873894680409,
Duration: 2000,
},
},
Processes: map[string]TraceProcess{
"p2": {
ServiceName: "service-2",
},
},
},
}
frame := transformSearchResponse(response, dsInfo)
experimental.CheckGoldenJSONFrame(t, "./testdata", "search_multiple_response.golden", frame, false)
})
}
func TestTransformTraceResponse(t *testing.T) {
t.Run("simple_trace", func(t *testing.T) {
trace := TraceResponse{

@ -0,0 +1,100 @@
// 🌟 This was machine generated. Do not edit. 🌟
//
// Frame[0] {
// "typeVersion": [
// 0,
// 0
// ],
// "preferredVisualisationType": "table"
// }
// Name: traces
// Dimensions: 4 Fields by 0 Rows
// +----------------+-----------------+-------------------+----------------+
// | Name: traceID | Name: traceName | Name: startTime | Name: duration |
// | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []time.Time | Type: []int64 |
// +----------------+-----------------+-------------------+----------------+
// +----------------+-----------------+-------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
{
"status": 200,
"frames": [
{
"schema": {
"name": "traces",
"meta": {
"typeVersion": [
0,
0
],
"preferredVisualisationType": "table"
},
"fields": [
{
"name": "traceID",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"displayName": "Trace ID",
"links": [
{
"title": "Trace: ${__value.raw}",
"internal": {
"query": {
"query": "${__value.raw}"
},
"datasourceUid": "test-uid",
"datasourceName": "test-name"
}
}
]
}
},
{
"name": "traceName",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"displayName": "Trace name"
}
},
{
"name": "startTime",
"type": "time",
"typeInfo": {
"frame": "time.Time"
},
"config": {
"displayName": "Start time"
}
},
{
"name": "duration",
"type": "number",
"typeInfo": {
"frame": "int64"
},
"config": {
"displayName": "Duration",
"unit": "µs"
}
}
]
},
"data": {
"values": [
[],
[],
[],
[]
]
}
}
]
}

@ -0,0 +1,123 @@
// 🌟 This was machine generated. Do not edit. 🌟
//
// Frame[0] {
// "typeVersion": [
// 0,
// 0
// ],
// "preferredVisualisationType": "table"
// }
// Name: traces
// Dimensions: 4 Fields by 2 Rows
// +----------------+-----------------+--------------------------------------+----------------+
// | Name: traceID | Name: traceName | Name: startTime | Name: duration |
// | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []time.Time | Type: []int64 |
// +----------------+-----------------+--------------------------------------+----------------+
// | trace-1 | service-1: op1 | 2020-11-20 12:04:54.680409 +0000 GMT | 1000 |
// | trace-2 | service-2: op2 | 2020-11-20 12:04:54.680409 +0000 GMT | 2000 |
// +----------------+-----------------+--------------------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
{
"status": 200,
"frames": [
{
"schema": {
"name": "traces",
"meta": {
"typeVersion": [
0,
0
],
"preferredVisualisationType": "table"
},
"fields": [
{
"name": "traceID",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"displayName": "Trace ID",
"links": [
{
"title": "Trace: ${__value.raw}",
"internal": {
"query": {
"query": "${__value.raw}"
},
"datasourceUid": "test-uid",
"datasourceName": "test-name"
}
}
]
}
},
{
"name": "traceName",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"displayName": "Trace name"
}
},
{
"name": "startTime",
"type": "time",
"typeInfo": {
"frame": "time.Time"
},
"config": {
"displayName": "Start time"
}
},
{
"name": "duration",
"type": "number",
"typeInfo": {
"frame": "int64"
},
"config": {
"displayName": "Duration",
"unit": "µs"
}
}
]
},
"data": {
"values": [
[
"trace-1",
"trace-2"
],
[
"service-1: op1",
"service-2: op2"
],
[
1605873894680,
1605873894680
],
[
1000,
2000
]
],
"nanos": [
null,
null,
[
409000,
409000
],
null
]
}
}
]
}

@ -0,0 +1,117 @@
// 🌟 This was machine generated. Do not edit. 🌟
//
// Frame[0] {
// "typeVersion": [
// 0,
// 0
// ],
// "preferredVisualisationType": "table"
// }
// Name: traces
// Dimensions: 4 Fields by 1 Rows
// +----------------+------------------------------+--------------------------------------+----------------+
// | Name: traceID | Name: traceName | Name: startTime | Name: duration |
// | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []time.Time | Type: []int64 |
// +----------------+------------------------------+--------------------------------------+----------------+
// | test-trace-id | test-service: test-operation | 2020-11-20 12:04:54.680409 +0000 GMT | 1000 |
// +----------------+------------------------------+--------------------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
{
"status": 200,
"frames": [
{
"schema": {
"name": "traces",
"meta": {
"typeVersion": [
0,
0
],
"preferredVisualisationType": "table"
},
"fields": [
{
"name": "traceID",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"displayName": "Trace ID",
"links": [
{
"title": "Trace: ${__value.raw}",
"internal": {
"query": {
"query": "${__value.raw}"
},
"datasourceUid": "test-uid",
"datasourceName": "test-name"
}
}
]
}
},
{
"name": "traceName",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"displayName": "Trace name"
}
},
{
"name": "startTime",
"type": "time",
"typeInfo": {
"frame": "time.Time"
},
"config": {
"displayName": "Start time"
}
},
{
"name": "duration",
"type": "number",
"typeInfo": {
"frame": "int64"
},
"config": {
"displayName": "Duration",
"unit": "µs"
}
}
]
},
"data": {
"values": [
[
"test-trace-id"
],
[
"test-service: test-operation"
],
[
1605873894680
],
[
1000
]
],
"nanos": [
null,
null,
[
409000
],
null
]
}
}
]
}

@ -69,6 +69,9 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
return !!query.service;
}
/**
* Migrated to backend with feature toggle `jaegerBackendMigration`
*/
query(options: DataQueryRequest<JaegerQuery>): Observable<DataQueryResponse> {
// 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.
@ -77,11 +80,7 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
return of({ data: [emptyTraceDataFrame] });
}
if (
config.featureToggles.jaegerBackendMigration &&
// No query type means that the query is a trace ID query
(!target.queryType || target.queryType === 'dependencyGraph')
) {
if (config.featureToggles.jaegerBackendMigration && target.queryType !== 'upload') {
return super.query({ ...options, targets: [target] }).pipe(
map((response) => {
// If the node graph is enabled and the query is a trace ID query, add the node graph frames to the response

Loading…
Cancel
Save