Zipkin: Move query running to backend (#96404)

* Rename method applyTemplateVariables so it used by backend

* Zipkin: Move query running to backend

* Fix error source

* Hndle invalid query and return error response
This commit is contained in:
Ivana Huckova
2024-11-18 10:59:29 +01:00
committed by GitHub
parent b73ab15878
commit 8a551bacfb
7 changed files with 631 additions and 9 deletions
+4 -3
View File
@@ -127,6 +127,10 @@ func (z *ZipkinClient) Trace(traceId string) ([]model.SpanModel, error) {
}
res, err := z.httpClient.Get(traceUrl)
if err != nil {
return trace, err
}
defer func() {
if res != nil {
if err = res.Body.Close(); err != nil {
@@ -134,9 +138,6 @@ func (z *ZipkinClient) Trace(traceId string) ([]model.SpanModel, error) {
}
}
}()
if err != nil {
return trace, err
}
if err := json.NewDecoder(res.Body).Decode(&trace); err != nil {
return trace, err
}
+22 -2
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
@@ -250,7 +251,8 @@ func TestZipkinClient_Trace(t *testing.T) {
Tags: map[string]string{"key1": "value1"},
},
},
expectError: false,
expectError: false,
expectedError: "",
},
{
name: "Invalid traceID",
@@ -261,13 +263,31 @@ func TestZipkinClient_Trace(t *testing.T) {
expectError: true,
expectedError: "invalid/empty traceId",
},
{
name: "Special characters traceID",
traceID: "a/b",
mockResponse: `[{"traceId":"00000000000004d2","id":"0000000000000001","name":"operation1","tags":{"key1":"value1"}}]`,
mockStatusCode: http.StatusOK,
expectedResult: []model.SpanModel{
{
SpanContext: model.SpanContext{
TraceID: model.TraceID{Low: 1234},
ID: model.ID(1),
},
Name: "operation1",
Tags: map[string]string{"key1": "value1"},
},
},
expectError: false,
expectedError: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var client ZipkinClient
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v2/trace/"+tt.traceID, r.URL.Path)
assert.Contains(t, r.URL.String(), "/api/v2/trace/"+url.QueryEscape(tt.traceID))
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
+260
View File
@@ -0,0 +1,260 @@
package zipkin
import (
"context"
"encoding/json"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/openzipkin/zipkin-go/model"
)
func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
response := backend.NewQueryDataResponse()
logger := dsInfo.ZipkinClient.logger.FromContext(ctx)
for _, q := range req.Queries {
query, err := loadQuery(q)
if err != nil {
es := backend.ErrorSourcePlugin
if backend.IsDownstreamError(err) {
es = backend.ErrorSourceDownstream
}
response.Responses[q.RefID] = backend.DataResponse{
Error: err,
ErrorSource: es,
}
continue
}
switch query.QueryType {
case zipkinQueryTypeUpload:
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.ErrorSourcePlugin,
}
default:
traces, err := dsInfo.ZipkinClient.Trace(query.Query)
if err != nil {
es := backend.ErrorSourcePlugin
if backend.IsDownstreamHTTPError(err) {
es = backend.ErrorSourceDownstream
}
response.Responses[q.RefID] = backend.DataResponse{
Error: err,
ErrorSource: es,
}
continue
}
frame := transformResponse(traces, q.RefID)
response.Responses[q.RefID] = backend.DataResponse{
Frames: []*data.Frame{frame},
}
}
}
return response, nil
}
type zipkinQueryType string
const (
zipkinQueryTypeTraceId zipkinQueryType = "traceID"
zipkinQueryTypeUpload zipkinQueryType = "upload"
)
type zipkinQuery struct {
Query string `json:"query,omitempty"`
QueryType zipkinQueryType `json:"queryType,omitempty"`
}
func loadQuery(backendQuery backend.DataQuery) (zipkinQuery, error) {
var query zipkinQuery
err := json.Unmarshal(backendQuery.JSON, &query)
if err != nil {
return query, backend.DownstreamError(fmt.Errorf("error while parsing the query json. %w", err))
}
return query, err
}
type TraceKeyValuePair struct {
Key string `json:"key"`
Value interface{} `json:"value"`
Type string `json:"type,omitempty"`
}
type TraceLog struct {
Timestamp int64
Fields []TraceKeyValuePair
}
func transformResponse(zipkinSpans []model.SpanModel, refId string) *data.Frame {
newFrame := 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("tags", nil, []json.RawMessage{}),
)
newFrame.Meta = &data.FrameMeta{
PreferredVisualization: "trace",
Custom: map[string]interface{}{
"traceFormat": "zipkin",
},
}
// go through each span and add to the frame
for _, span := range zipkinSpans {
var parentSpanIdString *string
if span.ParentID != nil {
s := span.ParentID.String()
parentSpanIdString = &s
}
var serviceTags json.RawMessage
serviceTagsMarshaled, err := json.Marshal(getServiceTags(span))
if err == nil {
serviceTags = json.RawMessage(serviceTagsMarshaled)
}
var logs json.RawMessage
logsMarshaled, err := json.Marshal(transformAnnotationsToTraceLogs(span.Annotations))
if err == nil {
logs = json.RawMessage(logsMarshaled)
}
var tags json.RawMessage
tagsMarshaled, err := json.Marshal(transformTags(span))
if err == nil {
tags = json.RawMessage(tagsMarshaled)
}
newFrame.AppendRow(
span.TraceID.String(),
span.ID.String(),
parentSpanIdString,
span.Name,
getServiceName(span),
serviceTags,
float64(span.Timestamp.UnixMicro())/1000,
float64(span.Duration.Microseconds())/1000,
logs,
tags,
)
}
return newFrame
}
func getServiceName(span model.SpanModel) string {
if span.LocalEndpoint != nil && span.LocalEndpoint.ServiceName != "" {
return span.LocalEndpoint.ServiceName
} else if span.RemoteEndpoint != nil && span.RemoteEndpoint.ServiceName != "" {
return span.RemoteEndpoint.ServiceName
}
return "unknown"
}
func getServiceTags(span model.SpanModel) []TraceKeyValuePair {
tags := make([]TraceKeyValuePair, 0, 4)
endpoint := span.LocalEndpoint
endpointType := "local"
if endpoint == nil {
endpoint = span.RemoteEndpoint
endpointType = "remote"
}
if endpoint == nil {
return tags
}
if endpoint.IPv4 != nil {
tag := valueToTag("ipv4", endpoint.IPv4.String())
tags = append(tags, tag)
}
if endpoint.IPv6 != nil {
tag := valueToTag("ipv6", endpoint.IPv6.String())
tags = append(tags, tag)
}
if endpoint.Port != 0 {
tag := valueToTag("port", endpoint.Port)
tags = append(tags, tag)
}
if endpointType != "" {
tag := valueToTag("endpointType", endpointType)
tags = append(tags, tag)
}
return tags
}
func valueToTag(key string, value interface{}) TraceKeyValuePair {
return TraceKeyValuePair{
Key: key,
Value: value,
}
}
func transformAnnotationsToTraceLogs(annotations []model.Annotation) []TraceLog {
transformed := make([]TraceLog, 0, len(annotations))
if len(annotations) == 0 {
return transformed
}
for _, annotation := range annotations {
transformedAnnotation := TraceLog{
Timestamp: annotation.Timestamp.UnixMicro(),
Fields: []TraceKeyValuePair{
{
Key: "annotation",
Value: annotation.Value,
},
},
}
transformed = append(transformed, transformedAnnotation)
}
return transformed
}
func transformTags(span model.SpanModel) []TraceKeyValuePair {
tags := make([]TraceKeyValuePair, 0, len(span.Tags)+2)
for key, value := range span.Tags {
if key == "error" {
// Remap error tag to show error icon and include error details
tags = append(tags, TraceKeyValuePair{
Key: "error",
Value: true,
})
tags = append(tags, TraceKeyValuePair{
Key: "errorValue",
Value: value,
})
} else {
tags = append(tags, TraceKeyValuePair{
Key: key,
Value: value,
})
}
}
// Prepend kind if present
if span.Kind != "" {
tags = append([]TraceKeyValuePair{{Key: "kind", Value: span.Kind}}, tags...)
}
// Prepend shared if present
if span.Shared {
tags = append([]TraceKeyValuePair{{Key: "shared", Value: span.Shared}}, tags...)
}
return tags
}
+83
View File
@@ -0,0 +1,83 @@
package zipkin
import (
"net"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/experimental"
"github.com/openzipkin/zipkin-go/model"
)
func TestTransformResponse(t *testing.T) {
t.Run("simple_trace", func(t *testing.T) {
span1 := model.SpanModel{
SpanContext: model.SpanContext{
TraceID: model.TraceID{
High: 123,
Low: 456,
},
ID: 1,
},
Name: "span 1",
Kind: "CLIENT",
Timestamp: time.Unix(0, 1*int64(time.Microsecond)),
Duration: 10 * time.Microsecond,
LocalEndpoint: &model.Endpoint{
ServiceName: "service 1",
IPv4: net.IPv4(1, 0, 0, 1),
Port: 42,
},
Annotations: []model.Annotation{
{Timestamp: time.Unix(0, 2*int64(time.Microsecond)), Value: "annotation text"},
{Timestamp: time.Unix(0, 6*int64(time.Microsecond)), Value: "annotation text 3"},
},
Tags: map[string]string{
"tag1": "val1",
"tag2": "val2",
},
}
span2 := model.SpanModel{
SpanContext: model.SpanContext{
TraceID: model.TraceID{
High: 123,
Low: 456,
},
ID: 2,
ParentID: &span1.ID,
},
Name: "span 2",
Timestamp: time.Unix(0, 4*int64(time.Microsecond)),
Duration: 5 * time.Microsecond,
LocalEndpoint: &model.Endpoint{
ServiceName: "service 2",
IPv4: net.IPv4(1, 0, 0, 1),
},
Tags: map[string]string{
"error": "404",
},
}
span3 := model.SpanModel{
SpanContext: model.SpanContext{
TraceID: model.TraceID{
High: 123,
Low: 456,
},
ID: 3,
ParentID: &span1.ID,
},
Name: "span 3",
Timestamp: time.Unix(0, 6*int64(time.Microsecond)),
Duration: 7 * time.Microsecond,
RemoteEndpoint: &model.Endpoint{
ServiceName: "spanstore-jdbc",
IPv6: net.ParseIP("::1"),
},
}
spans := []model.SpanModel{span1, span2, span3}
frames := transformResponse(spans, "test")
experimental.CheckGoldenJSONFrame(t, "./testdata", "simple_trace.golden", frames, false)
})
}
+247
View File
@@ -0,0 +1,247 @@
// 🌟 This was machine generated. Do not edit. 🌟
//
// Frame[0] {
// "typeVersion": [
// 0,
// 0
// ],
// "custom": {
// "traceFormat": "zipkin"
// },
// "preferredVisualisationType": "trace"
// }
// Name: test
// Dimensions: 10 Fields by 3 Rows
// +----------------------------------+------------------+--------------------+---------------------+-------------------+-----------------------------------------------------------------------------------------------------+-----------------+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------+
// | Name: traceID | Name: spanID | Name: parentSpanID | Name: operationName | Name: serviceName | Name: serviceTags | Name: startTime | Name: duration | Name: logs | Name: tags |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []*string | Type: []string | Type: []string | Type: []json.RawMessage | Type: []float64 | Type: []float64 | Type: []json.RawMessage | Type: []json.RawMessage |
// +----------------------------------+------------------+--------------------+---------------------+-------------------+-----------------------------------------------------------------------------------------------------+-----------------+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------+
// | 000000000000007b00000000000001c8 | 0000000000000001 | null | span 1 | service 1 | [{"key":"ipv4","value":"1.0.0.1"},{"key":"port","value":42},{"key":"endpointType","value":"local"}] | 0.001 | 0.01 | [{"Timestamp":2,"Fields":[{"key":"annotation","value":"annotation text"}],"Name":""},{"Timestamp":6,"Fields":[{"key":"annotation","value":"annotation text 3"}],"Name":""}] | [{"key":"kind","value":"CLIENT"},{"key":"tag2","value":"val2"},{"key":"tag1","value":"val1"}] |
// | 000000000000007b00000000000001c8 | 0000000000000002 | 0000000000000001 | span 2 | service 2 | [{"key":"ipv4","value":"1.0.0.1"},{"key":"endpointType","value":"local"}] | 0.004 | 0.005 | [] | [{"key":"error","value":true},{"key":"errorValue","value":"404"}] |
// | 000000000000007b00000000000001c8 | 0000000000000003 | 0000000000000001 | span 3 | spanstore-jdbc | [{"key":"ipv6","value":"::1"},{"key":"endpointType","value":"remote"}] | 0.006 | 0.007 | [] | [] |
// +----------------------------------+------------------+--------------------+---------------------+-------------------+-----------------------------------------------------------------------------------------------------+-----------------+-----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
{
"status": 200,
"frames": [
{
"schema": {
"name": "test",
"meta": {
"typeVersion": [
0,
0
],
"custom": {
"traceFormat": "zipkin"
},
"preferredVisualisationType": "trace"
},
"fields": [
{
"name": "traceID",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "spanID",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "parentSpanID",
"type": "string",
"typeInfo": {
"frame": "string",
"nullable": true
}
},
{
"name": "operationName",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "serviceName",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "serviceTags",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage"
}
},
{
"name": "startTime",
"type": "number",
"typeInfo": {
"frame": "float64"
}
},
{
"name": "duration",
"type": "number",
"typeInfo": {
"frame": "float64"
}
},
{
"name": "logs",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage"
}
},
{
"name": "tags",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage"
}
}
]
},
"data": {
"values": [
[
"000000000000007b00000000000001c8",
"000000000000007b00000000000001c8",
"000000000000007b00000000000001c8"
],
[
"0000000000000001",
"0000000000000002",
"0000000000000003"
],
[
null,
"0000000000000001",
"0000000000000001"
],
[
"span 1",
"span 2",
"span 3"
],
[
"service 1",
"service 2",
"spanstore-jdbc"
],
[
[
{
"key": "ipv4",
"value": "1.0.0.1"
},
{
"key": "port",
"value": 42
},
{
"key": "endpointType",
"value": "local"
}
],
[
{
"key": "ipv4",
"value": "1.0.0.1"
},
{
"key": "endpointType",
"value": "local"
}
],
[
{
"key": "ipv6",
"value": "::1"
},
{
"key": "endpointType",
"value": "remote"
}
]
],
[
0.001,
0.004,
0.006
],
[
0.01,
0.005,
0.007
],
[
[
{
"Timestamp": 2,
"Fields": [
{
"key": "annotation",
"value": "annotation text"
}
],
"Name": ""
},
{
"Timestamp": 6,
"Fields": [
{
"key": "annotation",
"value": "annotation text 3"
}
],
"Name": ""
}
],
[],
[]
],
[
[
{
"key": "kind",
"value": "CLIENT"
},
{
"key": "tag2",
"value": "val2"
},
{
"key": "tag1",
"value": "val1"
}
],
[
{
"key": "error",
"value": true
},
{
"key": "errorValue",
"value": "404"
}
],
[]
]
]
}
}
]
}
+9 -1
View File
@@ -58,7 +58,7 @@ func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext
}
instance, ok := i.(*datasourceInfo)
if !ok {
return nil, errors.New("failed to cast datasource info")
return nil, backend.DownstreamError(errors.New("failed to cast datasource info"))
}
return instance, nil
}
@@ -87,3 +87,11 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq
handler := httpadapter.New(s.registerResourceRoutes())
return handler.CallResource(ctx, req, sender)
}
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
dsInfo, err := s.getDSInfo(ctx, req.PluginContext)
if err != nil {
return nil, err
}
return queryData(ctx, dsInfo, req)
}
@@ -60,7 +60,10 @@ export class ZipkinDatasource extends DataSourceWithBackend<ZipkinQuery, ZipkinJ
}
if (target.query) {
const query = this.applyVariables(target, options.scopedVars);
if (config.featureToggles.zipkinBackendMigration && !this.nodeGraph?.enabled) {
return super.query(options);
}
const query = this.applyTemplateVariables(target, options.scopedVars);
return this.request<ZipkinSpan[]>(`${apiPrefix}/trace/${encodeURIComponent(query.query)}`).pipe(
map((res) => responseToDataQueryResponse(res, this.nodeGraph?.enabled))
);
@@ -99,12 +102,12 @@ export class ZipkinDatasource extends DataSourceWithBackend<ZipkinQuery, ZipkinJ
return {
...query,
datasource: this.getRef(),
...this.applyVariables(query, scopedVars),
...this.applyTemplateVariables(query, scopedVars),
};
});
}
applyVariables(query: ZipkinQuery, scopedVars: ScopedVars) {
applyTemplateVariables(query: ZipkinQuery, scopedVars: ScopedVars) {
const expandedQuery = { ...query };
return {