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