mirror of https://github.com/grafana/grafana.git
				
				
				
			Zipkin: Run resource calls through backend with feature toggle enabled (#96139)
* Zipkin: Run resource calls througgh backend with feature toggle enabled * Update * Don't return early in createZipkinURL and add tests * Update pkg/tsdb/zipkin/client.go Co-authored-by: Sriram <153843+yesoreyeram@users.noreply.github.com> * Update pkg/tsdb/zipkin/client.go Co-authored-by: Sriram <153843+yesoreyeram@users.noreply.github.com> * Update pkg/tsdb/zipkin/client.go Co-authored-by: Sriram <153843+yesoreyeram@users.noreply.github.com> * Update pkg/tsdb/zipkin/client_test.go Co-authored-by: Sriram <153843+yesoreyeram@users.noreply.github.com> * Update pkg/tsdb/zipkin/client_test.go Co-authored-by: Sriram <153843+yesoreyeram@users.noreply.github.com> * Update pkg/tsdb/zipkin/client_test.go Co-authored-by: Sriram <153843+yesoreyeram@users.noreply.github.com> * Update pkg/tsdb/zipkin/client_test.go Co-authored-by: Sriram <153843+yesoreyeram@users.noreply.github.com> * Fix lint * Fix tests --------- Co-authored-by: Sriram <153843+yesoreyeram@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									c7b6822a5e
								
							
						
					
					
						commit
						e5519161f2
					
				
							
								
								
									
										2
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										2
									
								
								go.mod
								
								
								
								
							|  | @ -476,6 +476,8 @@ require github.com/jmespath-community/go-jmespath v1.1.1 // @grafana/identity-ac | |||
| 
 | ||||
| require github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs | ||||
| 
 | ||||
| require github.com/openzipkin/zipkin-go v0.4.3 // @grafana/oss-big-tent | ||||
| 
 | ||||
| require ( | ||||
| 	cloud.google.com/go/longrunning v0.6.0 // indirect | ||||
| 	github.com/at-wat/mqtt-go v0.19.4 // indirect | ||||
|  |  | |||
							
								
								
									
										2
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										2
									
								
								go.sum
								
								
								
								
							|  | @ -2903,6 +2903,8 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ | |||
| github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= | ||||
| github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= | ||||
| github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= | ||||
| github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= | ||||
| github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= | ||||
| github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= | ||||
| github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= | ||||
| github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= | ||||
|  |  | |||
|  | @ -2,12 +2,14 @@ package zipkin | |||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend" | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend/log" | ||||
| 	"github.com/openzipkin/zipkin-go/model" | ||||
| ) | ||||
| 
 | ||||
| type ZipkinClient struct { | ||||
|  | @ -48,3 +50,119 @@ func (z *ZipkinClient) Services() ([]string, error) { | |||
| 	} | ||||
| 	return services, err | ||||
| } | ||||
| 
 | ||||
| // Spans returns list of spans for the given service
 | ||||
| // https://zipkin.io/zipkin-api/#/default/get_spans
 | ||||
| func (z *ZipkinClient) Spans(serviceName string) ([]string, error) { | ||||
| 	spans := []string{} | ||||
| 	if serviceName == "" { | ||||
| 		return spans, errors.New("invalid/empty serviceName") | ||||
| 	} | ||||
| 
 | ||||
| 	spansUrl, err := createZipkinURL(z.url, "/api/v2/spans", map[string]string{"serviceName": serviceName}) | ||||
| 	if err != nil { | ||||
| 		return spans, backend.DownstreamError(fmt.Errorf("failed to compose url: %w", err)) | ||||
| 	} | ||||
| 
 | ||||
| 	res, err := z.httpClient.Get(spansUrl) | ||||
| 	defer func() { | ||||
| 		if res != nil { | ||||
| 			if err = res.Body.Close(); err != nil { | ||||
| 				z.logger.Error("Failed to close response body", "error", err) | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 	if err != nil { | ||||
| 		return spans, err | ||||
| 	} | ||||
| 	if err := json.NewDecoder(res.Body).Decode(&spans); err != nil { | ||||
| 		return spans, err | ||||
| 	} | ||||
| 	return spans, err | ||||
| } | ||||
| 
 | ||||
| // Traces returns list of traces for the given service and span
 | ||||
| // https://zipkin.io/zipkin-api/#/default/get_traces
 | ||||
| func (z *ZipkinClient) Traces(serviceName string, spanName string) ([][]model.SpanModel, error) { | ||||
| 	traces := [][]model.SpanModel{} | ||||
| 	if serviceName == "" { | ||||
| 		return traces, errors.New("invalid/empty serviceName") | ||||
| 	} | ||||
| 	if spanName == "" { | ||||
| 		return traces, errors.New("invalid/empty spanName") | ||||
| 	} | ||||
| 	tracesUrl, err := createZipkinURL(z.url, "/api/v2/traces", map[string]string{"serviceName": serviceName, "spanName": spanName}) | ||||
| 	if err != nil { | ||||
| 		return traces, backend.DownstreamError(fmt.Errorf("failed to compose url: %w", err)) | ||||
| 	} | ||||
| 
 | ||||
| 	res, err := z.httpClient.Get(tracesUrl) | ||||
| 	defer func() { | ||||
| 		if res != nil { | ||||
| 			if err = res.Body.Close(); err != nil { | ||||
| 				z.logger.Error("Failed to close response body", "error", err) | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 	if err != nil { | ||||
| 		return traces, err | ||||
| 	} | ||||
| 	if err := json.NewDecoder(res.Body).Decode(&traces); err != nil { | ||||
| 		return traces, err | ||||
| 	} | ||||
| 	return traces, err | ||||
| } | ||||
| 
 | ||||
| // Trace returns trace for the given traceId
 | ||||
| // https://zipkin.io/zipkin-api/#/default/get_trace__traceId_
 | ||||
| func (z *ZipkinClient) Trace(traceId string) ([]model.SpanModel, error) { | ||||
| 	trace := []model.SpanModel{} | ||||
| 	if traceId == "" { | ||||
| 		return trace, errors.New("invalid/empty traceId") | ||||
| 	} | ||||
| 
 | ||||
| 	traceUrl, err := url.JoinPath(z.url, "/api/v2/trace", url.QueryEscape(traceId)) | ||||
| 	if err != nil { | ||||
| 		return trace, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err)) | ||||
| 	} | ||||
| 
 | ||||
| 	res, err := z.httpClient.Get(traceUrl) | ||||
| 	defer func() { | ||||
| 		if res != nil { | ||||
| 			if err = res.Body.Close(); err != nil { | ||||
| 				z.logger.Error("Failed to close response body", "error", err) | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 	if err != nil { | ||||
| 		return trace, err | ||||
| 	} | ||||
| 	if err := json.NewDecoder(res.Body).Decode(&trace); err != nil { | ||||
| 		return trace, err | ||||
| 	} | ||||
| 	return trace, err | ||||
| } | ||||
| 
 | ||||
| func createZipkinURL(baseURL string, path string, params map[string]string) (string, error) { | ||||
| 	// Parse the base URL
 | ||||
| 	finalUrl, err := url.Parse(baseURL) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	// Add the path
 | ||||
| 	urlPath, err := url.JoinPath(finalUrl.Path, path) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	finalUrl.Path = urlPath | ||||
| 	// If there are query parameters, add them
 | ||||
| 	if len(params) > 0 { | ||||
| 		queryParams := finalUrl.Query() | ||||
| 		for k, v := range params { | ||||
| 			queryParams.Set(k, v) | ||||
| 		} | ||||
| 		finalUrl.RawQuery = queryParams.Encode() | ||||
| 	} | ||||
| 	// Return the composed URL as a string
 | ||||
| 	return finalUrl.String(), nil | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,357 @@ | |||
| package zipkin | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend/log" | ||||
| 	"github.com/openzipkin/zipkin-go/model" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestZipkinClient_Services(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		mockResponse   string | ||||
| 		mockStatusCode int | ||||
| 		expectedResult []string | ||||
| 		expectError    bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Successful response", | ||||
| 			mockResponse:   `["service1", "service2"]`, | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: []string{"service1", "service2"}, | ||||
| 			expectError:    false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Non-200 response", | ||||
| 			mockResponse:   "", | ||||
| 			mockStatusCode: http.StatusInternalServerError, | ||||
| 			expectedResult: []string{}, | ||||
| 			expectError:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Invalid JSON response", | ||||
| 			mockResponse:   `{invalid json`, | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: []string{}, | ||||
| 			expectError:    true, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 				assert.Equal(t, "/api/v2/services", r.URL.Path) | ||||
| 				w.WriteHeader(tt.mockStatusCode) | ||||
| 				_, _ = w.Write([]byte(tt.mockResponse)) | ||||
| 			})) | ||||
| 			defer server.Close() | ||||
| 
 | ||||
| 			client, _ := New(server.URL, server.Client(), log.New()) | ||||
| 			services, err := client.Services() | ||||
| 
 | ||||
| 			if tt.expectError { | ||||
| 				assert.Error(t, err) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			assert.Equal(t, tt.expectedResult, services) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestZipkinClient_Spans(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		serviceName    string | ||||
| 		mockResponse   string | ||||
| 		mockStatusCode int | ||||
| 		expectedResult []string | ||||
| 		expectError    bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Successful response", | ||||
| 			serviceName:    "service1", | ||||
| 			mockResponse:   `["span1", "span2"]`, | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: []string{"span1", "span2"}, | ||||
| 			expectError:    false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Non-200 response", | ||||
| 			serviceName:    "service1", | ||||
| 			mockResponse:   "", | ||||
| 			mockStatusCode: http.StatusNotFound, | ||||
| 			expectedResult: []string{}, | ||||
| 			expectError:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Invalid JSON response", | ||||
| 			serviceName:    "service1", | ||||
| 			mockResponse:   `{invalid json`, | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: []string{}, | ||||
| 			expectError:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty serviceName", | ||||
| 			serviceName:    "", | ||||
| 			mockResponse:   "", | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: []string{}, | ||||
| 			expectError:    true, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 				assert.Equal(t, "/api/v2/spans", r.URL.Path) | ||||
| 				w.WriteHeader(tt.mockStatusCode) | ||||
| 				_, _ = w.Write([]byte(tt.mockResponse)) | ||||
| 			})) | ||||
| 			defer server.Close() | ||||
| 
 | ||||
| 			client, _ := New(server.URL, server.Client(), log.New()) | ||||
| 			spans, err := client.Spans(tt.serviceName) | ||||
| 
 | ||||
| 			if tt.expectError { | ||||
| 				assert.Error(t, err) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			assert.Equal(t, tt.expectedResult, spans) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestZipkinClient_Traces(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		serviceName    string | ||||
| 		spanName       string | ||||
| 		mockResponse   interface{} | ||||
| 		mockStatusCode int | ||||
| 		expectedResult [][]model.SpanModel | ||||
| 		expectError    bool | ||||
| 		expectedError  string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Successful response", | ||||
| 			serviceName:    "service1", | ||||
| 			spanName:       "span1", | ||||
| 			mockResponse:   [][]model.SpanModel{{{SpanContext: model.SpanContext{TraceID: model.TraceID{Low: 1234}, ID: 1}, Name: "operation1", Tags: map[string]string{"key1": "value1"}}}}, | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: [][]model.SpanModel{{{SpanContext: model.SpanContext{TraceID: model.TraceID{Low: 1234}, ID: 1}, Name: "operation1", Tags: map[string]string{"key1": "value1"}}}}, | ||||
| 			expectError:    false, | ||||
| 			expectedError:  "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Non-200 response", | ||||
| 			serviceName:    "service1", | ||||
| 			spanName:       "span1", | ||||
| 			mockResponse:   nil, | ||||
| 			mockStatusCode: http.StatusForbidden, | ||||
| 			expectedResult: [][]model.SpanModel{}, | ||||
| 			expectError:    true, | ||||
| 			expectedError:  "EOF", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty serviceName", | ||||
| 			serviceName:    "", | ||||
| 			spanName:       "span1", | ||||
| 			mockResponse:   nil, | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: [][]model.SpanModel{}, | ||||
| 			expectError:    true, | ||||
| 			expectedError:  "invalid/empty serviceName", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Empty spanName", | ||||
| 			serviceName:    "service1", | ||||
| 			spanName:       "", | ||||
| 			mockResponse:   nil, | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: [][]model.SpanModel{}, | ||||
| 			expectError:    true, | ||||
| 			expectedError:  "invalid/empty spanName", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Valid response with empty trace list", | ||||
| 			serviceName:    "service1", | ||||
| 			spanName:       "span1", | ||||
| 			mockResponse:   [][]model.SpanModel{}, | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: [][]model.SpanModel{}, | ||||
| 			expectError:    false, | ||||
| 			expectedError:  "", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			var response []byte | ||||
| 			if mockData, ok := tt.mockResponse.([][]model.SpanModel); ok { | ||||
| 				response, _ = json.Marshal(mockData) | ||||
| 			} else if str, ok := tt.mockResponse.(string); ok { | ||||
| 				response = []byte(str) | ||||
| 			} | ||||
| 
 | ||||
| 			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 				assert.Equal(t, "/api/v2/traces", r.URL.Path) | ||||
| 				w.WriteHeader(tt.mockStatusCode) | ||||
| 				_, _ = w.Write(response) | ||||
| 			})) | ||||
| 			defer server.Close() | ||||
| 
 | ||||
| 			client, _ := New(server.URL, server.Client(), log.New()) | ||||
| 			traces, err := client.Traces(tt.serviceName, tt.spanName) | ||||
| 
 | ||||
| 			if tt.expectError { | ||||
| 				assert.Error(t, err) | ||||
| 				assert.Equal(t, err.Error(), tt.expectedError) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			assert.Equal(t, tt.expectedResult, traces) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestZipkinClient_Trace(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		traceID        string | ||||
| 		mockResponse   string | ||||
| 		mockStatusCode int | ||||
| 		expectedResult []model.SpanModel | ||||
| 		expectError    bool | ||||
| 		expectedError  string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Successful response", | ||||
| 			traceID:        "trace-id", | ||||
| 			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, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Invalid traceID", | ||||
| 			traceID:        "", | ||||
| 			mockResponse:   "", | ||||
| 			mockStatusCode: http.StatusOK, | ||||
| 			expectedResult: []model.SpanModel{}, | ||||
| 			expectError:    true, | ||||
| 			expectedError:  "invalid/empty traceId", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| 				w.WriteHeader(tt.mockStatusCode) | ||||
| 				_, _ = w.Write([]byte(tt.mockResponse)) | ||||
| 			})) | ||||
| 			defer server.Close() | ||||
| 			client, _ = New(server.URL, server.Client(), log.New()) | ||||
| 
 | ||||
| 			trace, err := client.Trace(tt.traceID) | ||||
| 
 | ||||
| 			if tt.expectError { | ||||
| 				assert.Error(t, err) | ||||
| 				assert.Empty(t, trace) | ||||
| 				assert.Equal(t, tt.expectedError, err.Error()) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 				assert.Equal(t, tt.expectedResult, trace) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCreateZipkinURL(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name      string | ||||
| 		baseURL   string | ||||
| 		path      string | ||||
| 		params    map[string]string | ||||
| 		expected  string | ||||
| 		shouldErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "WithPathAndParams", | ||||
| 			baseURL:  "http://example.com", | ||||
| 			path:     "api/v1/trace", | ||||
| 			params:   map[string]string{"key1": "value1", "key2": "value2"}, | ||||
| 			expected: "http://example.com/api/v1/trace?key1=value1&key2=value2", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "OnlyParams", | ||||
| 			baseURL:  "http://example.com", | ||||
| 			path:     "", | ||||
| 			params:   map[string]string{"key1": "value1"}, | ||||
| 			expected: "http://example.com?key1=value1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "NoParams", | ||||
| 			baseURL:  "http://example.com", | ||||
| 			path:     "api/v1/trace", | ||||
| 			params:   map[string]string{}, | ||||
| 			expected: "http://example.com/api/v1/trace", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:      "InvalidBaseURL", | ||||
| 			baseURL:   "http://example .com", | ||||
| 			path:      "api/v1/trace", | ||||
| 			params:    map[string]string{}, | ||||
| 			shouldErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "BaseURLWithPath", | ||||
| 			baseURL:  "http://example.com/base", | ||||
| 			path:     "api/v1/trace", | ||||
| 			params:   map[string]string{"key1": "value1"}, | ||||
| 			expected: "http://example.com/base/api/v1/trace?key1=value1", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			result, err := createZipkinURL(tc.baseURL, tc.path, tc.params) | ||||
| 
 | ||||
| 			if tc.shouldErr { | ||||
| 				if err == nil { | ||||
| 					t.Fatalf("Expected error, but got nil") | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Unexpected error: %v", err) | ||||
| 			} | ||||
| 
 | ||||
| 			if result != tc.expected { | ||||
| 				t.Errorf("Expected %s, got %s", tc.expected, result) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,88 @@ | |||
| package zipkin | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend" | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend/log" | ||||
| ) | ||||
| 
 | ||||
| func (s *Service) registerResourceRoutes() *http.ServeMux { | ||||
| 	router := http.NewServeMux() | ||||
| 	router.HandleFunc("GET /services", s.withDatasourceHandlerFunc(getServicesHandler)) | ||||
| 	router.HandleFunc("GET /spans", s.withDatasourceHandlerFunc(getSpansHandler)) | ||||
| 	router.HandleFunc("GET /traces", s.withDatasourceHandlerFunc(getTracesHandler)) | ||||
| 	router.HandleFunc("GET /trace/{traceId}", s.withDatasourceHandlerFunc(getTraceHandler)) | ||||
| 	return router | ||||
| } | ||||
| 
 | ||||
| func (s *Service) withDatasourceHandlerFunc(getHandler func(d *datasourceInfo) http.HandlerFunc) func(rw http.ResponseWriter, r *http.Request) { | ||||
| 	return func(rw http.ResponseWriter, r *http.Request) { | ||||
| 		client, err := s.getDSInfo(r.Context(), backend.PluginConfigFromContext(r.Context())) | ||||
| 		if err != nil { | ||||
| 			writeResponse(nil, errors.New("error getting data source information from context"), rw, client.ZipkinClient.logger) | ||||
| 			return | ||||
| 		} | ||||
| 		h := getHandler(client) | ||||
| 		h.ServeHTTP(rw, r) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func getServicesHandler(ds *datasourceInfo) http.HandlerFunc { | ||||
| 	return func(rw http.ResponseWriter, r *http.Request) { | ||||
| 		services, err := ds.ZipkinClient.Services() | ||||
| 		writeResponse(services, err, rw, ds.ZipkinClient.logger) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func getSpansHandler(ds *datasourceInfo) http.HandlerFunc { | ||||
| 	return func(rw http.ResponseWriter, r *http.Request) { | ||||
| 		serviceName := strings.TrimSpace(r.URL.Query().Get("serviceName")) | ||||
| 		spans, err := ds.ZipkinClient.Spans(serviceName) | ||||
| 		writeResponse(spans, err, rw, ds.ZipkinClient.logger) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func getTracesHandler(ds *datasourceInfo) http.HandlerFunc { | ||||
| 	return func(rw http.ResponseWriter, r *http.Request) { | ||||
| 		serviceName := strings.TrimSpace(r.URL.Query().Get("serviceName")) | ||||
| 		spanName := strings.TrimSpace(r.URL.Query().Get("spanName")) | ||||
| 		traces, err := ds.ZipkinClient.Traces(serviceName, spanName) | ||||
| 		writeResponse(traces, err, rw, ds.ZipkinClient.logger) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func getTraceHandler(ds *datasourceInfo) http.HandlerFunc { | ||||
| 	return func(rw http.ResponseWriter, r *http.Request) { | ||||
| 		traceId := strings.TrimSpace(r.PathValue("traceId")) | ||||
| 		trace, err := ds.ZipkinClient.Trace(traceId) | ||||
| 		writeResponse(trace, err, rw, ds.ZipkinClient.logger) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func writeResponse(res interface{}, err error, rw http.ResponseWriter, logger log.Logger) { | ||||
| 	if err != nil { | ||||
| 		// This is used for resource calls, we don't need to add actual error message, but we should log it
 | ||||
| 		logger.Warn("An error occurred while doing a resource call", "error", err) | ||||
| 		http.Error(rw, "An error occurred within the plugin", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 	// Response should not be string, but just in case, handle it
 | ||||
| 	if str, ok := res.(string); ok { | ||||
| 		rw.Header().Set("Content-Type", "text/plain") | ||||
| 		_, _ = rw.Write([]byte(str)) | ||||
| 		return | ||||
| 	} | ||||
| 	b, err := json.Marshal(res) | ||||
| 	if err != nil { | ||||
| 		// This is used for resource calls, we don't need to add actual error message, but we should log it
 | ||||
| 		logger.Warn("An error occurred while processing response from resource call", "error", err) | ||||
| 		http.Error(rw, "An error occurred within the plugin", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 	rw.Header().Set("Content-Type", "application/json") | ||||
| 	_, _ = rw.Write(b) | ||||
| } | ||||
|  | @ -8,6 +8,7 @@ import ( | |||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend" | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend/datasource" | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/infra/httpclient" | ||||
| ) | ||||
|  | @ -81,3 +82,8 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque | |||
| 		Message: "Data source is working", | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { | ||||
| 	handler := httpadapter.New(s.registerResourceRoutes()) | ||||
| 	return handler.CallResource(ctx, req, sender) | ||||
| } | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ describe('useServices', () => { | |||
|   it('returns services from datasource', async () => { | ||||
|     const ds = { | ||||
|       async metadataRequest(url) { | ||||
|         if (url === '/api/v2/services') { | ||||
|         if (url === 'services') { | ||||
|           return Promise.resolve(['service1', 'service2']); | ||||
|         } | ||||
|         return undefined; | ||||
|  | @ -50,11 +50,11 @@ describe('useLoadOptions', () => { | |||
|   it('loads spans and traces', async () => { | ||||
|     const ds = { | ||||
|       async metadataRequest(url, params) { | ||||
|         if (url === '/api/v2/spans' && params?.serviceName === 'service1') { | ||||
|         if (url === 'spans' && params?.serviceName === 'service1') { | ||||
|           return Promise.resolve(['span1', 'span2']); | ||||
|         } | ||||
| 
 | ||||
|         if (url === '/api/v2/traces' && params?.serviceName === 'service1' && params?.spanName === 'span1') { | ||||
|         if (url === 'traces' && params?.serviceName === 'service1' && params?.spanName === 'span1') { | ||||
|           return Promise.resolve([[{ name: 'trace1', duration: 10_000, traceId: 'traceId1' }]]); | ||||
|         } | ||||
|         return undefined; | ||||
|  |  | |||
|  | @ -21,7 +21,6 @@ import { | |||
|   Button, | ||||
| } from '@grafana/ui'; | ||||
| 
 | ||||
| import { apiPrefix } from './constants'; | ||||
| import { ZipkinDatasource } from './datasource'; | ||||
| import { ZipkinQuery, ZipkinQueryType, ZipkinSpan } from './types'; | ||||
| 
 | ||||
|  | @ -147,11 +146,9 @@ export function useServices( | |||
|   datasource: ZipkinDatasource, | ||||
|   setErrorText: (text: string) => void | ||||
| ): AsyncState<CascaderOption[]> { | ||||
|   const url = `${apiPrefix}/services`; | ||||
| 
 | ||||
|   const [servicesOptions, fetch] = useAsyncFn(async (): Promise<CascaderOption[]> => { | ||||
|     try { | ||||
|       const services: string[] | null = await datasource.metadataRequest(url); | ||||
|       const services: string[] | null = await datasource.metadataRequest('services'); | ||||
|       if (services) { | ||||
|         return services.sort().map((service) => ({ | ||||
|           label: service, | ||||
|  | @ -191,12 +188,11 @@ export function useLoadOptions(datasource: ZipkinDatasource, setErrorText: (text | |||
| 
 | ||||
|   const [, fetchSpans] = useAsyncFn( | ||||
|     async function findSpans(service: string): Promise<void> { | ||||
|       const url = `${apiPrefix}/spans`; | ||||
|       try { | ||||
|         // The response of this should have been full ZipkinSpan objects based on API docs but is just list
 | ||||
|         // of span names.
 | ||||
|         // TODO: check if this is some issue of version used or something else
 | ||||
|         const response: string[] = await datasource.metadataRequest(url, { serviceName: service }); | ||||
|         const response: string[] = await datasource.metadataRequest('spans', { serviceName: service }); | ||||
|         if (isMounted()) { | ||||
|           setAllOptions((state) => { | ||||
|             const spanOptions = fromPairs(response.map((span: string) => [span, undefined])); | ||||
|  | @ -218,7 +214,6 @@ export function useLoadOptions(datasource: ZipkinDatasource, setErrorText: (text | |||
| 
 | ||||
|   const [, fetchTraces] = useAsyncFn( | ||||
|     async function findTraces(serviceName: string, spanName: string): Promise<void> { | ||||
|       const url = `${apiPrefix}/traces`; | ||||
|       const search = { | ||||
|         serviceName, | ||||
|         spanName, | ||||
|  | @ -226,7 +221,7 @@ export function useLoadOptions(datasource: ZipkinDatasource, setErrorText: (text | |||
|       }; | ||||
|       try { | ||||
|         // This should return just root traces as there isn't any nesting
 | ||||
|         const traces: ZipkinSpan[][] = await datasource.metadataRequest(url, search); | ||||
|         const traces: ZipkinSpan[][] = await datasource.metadataRequest('traces', search); | ||||
|         if (isMounted()) { | ||||
|           const newTraces = traces.length | ||||
|             ? fromPairs( | ||||
|  |  | |||
|  | @ -1 +0,0 @@ | |||
| export const apiPrefix = '/api/v2'; | ||||
|  | @ -76,7 +76,7 @@ describe('ZipkinDatasource', () => { | |||
|     it('runs query', async () => { | ||||
|       setupBackendSrv(['service 1', 'service 2'] as unknown as ZipkinSpan[]); | ||||
|       const ds = new ZipkinDatasource(defaultSettings); | ||||
|       const response = await ds.metadataRequest('/api/v2/services'); | ||||
|       const response = await ds.metadataRequest('services'); | ||||
|       expect(response).toEqual(['service 1', 'service 2']); | ||||
|     }); | ||||
|   }); | ||||
|  |  | |||
|  | @ -22,11 +22,12 @@ import { | |||
|   TemplateSrv, | ||||
| } from '@grafana/runtime'; | ||||
| 
 | ||||
| import { apiPrefix } from './constants'; | ||||
| import { ZipkinQuery, ZipkinSpan } from './types'; | ||||
| import { createGraphFrames } from './utils/graphTransform'; | ||||
| import { transformResponse } from './utils/transforms'; | ||||
| 
 | ||||
| const apiPrefix = '/api/v2'; | ||||
| 
 | ||||
| export interface ZipkinJsonData extends DataSourceJsonData { | ||||
|   nodeGraph?: NodeGraphOptions; | ||||
| } | ||||
|  | @ -68,7 +69,11 @@ export class ZipkinDatasource extends DataSourceWithBackend<ZipkinQuery, ZipkinJ | |||
|   } | ||||
| 
 | ||||
|   async metadataRequest(url: string, params?: Record<string, unknown>) { | ||||
|     const res = await lastValueFrom(this.request(url, params, { hideFromInspector: true })); | ||||
|     if (config.featureToggles.zipkinBackendMigration) { | ||||
|       return await this.getResource(url, params); | ||||
|     } | ||||
|     const urlWithPrefix = `${apiPrefix}/${url}`; | ||||
|     const res = await lastValueFrom(this.request(urlWithPrefix, params, { hideFromInspector: true })); | ||||
|     return res.data; | ||||
|   } | ||||
| 
 | ||||
|  | @ -77,7 +82,7 @@ export class ZipkinDatasource extends DataSourceWithBackend<ZipkinQuery, ZipkinJ | |||
|       return await super.testDatasource(); | ||||
|     } | ||||
| 
 | ||||
|     await this.metadataRequest(`${apiPrefix}/services`); | ||||
|     await this.metadataRequest('services'); | ||||
|     return { status: 'success', message: 'Data source is working' }; | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue