Jaeger: Migrate API calls to gRPC endpoint (#113297)

* Jaeger: Migrate Services and Operations to the gRPC Jaeger endpoint (#112384)

* add grpc feature toggle

* move types into types.go

* creates grpc client functions for services and operations

* Call grpc services function when feature flag is enabled for health check

* remove unnecessary double encoding

* check for successful status code before decoding response and return nil in case of successful response

* remove duplicate code

* use variable

* fix error type in testsz

* Jaeger: Migrate search and Trace Search calls to use gRPC endpoint (#112610)

* move all types into types package except for JagerClient

* move all helper functions into utils package

* change return type of search function to be frames and add grpc search functionality

* fix tests

* fix types and the way we check error response from grpc

* change trace name and duration unit conversion

* fix types and add tests

* support queryAttributes

* quick limit implementation in post processing

* add todo for attributes / tags

* make trace functionality ready to support grpc flow

* add functions to process search response for a specific trace and create the Trace frame

* tests for helper funtions

* remove grpc querying for now!

* change logic to be able to process and support multiple resource spans

* remove logic for gRPC from grpc_client.go

* add equivalent fields for logs and references

* add tests for grpcTraceResponse function

* fix types after merge with main

* fix status code checks and return nil for error on successful responses

* enable reading through config flag for trace search

* create sigle key value type since they are similar for OTLP and non OTLP based formats

* reference right type

* convert events and links into references and logs

* add status code, status message and kind to data frame

* fix tests to accomodate new format

* remove unused function and add more tests

* remove edit flag for jsonc golden test files

* add clarifying comment

* fix tests and linting

* fix golden files for testing

* fix typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add clarifying comment

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* remove unnecessary logging statement

* fix downstream errors

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* use downstreamerrorf where applicable and add missing downstream eror sources.

* tests

---------

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jocelyn Collado-Kuri
2025-10-31 11:19:16 -07:00
committed by GitHub
co-authored by Copilot ismail simsek
parent 64da716a2e
commit d0ea82633f
22 changed files with 3364 additions and 621 deletions
+213
View File
@@ -0,0 +1,213 @@
package utils
import (
"encoding/json"
"fmt"
"sort"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/jaeger/types"
)
func TransformSearchResponse(response []types.TraceResponse, dsUID string, dsName string) *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: dsUID,
DatasourceName: dsName,
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 types.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 := []types.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
}
+261
View File
@@ -0,0 +1,261 @@
package utils
import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/grafana/grafana/pkg/tsdb/jaeger/types"
)
func TestTransformSearchResponse(t *testing.T) {
t.Run("empty_response", func(t *testing.T) {
frame := TransformSearchResponse([]types.TraceResponse{}, "test-uid", "test-name")
experimental.CheckGoldenJSONFrame(t, "../testdata", "search_empty_response.golden", frame, false)
})
t.Run("single_trace", func(t *testing.T) {
response := []types.TraceResponse{
{
TraceID: "test-trace-id",
Spans: []types.Span{
{
TraceID: "test-trace-id",
ProcessID: "p1",
OperationName: "test-operation",
StartTime: 1605873894680409,
Duration: 1000,
},
},
Processes: map[string]types.TraceProcess{
"p1": {
ServiceName: "test-service",
},
},
},
}
frame := TransformSearchResponse(response, "test-uid", "test-name")
experimental.CheckGoldenJSONFrame(t, "../testdata", "search_single_response.golden", frame, false)
})
t.Run("multiple_traces", func(t *testing.T) {
response := []types.TraceResponse{
{
TraceID: "trace-1",
Spans: []types.Span{
{
TraceID: "trace-1",
ProcessID: "p1",
OperationName: "op1",
StartTime: 1605873894680409,
Duration: 1000,
},
},
Processes: map[string]types.TraceProcess{
"p1": {
ServiceName: "service-1",
},
},
},
{
TraceID: "trace-2",
Spans: []types.Span{
{
TraceID: "trace-2",
ProcessID: "p2",
OperationName: "op2",
StartTime: 1605873894680409,
Duration: 2000,
},
},
Processes: map[string]types.TraceProcess{
"p2": {
ServiceName: "service-2",
},
},
},
}
frame := TransformSearchResponse(response, "test-uid", "test-name")
experimental.CheckGoldenJSONFrame(t, "../testdata", "search_multiple_response.golden", frame, false)
})
}
func TestTransformTraceResponse(t *testing.T) {
t.Run("simple_trace", func(t *testing.T) {
trace := types.TraceResponse{
TraceID: "3fa414edcef6ad90",
Spans: []types.Span{
{
TraceID: "3fa414edcef6ad90",
SpanID: "3fa414edcef6ad90",
OperationName: "HTTP GET - api_traces_traceid",
StartTime: 1605873894680409,
Duration: 1049141,
Tags: []types.KeyValueType{
{Key: "sampler.type", Type: "string", Value: "probabilistic"},
{Key: "sampler.param", Type: "float64", Value: 1},
},
Logs: []types.TraceLog{},
ProcessID: "p1",
Warnings: nil,
Flags: 0,
},
{
TraceID: "3fa414edcef6ad90",
SpanID: "0f5c1808567e4403",
OperationName: "/tempopb.Querier/FindTraceByID",
References: []types.TraceSpanReference{
{
RefType: "CHILD_OF",
TraceID: "3fa414edcef6ad90",
SpanID: "3fa414edcef6ad90",
},
},
StartTime: 1605873894680587,
Duration: 1847,
Tags: []types.KeyValueType{
{Key: "component", Type: "string", Value: "gRPC"},
{Key: "span.kind", Type: "string", Value: "client"},
},
Logs: []types.TraceLog{},
ProcessID: "p1",
Warnings: nil,
Flags: 0,
},
},
Processes: map[string]types.TraceProcess{
"p1": {
ServiceName: "tempo-querier",
Tags: []types.KeyValueType{
{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 := types.TraceResponse{
TraceID: "3fa414edcef6ad90",
Spans: []types.Span{
{
TraceID: "3fa414edcef6ad90",
SpanID: "3fa414edcef6ad90",
OperationName: "HTTP GET - api_traces_traceid",
References: []types.TraceSpanReference{},
StartTime: 1605873894680409,
Duration: 1049141,
Tags: []types.KeyValueType{
{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: []types.TraceLog{
{
Timestamp: 1605873894681000,
Fields: []types.KeyValueType{
{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: []types.TraceSpanReference{
{
RefType: "CHILD_OF",
TraceID: "3fa414edcef6ad90",
SpanID: "3fa414edcef6ad90",
},
},
StartTime: 1605873894680587,
Duration: 1847,
Tags: []types.KeyValueType{
{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: []types.TraceLog{
{
Timestamp: 1605873894680700,
Fields: []types.KeyValueType{
{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: []types.TraceSpanReference{
{
RefType: "CHILD_OF",
TraceID: "3fa414edcef6ad90",
SpanID: "0f5c1808567e4403",
},
},
StartTime: 1605873894680800,
Duration: 500,
Tags: []types.KeyValueType{
{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: []types.TraceLog{
{
Timestamp: 1605873894680850,
Fields: []types.KeyValueType{
{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]types.TraceProcess{
"p1": {
ServiceName: "tempo-querier",
Tags: []types.KeyValueType{
{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: []types.KeyValueType{
{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)
})
}
+382
View File
@@ -0,0 +1,382 @@
package utils
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/jaeger/types"
)
func TransformGrpcSearchResponse(response types.GrpcTracesResult, dsUID string, dsName string, limit int) *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: dsUID,
DatasourceName: dsName,
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)
resourceSpans := response.ResourceSpans
sort.Slice(resourceSpans, func(i, j int) bool {
rootSpanI := resourceSpans[i].ScopeSpans[0].Spans[0]
rootSpanJ := resourceSpans[j].ScopeSpans[0].Spans[0]
for _, scopeSpan := range resourceSpans[i].ScopeSpans {
for _, span := range scopeSpan.Spans {
if span.StartTimeUnixNano < rootSpanI.StartTimeUnixNano {
rootSpanI = span
}
}
}
for _, scopeSpan := range resourceSpans[j].ScopeSpans {
for _, span := range scopeSpan.Spans {
if span.StartTimeUnixNano < rootSpanJ.StartTimeUnixNano {
rootSpanJ = span
}
}
}
return rootSpanI.StartTimeUnixNano > rootSpanJ.StartTimeUnixNano
})
if limit > 0 {
resourceSpans = resourceSpans[:limit]
}
// process each individual resource
for _, res := range resourceSpans {
serviceName := getAttribute(res.Resource.Attributes, "service.name")
for _, scopeSpan := range res.ScopeSpans {
if len(scopeSpan.Spans) == 0 {
continue
}
// Get the root span
rootSpan := scopeSpan.Spans[0]
for _, span := range scopeSpan.Spans {
if span.StartTimeUnixNano < rootSpan.StartTimeUnixNano {
rootSpan = span
}
}
// get trace name
traceName := fmt.Sprintf("%s: %s", serviceName.StringValue, rootSpan.Name)
startTimeInt, startErr := strconv.ParseInt(rootSpan.StartTimeUnixNano, 10, 64)
endTimeInt, endErr := strconv.ParseInt(rootSpan.EndTimeUnixNano, 10, 64)
duration := int64(0)
if startErr == nil && endErr == nil {
duration = (endTimeInt - startTimeInt) / 1000 // convert to microseconds
}
frame.AppendRow(
rootSpan.TraceID,
traceName,
time.Unix(0, startTimeInt),
duration,
)
}
}
return frame
}
func TransformGrpcTraceResponse(trace []types.GrpcResourceSpans, 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("statusCode", nil, []int64{}),
data.NewField("statusMessage", nil, []string{}),
data.NewField("kind", 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{}),
)
// Set metadata for trace visualization
frame.Meta = &data.FrameMeta{
PreferredVisualization: "trace",
Custom: map[string]interface{}{
"traceFormat": "jaeger",
},
}
// each resource is a difference service name or "process"
for _, resource := range trace {
for _, scopeSpan := range resource.ScopeSpans {
for _, span := range scopeSpan.Spans {
parentSpanID := span.ParentSpanID
// Get service name and tags
serviceName := getAttribute(resource.Resource.Attributes, "service.name").StringValue
serviceTags := json.RawMessage{}
processedResAttributes := processAttributes(resource.Resource.Attributes)
tagsMarshaled, err := json.Marshal(processedResAttributes)
if err == nil {
serviceTags = json.RawMessage(tagsMarshaled)
}
// Convert tags
tags := json.RawMessage{}
processedSpanAttributes := processAttributes(span.Attributes)
// add otel attributes scope name, scope version and span kind
if scopeSpan.Scope.Name != "" {
processedSpanAttributes = append(processedSpanAttributes, types.KeyValueType{
Key: "otel.scope.name",
Value: scopeSpan.Scope.Name,
Type: "string",
})
}
if scopeSpan.Scope.Version != "" {
processedSpanAttributes = append(processedSpanAttributes, types.KeyValueType{
Key: "otel.scope.version",
Value: scopeSpan.Scope.Version,
Type: "string",
})
}
tagsMarshaled, err = json.Marshal(processedSpanAttributes)
if err == nil {
tags = json.RawMessage(tagsMarshaled)
}
// Convert logs
// In the new API (OTLP based), logs are span events. See:
// https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L630C9-L636C11
logs := json.RawMessage{}
processedEvents := convertGrpcEventsToLogs(span.Events)
logsMarshaled, err := json.Marshal(processedEvents)
if err == nil {
logs = json.RawMessage(logsMarshaled)
}
// Convert references (excluding parent)
references := json.RawMessage{}
filteredLinks := []types.GrpcSpanLink{}
// in the new API (OTLP based), references are defined as "SpanLinks" see:
// https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L642C8-L648C11
for _, ref := range span.Links {
if parentSpanID == "" || ref.SpanID != parentSpanID {
filteredLinks = append(filteredLinks, ref)
}
}
processedLinks := convertGrpcLinkToReference(filteredLinks)
refsMarshaled, err := json.Marshal(processedLinks)
if err == nil {
references = json.RawMessage(refsMarshaled)
}
// convert start time and calculate duration
startTimeFloat, startErr := strconv.ParseFloat(span.StartTimeUnixNano, 64)
endTimeFloat, endErr := strconv.ParseFloat(span.EndTimeUnixNano, 64)
duration := float64(0)
if startErr == nil && endErr == nil {
duration = (endTimeFloat - startTimeFloat) / 1000000 // convert to milliseconds
}
// Add span to frame
frame.AppendRow(
span.TraceID,
span.SpanID,
parentSpanID,
span.Status.Code,
span.Status.Message,
processSpanKind(span.Kind),
span.Name,
serviceName,
serviceTags,
startTimeFloat/1000000, // Convert nanoseconds to milliseconds
duration,
logs,
references,
tags,
)
}
}
}
return frame
}
func processAttributes(attributes []types.GrpcKeyValue) []types.KeyValueType {
tags := []types.KeyValueType{}
for _, att := range attributes {
if att.Value.StringValue != "" {
tags = append(tags, types.KeyValueType{
Key: att.Key,
Value: att.Value.StringValue,
Type: "string",
})
continue
}
if att.Value.BoolValue != "" {
boolVal, err := strconv.ParseBool(att.Value.BoolValue)
if err != nil {
continue
}
tags = append(tags, types.KeyValueType{
Key: att.Key,
Value: boolVal,
Type: "boolean",
})
continue
}
if att.Value.IntValue != "" {
intVal, err := strconv.Atoi(att.Value.IntValue)
if err != nil {
continue
}
tags = append(tags, types.KeyValueType{
Key: att.Key,
Value: int64(intVal),
Type: "int64",
})
continue
}
if att.Value.DoubleValue != "" {
floatVal, err := strconv.ParseFloat(att.Value.DoubleValue, 64)
if err != nil {
continue
}
tags = append(tags, types.KeyValueType{
Key: att.Key,
Value: floatVal,
Type: "float64",
})
continue
}
if len(att.Value.ArrayValue.Values) > 0 {
tags = append(tags, types.KeyValueType{
Key: att.Key,
Value: att.Value.ArrayValue.Values,
})
continue
}
if len(att.Value.KvListValue.Values) > 0 {
tags = append(tags, types.KeyValueType{
Key: att.Key,
Value: att.Value.KvListValue.Values,
})
continue
}
if att.Value.BytesValue != "" {
tags = append(tags, types.KeyValueType{
Key: att.Key,
Value: att.Value.BytesValue,
Type: "bytes",
})
continue
}
}
return tags
}
func getAttribute(attributes []types.GrpcKeyValue, attName string) types.GrpcAnyValue {
var attValue types.GrpcAnyValue
for _, att := range attributes {
if att.Key == attName {
return att.Value
}
}
return attValue
}
func processSpanKind(kind int64) string {
switch kind {
case 0:
return "unspecified"
case 1:
return "internal"
case 2:
return "server"
case 3:
return "client"
case 4:
return "producer"
case 5:
return "consumer"
default:
return "unspecified"
}
}
// This is to help ensure backwards compatibility with the current non OTLP based Jager trace format
// a few fields are different between TraceLogs and GrpcSpanEvents
func convertGrpcEventsToLogs(events []types.GrpcSpanEvent) []types.TraceLog {
logs := []types.TraceLog{}
for _, event := range events {
timestamp, err := strconv.Atoi(event.TimeUnixNano)
if err == nil {
timestamp = timestamp / 1000 // converting from nanoseconds to milliseconds
}
log := types.TraceLog{
Name: event.Name,
Timestamp: int64(timestamp),
Fields: processAttributes(event.Attributes),
}
logs = append(logs, log)
}
return logs
}
// this is to help ensure backwards compatibility between references and links with the current non OTLP based Jaeger trace format
// There is no concept of RefType in the new OTLP based SpanLink, so we are only converting the SpanID and TraceID
func convertGrpcLinkToReference(links []types.GrpcSpanLink) []types.TraceSpanReference {
references := []types.TraceSpanReference{}
for _, ref := range links {
references = append(references, types.TraceSpanReference{
TraceID: ref.TraceID,
SpanID: ref.SpanID,
})
}
return references
}
+826
View File
@@ -0,0 +1,826 @@
package utils
import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/grafana/grafana/pkg/tsdb/jaeger/types"
"github.com/stretchr/testify/assert"
)
func TestTransformGrpcSearchResponse(t *testing.T) {
t.Run("empty_response", func(t *testing.T) {
frame := TransformGrpcSearchResponse(types.GrpcTracesResult{}, "test-uid", "test-name", 0)
experimental.CheckGoldenJSONFrame(t, "../testdata", "search_empty_response.golden", frame, false)
})
t.Run("single_trace", func(t *testing.T) {
response := types.GrpcTracesResult{
ResourceSpans: []types.GrpcResourceSpans{
{
Resource: types.GrpcResource{
Attributes: []types.GrpcKeyValue{
{
Key: "service.name",
Value: types.GrpcAnyValue{
StringValue: "test-service",
},
},
},
},
ScopeSpans: []types.GrpcScopeSpans{
{
Spans: []types.GrpcSpan{
{
TraceID: "test-trace-id",
Name: "test-operation",
StartTimeUnixNano: "1605873894680409000",
EndTimeUnixNano: "1605873894681409000",
},
},
},
},
SchemaURL: "someschemaurl.com",
},
},
}
frame := TransformGrpcSearchResponse(response, "test-uid", "test-name", 0)
experimental.CheckGoldenJSONFrame(t, "../testdata", "search_single_response.golden", frame, false)
})
t.Run("multiple_traces", func(t *testing.T) {
response := types.GrpcTracesResult{
ResourceSpans: []types.GrpcResourceSpans{
{
Resource: types.GrpcResource{
Attributes: []types.GrpcKeyValue{
{
Key: "service.name",
Value: types.GrpcAnyValue{
StringValue: "service-1",
},
},
},
},
ScopeSpans: []types.GrpcScopeSpans{
{
Spans: []types.GrpcSpan{
{
TraceID: "trace-1",
Name: "op1",
StartTimeUnixNano: "1605873894680409000",
EndTimeUnixNano: "1605873894681409000",
},
},
},
},
SchemaURL: "someschemaurl.com",
},
{
Resource: types.GrpcResource{
Attributes: []types.GrpcKeyValue{
{
Key: "service.name",
Value: types.GrpcAnyValue{
StringValue: "service-2",
},
},
},
},
ScopeSpans: []types.GrpcScopeSpans{
{
Spans: []types.GrpcSpan{
{
TraceID: "trace-2",
Name: "op2",
StartTimeUnixNano: "1605873894680409000",
EndTimeUnixNano: "1605873894682409000",
},
},
},
},
SchemaURL: "someschemaurl.com",
},
},
}
frame := TransformGrpcSearchResponse(response, "test-uid", "test-name", 0)
experimental.CheckGoldenJSONFrame(t, "../testdata", "search_multiple_response.golden", frame, false)
})
}
func TestGetAttributes(t *testing.T) {
testAttributes := []types.GrpcKeyValue{
{
Key: "some-key1",
Value: types.GrpcAnyValue{
StringValue: "some-stringValue1",
},
},
{
Key: "some-key2",
Value: types.GrpcAnyValue{
BoolValue: "true",
},
},
{
Key: "some-key3",
Value: types.GrpcAnyValue{
IntValue: "0",
},
},
{
Key: "some-key4",
Value: types.GrpcAnyValue{
DoubleValue: "0",
},
},
{
Key: "some-key5",
Value: types.GrpcAnyValue{
ArrayValue: types.GrpcArrayValue{
Values: []types.GrpcAnyValue{},
},
},
},
{
Key: "some-key6",
Value: types.GrpcAnyValue{
KvListValue: types.KeyValueList{
Values: []types.GrpcKeyValue{},
},
},
},
{
Key: "some-key7",
Value: types.GrpcAnyValue{
BytesValue: "somebytesvalue",
},
},
}
t.Run("handles StringValue", func(t *testing.T) {
actual := getAttribute(testAttributes, "some-key1")
assert.Equal(t, types.GrpcAnyValue{
StringValue: "some-stringValue1",
}, actual)
})
t.Run("handles BoolValue", func(t *testing.T) {
actual := getAttribute(testAttributes, "some-key2")
assert.Equal(t, types.GrpcAnyValue{
BoolValue: "true",
}, actual)
})
t.Run("handles IntValue", func(t *testing.T) {
actual := getAttribute(testAttributes, "some-key3")
assert.Equal(t, types.GrpcAnyValue{
IntValue: "0",
}, actual)
})
t.Run("handles DoubleValue", func(t *testing.T) {
actual := getAttribute(testAttributes, "some-key4")
assert.Equal(t, types.GrpcAnyValue{
DoubleValue: "0",
}, actual)
})
t.Run("handles ArrayValue", func(t *testing.T) {
actual := getAttribute(testAttributes, "some-key5")
assert.Equal(t, types.GrpcAnyValue{
ArrayValue: types.GrpcArrayValue{
Values: []types.GrpcAnyValue{},
},
}, actual)
})
t.Run("handles KvListValue", func(t *testing.T) {
actual := getAttribute(testAttributes, "some-key6")
assert.Equal(t, types.GrpcAnyValue{
KvListValue: types.KeyValueList{
Values: []types.GrpcKeyValue{},
},
}, actual)
})
t.Run("handles BytesValue", func(t *testing.T) {
actual := getAttribute(testAttributes, "some-key7")
assert.Equal(t, types.GrpcAnyValue{
BytesValue: "somebytesvalue",
}, actual)
})
t.Run("handles non-existent value", func(t *testing.T) {
actual := getAttribute(testAttributes, "some-key8")
assert.Equal(t, types.GrpcAnyValue{}, actual)
})
}
func TestTransformGrpcTraceResponse(t *testing.T) {
t.Run("simple_trace", func(t *testing.T) {
trace := []types.GrpcResourceSpans{
{
Resource: types.GrpcResource{
Attributes: []types.GrpcKeyValue{
{
Key: "service.name",
Value: types.GrpcAnyValue{
StringValue: "tempo-querier",
},
},
{
Key: "cluster",
Value: types.GrpcAnyValue{
StringValue: "ops-tools1",
},
},
{
Key: "container",
Value: types.GrpcAnyValue{
StringValue: "tempo-query",
},
},
},
},
ScopeSpans: []types.GrpcScopeSpans{
{
Scope: types.GrpcInstrumentationScope{
Name: "some_scope1",
Version: "0.0.39",
},
Spans: []types.GrpcSpan{
{
TraceID: "3fa414edcef6ad90",
SpanID: "3fa414edcef6ad90",
ParentSpanID: "",
Name: "HTTP GET - api_traces_traceid",
Attributes: []types.GrpcKeyValue{
{
Key: "sampler.type",
Value: types.GrpcAnyValue{
StringValue: "probabilistic",
},
},
{
Key: "sampler.param",
Value: types.GrpcAnyValue{
DoubleValue: "100.00",
},
},
},
StartTimeUnixNano: "1605873894680409000",
EndTimeUnixNano: "1605873895729550000",
},
{
TraceID: "3fa414edcef6ad90",
SpanID: "0f5c1808567e4403",
ParentSpanID: "3fa414edcef6ad90",
Name: "HTTP GET - api_traces_traceid",
Attributes: []types.GrpcKeyValue{
{
Key: "component",
Value: types.GrpcAnyValue{
StringValue: "gRPC",
},
},
{
Key: "span.kind",
Value: types.GrpcAnyValue{
DoubleValue: "client",
},
},
},
StartTimeUnixNano: "1605873894680587000",
EndTimeUnixNano: "1605873894682434000",
},
},
},
},
},
}
frame := TransformGrpcTraceResponse(trace, "test")
experimental.CheckGoldenJSONFrame(t, "../testdata", "simple_trace_grpc.golden", frame, false)
})
t.Run("complex_trace", func(t *testing.T) {
trace := []types.GrpcResourceSpans{
{
Resource: types.GrpcResource{
Attributes: []types.GrpcKeyValue{
{
Key: "service.name",
Value: types.GrpcAnyValue{
StringValue: "tempo-querier",
},
},
{
Key: "cluster",
Value: types.GrpcAnyValue{
StringValue: "ops-tools1",
},
},
{
Key: "container",
Value: types.GrpcAnyValue{
StringValue: "tempo-storage",
},
},
{
Key: "version",
Value: types.GrpcAnyValue{
StringValue: "2.0.1",
},
},
},
},
ScopeSpans: []types.GrpcScopeSpans{
{
Spans: []types.GrpcSpan{
{
TraceID: "3fa414edcef6ad90",
SpanID: "3fa414edcef6ad90",
Name: "HTTP GET - api_traces_traceid",
Links: []types.GrpcSpanLink{},
StartTimeUnixNano: "1605873894680409000",
EndTimeUnixNano: "1605873895729550000",
Attributes: []types.GrpcKeyValue{
{
Key: "sampler.type",
Value: types.GrpcAnyValue{
StringValue: "probabilistic",
},
},
{
Key: "sampler.param",
Value: types.GrpcAnyValue{
DoubleValue: "1",
},
},
{
Key: "error",
Value: types.GrpcAnyValue{
BoolValue: "true",
},
},
{
Key: "http.status_code",
Value: types.GrpcAnyValue{
IntValue: "500",
},
},
},
Events: []types.GrpcSpanEvent{
{
TimeUnixNano: "1605873894681000000",
Attributes: []types.GrpcKeyValue{
{
Key: "event",
Value: types.GrpcAnyValue{
StringValue: "error",
},
},
{
Key: "message",
Value: types.GrpcAnyValue{
StringValue: "Internal server error",
},
},
},
},
},
},
{
TraceID: "3fa414edcef6ad90",
SpanID: "0f5c1808567e4403",
Name: "/tempopb.Querier/FindTraceByID",
Links: []types.GrpcSpanLink{
{
TraceID: "3fa414edcef6ad90",
SpanID: "3fa414edcef6ad90",
},
},
StartTimeUnixNano: "1605873894680587000",
EndTimeUnixNano: "1605873894682434000",
Attributes: []types.GrpcKeyValue{
{
Key: "component",
Value: types.GrpcAnyValue{
StringValue: "gRPC",
},
},
{
Key: "span.kind",
Value: types.GrpcAnyValue{
StringValue: "client",
},
},
{
Key: "error",
Value: types.GrpcAnyValue{
BoolValue: "true",
},
},
{
Key: "grpc.status_code",
Value: types.GrpcAnyValue{
IntValue: "13",
},
},
},
Events: []types.GrpcSpanEvent{
{
TimeUnixNano: "1605873894680700000",
Attributes: []types.GrpcKeyValue{
{
Key: "event",
Value: types.GrpcAnyValue{
StringValue: "error",
},
},
{
Key: "message",
Value: types.GrpcAnyValue{
StringValue: "gRPC error: INTERNAL",
},
},
},
},
},
},
},
},
},
},
{
Resource: types.GrpcResource{
Attributes: []types.GrpcKeyValue{
{
Key: "service.name",
Value: types.GrpcAnyValue{
StringValue: "tempo-storage",
},
},
{
Key: "cluster",
Value: types.GrpcAnyValue{
StringValue: "ops-tools1",
},
},
{
Key: "container",
Value: types.GrpcAnyValue{
StringValue: "tempo-storage",
},
},
{
Key: "version",
Value: types.GrpcAnyValue{
StringValue: "2.0.1",
},
},
},
},
ScopeSpans: []types.GrpcScopeSpans{
{
Spans: []types.GrpcSpan{
{
TraceID: "3fa414edcef6ad90",
SpanID: "1a2b3c4d5e6f7g8h",
Name: "db.query",
Links: []types.GrpcSpanLink{
{
TraceID: "3fa414edcef6ad90",
SpanID: "0f5c1808567e4403",
},
},
StartTimeUnixNano: "1605873894680800000",
EndTimeUnixNano: "1605873894681300000",
Attributes: []types.GrpcKeyValue{
{
Key: "db.type",
Value: types.GrpcAnyValue{
StringValue: "postgresql",
},
},
{
Key: "db.statement",
Value: types.GrpcAnyValue{
StringValue: "SELECT * FROM traces WHERE id = $1",
},
},
{
Key: "error",
Value: types.GrpcAnyValue{
BoolValue: "true",
},
},
},
Events: []types.GrpcSpanEvent{
{
TimeUnixNano: "1605873894681000000",
Attributes: []types.GrpcKeyValue{
{
Key: "event",
Value: types.GrpcAnyValue{
StringValue: "error",
},
},
{
Key: "message",
Value: types.GrpcAnyValue{
StringValue: "Database connection timeout",
},
},
},
},
},
},
},
},
},
},
}
frame := TransformGrpcTraceResponse(trace, "test")
experimental.CheckGoldenJSONFrame(t, "../testdata", "complex_trace_grpc.golden", frame, false)
})
}
func TestProcessSpanKind(t *testing.T) {
t.Run("converts unspecified span kind", func(t *testing.T) {
actual := processSpanKind(0)
assert.Equal(t, "unspecified", actual)
})
t.Run("converts internal span kind", func(t *testing.T) {
actual := processSpanKind(1)
assert.Equal(t, "internal", actual)
})
t.Run("converts server span kind", func(t *testing.T) {
actual := processSpanKind(2)
assert.Equal(t, "server", actual)
})
t.Run("converts client span kind", func(t *testing.T) {
actual := processSpanKind(3)
assert.Equal(t, "client", actual)
})
t.Run("converts producer span kind", func(t *testing.T) {
actual := processSpanKind(4)
assert.Equal(t, "producer", actual)
})
t.Run("converts consumer span kind", func(t *testing.T) {
actual := processSpanKind(5)
assert.Equal(t, "consumer", actual)
})
t.Run("converts unsupported span kind", func(t *testing.T) {
actual := processSpanKind(10)
assert.Equal(t, "unspecified", actual)
})
}
func TestProcessAttributes(t *testing.T) {
t.Run("processes empty attributes", func(t *testing.T) {
actual := processAttributes([]types.GrpcKeyValue{})
assert.Equal(t, []types.KeyValueType{}, actual)
})
t.Run("processes string attribute types", func(t *testing.T) {
attributes := []types.GrpcKeyValue{
{
Key: "key1",
Value: types.GrpcAnyValue{
StringValue: "value1",
},
},
}
expected := []types.KeyValueType{
{
Key: "key1",
Value: "value1",
Type: "string",
},
}
actual := processAttributes(attributes)
assert.Equal(t, expected, actual)
})
t.Run("processes bool attribute types", func(t *testing.T) {
attributes := []types.GrpcKeyValue{
{
Key: "key1",
Value: types.GrpcAnyValue{
BoolValue: "true",
},
},
}
expected := []types.KeyValueType{
{
Key: "key1",
Value: true,
Type: "boolean",
},
}
actual := processAttributes(attributes)
assert.Equal(t, expected, actual)
})
t.Run("processes int attribute types", func(t *testing.T) {
attributes := []types.GrpcKeyValue{
{
Key: "key1",
Value: types.GrpcAnyValue{
IntValue: "10",
},
},
}
expected := []types.KeyValueType{
{
Key: "key1",
Value: int64(10),
Type: "int64",
},
}
actual := processAttributes(attributes)
assert.Equal(t, expected, actual)
})
t.Run("processes double attribute types", func(t *testing.T) {
attributes := []types.GrpcKeyValue{
{
Key: "key1",
Value: types.GrpcAnyValue{
DoubleValue: "100.50",
},
},
}
expected := []types.KeyValueType{
{
Key: "key1",
Value: float64(100.50),
Type: "float64",
},
}
actual := processAttributes(attributes)
assert.Equal(t, expected, actual)
})
t.Run("processes arrayvalue attribute types", func(t *testing.T) {
attributes := []types.GrpcKeyValue{
{
Key: "key1",
Value: types.GrpcAnyValue{
ArrayValue: types.GrpcArrayValue{
Values: []types.GrpcAnyValue{
{
StringValue: "value1",
},
},
},
},
},
}
expected := []types.KeyValueType{
{
Key: "key1",
Value: []types.GrpcAnyValue{
{
StringValue: "value1",
},
},
},
}
actual := processAttributes(attributes)
assert.Equal(t, expected, actual)
})
t.Run("processes kvlistvalue attribute types", func(t *testing.T) {
attributes := []types.GrpcKeyValue{
{
Key: "key1",
Value: types.GrpcAnyValue{
KvListValue: types.KeyValueList{
Values: []types.GrpcKeyValue{
{
Key: "key2",
Value: types.GrpcAnyValue{
StringValue: "value2",
},
},
},
},
},
},
}
expected := []types.KeyValueType{
{
Key: "key1",
Value: []types.GrpcKeyValue{
{
Key: "key2",
Value: types.GrpcAnyValue{
StringValue: "value2",
},
},
},
},
}
actual := processAttributes(attributes)
assert.Equal(t, expected, actual)
})
t.Run("processes bytes attribute types", func(t *testing.T) {
attributes := []types.GrpcKeyValue{
{
Key: "key1",
Value: types.GrpcAnyValue{
BytesValue: "bytesvalue1",
},
},
}
expected := []types.KeyValueType{
{
Key: "key1",
Value: "bytesvalue1",
Type: "bytes",
},
}
actual := processAttributes(attributes)
assert.Equal(t, expected, actual)
})
}
func TestConvertGrpcEventsToLogs(t *testing.T) {
t.Run("converts events with timestamp and attributes", func(t *testing.T) {
events := []types.GrpcSpanEvent{
{
TimeUnixNano: "2000",
Name: "error",
Attributes: []types.GrpcKeyValue{
{
Key: "event",
Value: types.GrpcAnyValue{
StringValue: "error",
},
},
},
},
}
logs := convertGrpcEventsToLogs(events)
expected := []types.TraceLog{
{
Name: "error",
Timestamp: int64(2),
Fields: []types.KeyValueType{
{
Key: "event",
Value: "error",
Type: "string",
},
},
},
}
assert.Equal(t, expected, logs)
})
t.Run("returns zero timestamp when parsing fails", func(t *testing.T) {
events := []types.GrpcSpanEvent{
{
TimeUnixNano: "invalid",
Name: "log-without-timestamp",
},
}
logs := convertGrpcEventsToLogs(events)
assert.Len(t, logs, 1)
assert.Equal(t, int64(0), logs[0].Timestamp)
assert.Equal(t, "log-without-timestamp", logs[0].Name)
assert.Empty(t, logs[0].Fields)
})
}
func TestConvertGrpcLinkToReference(t *testing.T) {
t.Run("converts links to references", func(t *testing.T) {
links := []types.GrpcSpanLink{
{
TraceID: "trace-id",
SpanID: "span-id",
},
}
references := convertGrpcLinkToReference(links)
expected := []types.TraceSpanReference{
{
TraceID: "trace-id",
SpanID: "span-id",
},
}
assert.Equal(t, expected, references)
})
t.Run("returns empty slice for no links", func(t *testing.T) {
references := convertGrpcLinkToReference(nil)
assert.Empty(t, references)
})
}