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 }