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?
This commit is contained in:
+108
-11
@@ -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 {
|
||||
|
||||
+178
-75
@@ -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,61 @@ func TestJaegerClient_Operations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestJaegerClient_Trace(t *testing.T) {
|
||||
func TestJaegerClient_Search(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
|
||||
query *JaegerQuery
|
||||
start int64
|
||||
end int64
|
||||
mockResponse string
|
||||
mockStatusCode int
|
||||
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 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 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 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: "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")),
|
||||
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))),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -258,7 +242,123 @@ 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,
|
||||
}
|
||||
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
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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: "",
|
||||
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")),
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+174
-58
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user