diff --git a/go.mod b/go.mod index a6f45a00d41..7b8fc74c59d 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index bf53ab65e21..31f3022ffb8 100644 --- a/go.sum +++ b/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= diff --git a/pkg/tsdb/zipkin/client.go b/pkg/tsdb/zipkin/client.go index f5e95333c8f..ff60decb326 100644 --- a/pkg/tsdb/zipkin/client.go +++ b/pkg/tsdb/zipkin/client.go @@ -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 +} diff --git a/pkg/tsdb/zipkin/client_test.go b/pkg/tsdb/zipkin/client_test.go new file mode 100644 index 00000000000..0e690194349 --- /dev/null +++ b/pkg/tsdb/zipkin/client_test.go @@ -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) + } + }) + } +} diff --git a/pkg/tsdb/zipkin/handler_callresource.go b/pkg/tsdb/zipkin/handler_callresource.go new file mode 100644 index 00000000000..c846ea2bae7 --- /dev/null +++ b/pkg/tsdb/zipkin/handler_callresource.go @@ -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) +} diff --git a/pkg/tsdb/zipkin/zipkin.go b/pkg/tsdb/zipkin/zipkin.go index 609a5046651..fc8576f266f 100644 --- a/pkg/tsdb/zipkin/zipkin.go +++ b/pkg/tsdb/zipkin/zipkin.go @@ -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) +} diff --git a/public/app/plugins/datasource/zipkin/QueryField.test.tsx b/public/app/plugins/datasource/zipkin/QueryField.test.tsx index 04f638e3688..b5d1ce77e37 100644 --- a/public/app/plugins/datasource/zipkin/QueryField.test.tsx +++ b/public/app/plugins/datasource/zipkin/QueryField.test.tsx @@ -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; diff --git a/public/app/plugins/datasource/zipkin/QueryField.tsx b/public/app/plugins/datasource/zipkin/QueryField.tsx index c79ee92307b..64a9aa85b3a 100644 --- a/public/app/plugins/datasource/zipkin/QueryField.tsx +++ b/public/app/plugins/datasource/zipkin/QueryField.tsx @@ -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 { - const url = `${apiPrefix}/services`; - const [servicesOptions, fetch] = useAsyncFn(async (): Promise => { 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 { - 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 { - 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( diff --git a/public/app/plugins/datasource/zipkin/constants.ts b/public/app/plugins/datasource/zipkin/constants.ts deleted file mode 100644 index 7e36f7eafb1..00000000000 --- a/public/app/plugins/datasource/zipkin/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const apiPrefix = '/api/v2'; diff --git a/public/app/plugins/datasource/zipkin/datasource.test.ts b/public/app/plugins/datasource/zipkin/datasource.test.ts index 10bafa53ad6..de32987694f 100644 --- a/public/app/plugins/datasource/zipkin/datasource.test.ts +++ b/public/app/plugins/datasource/zipkin/datasource.test.ts @@ -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']); }); }); diff --git a/public/app/plugins/datasource/zipkin/datasource.ts b/public/app/plugins/datasource/zipkin/datasource.ts index eb4a1e8a2ac..ceef9d14465 100644 --- a/public/app/plugins/datasource/zipkin/datasource.ts +++ b/public/app/plugins/datasource/zipkin/datasource.ts @@ -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) { - 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