Jaeger: run metadata requests through the backend (#100337)

* run metadata requests throught the backend

* fix tests

* add tests to backend

* fix lint
pull/101379/head
Gareth Dawson 10 months ago committed by GitHub
parent fcdbb5887d
commit af0e388622
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 69
      pkg/tsdb/jaeger/callresource.go
  2. 28
      pkg/tsdb/jaeger/client.go
  3. 166
      pkg/tsdb/jaeger/client_test.go
  4. 6
      pkg/tsdb/jaeger/jaeger.go
  5. 2
      public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx
  6. 8
      public/app/plugins/datasource/jaeger/components/SearchForm.tsx
  7. 12
      public/app/plugins/datasource/jaeger/datasource.ts

@ -0,0 +1,69 @@
package jaeger
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 /services/{service}/operations", s.withDatasourceHandlerFunc(getOperationsHandler))
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.JaegerClient.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.JaegerClient.Services()
writeResponse(services, err, rw, ds.JaegerClient.logger)
}
}
func getOperationsHandler(ds *datasourceInfo) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
service := strings.TrimSpace(r.PathValue("service"))
operations, err := ds.JaegerClient.Operations(service)
writeResponse(operations, err, rw, ds.JaegerClient.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)
}

@ -60,3 +60,31 @@ func (j *JaegerClient) Services() ([]string, error) {
services = response.Data
return services, err
}
func (j *JaegerClient) Operations(s string) ([]string, error) {
var response ServicesResponse
operations := []string{}
u, err := url.JoinPath(j.url, "/api/services/", s, "/operations")
if err != nil {
return operations, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err))
}
res, err := j.httpClient.Get(u)
if err != nil {
return operations, err
}
defer func() {
if err = res.Body.Close(); err != nil {
j.logger.Error("Failed to close response body", "error", err)
}
}()
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return operations, err
}
operations = response.Data
return operations, err
}

@ -0,0 +1,166 @@
package jaeger
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/stretchr/testify/assert"
)
func TestJaegerClient_Services(t *testing.T) {
tests := []struct {
name string
mockResponse string
mockStatusCode int
mockStatus string
expectedResult []string
expectError bool
expectedError error
}{
{
name: "Successful response",
mockResponse: `{"data": ["service1", "service2"], "total": 2, "limit": 0, "offset": 0}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{"service1", "service2"},
expectError: false,
expectedError: nil,
},
{
name: "Non-200 response",
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
mockStatus: "Internal Server Error",
expectedResult: []string{},
expectError: true,
expectedError: errors.New("Internal Server Error"),
},
{
name: "Invalid JSON response",
mockResponse: `{invalid json`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{},
expectError: true,
expectedError: &json.SyntaxError{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
client, err := New(server.URL, server.Client(), log.NewNullLogger())
assert.NoError(t, err)
services, err := client.Services()
if tt.expectError {
assert.Error(t, err)
if tt.expectedError != nil {
assert.IsType(t, tt.expectedError, err)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, services)
}
})
}
}
func TestJaegerClient_Operations(t *testing.T) {
tests := []struct {
name string
service string
mockResponse string
mockStatusCode int
mockStatus string
expectedResult []string
expectError bool
expectedError error
}{
{
name: "Successful response",
service: "test-service",
mockResponse: `{"data": ["operation1", "operation2"], "total": 2, "limit": 0, "offset": 0}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{"operation1", "operation2"},
expectError: false,
expectedError: nil,
},
{
name: "Non-200 response",
service: "test-service",
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
mockStatus: "Internal Server Error",
expectedResult: []string{},
expectError: true,
expectedError: errors.New("Internal Server Error"),
},
{
name: "Invalid JSON response",
service: "test-service",
mockResponse: `{invalid json`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{},
expectError: true,
expectedError: &json.SyntaxError{},
},
{
name: "Service with special characters",
service: "test/service:1",
mockResponse: `{"data": ["operation1"], "total": 1, "limit": 0, "offset": 0}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{"operation1"},
expectError: false,
expectedError: nil,
},
{
name: "Empty service",
service: "",
mockResponse: `{"data": [], "total": 0, "limit": 0, "offset": 0}`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{},
expectError: false,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
client, err := New(server.URL, server.Client(), log.NewNullLogger())
assert.NoError(t, err)
operations, err := client.Operations(tt.service)
if tt.expectError {
assert.Error(t, err)
if tt.expectedError != nil {
assert.IsType(t, tt.expectedError, err)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, operations)
}
})
}
}

@ -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"
)
@ -85,3 +86,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)
}

@ -34,7 +34,7 @@ describe('SearchForm', () => {
};
const ds = {
async metadataRequest(url) {
if (url === '/api/services') {
if (url === 'services') {
return Promise.resolve(['jaeger-query', 'service2', 'service3']);
}
return undefined;

@ -68,7 +68,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
useEffect(() => {
const getServices = async () => {
const services = await loadOptions('/api/services', 'services');
const services = await loadOptions('services', 'services');
if (query.service && getTemplateSrv().containsTemplate(query.service)) {
services.push(toOption(query.service));
}
@ -80,7 +80,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
useEffect(() => {
const getOperations = async () => {
const operations = await loadOptions(
`/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`,
`services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`,
'operations'
);
if (query.operation && getTemplateSrv().containsTemplate(query.operation)) {
@ -101,7 +101,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
<Select
inputId="service"
options={serviceOptions}
onOpenMenu={() => loadOptions('/api/services', 'services')}
onOpenMenu={() => loadOptions('services', 'services')}
isLoading={isLoading.services}
value={serviceOptions?.find((v) => v?.value === query.service) || undefined}
placeholder="Select a service"
@ -126,7 +126,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
options={operationOptions}
onOpenMenu={() =>
loadOptions(
`/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`,
`services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`,
'operations'
)
}

@ -52,8 +52,15 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
this.traceIdTimeParams = instanceSettings.jsonData.traceIdTimeParams;
}
/**
* Migrated to backend with feature toggle `jaegerBackendMigration`
*/
async metadataRequest(url: string, params?: Record<string, unknown>) {
const res = await lastValueFrom(this._request(url, params, { hideFromInspector: true }));
if (config.featureToggles.jaegerBackendMigration) {
return await this.getResource(url, params);
}
const res = await lastValueFrom(this._request('/api/' + url, params, { hideFromInspector: true }));
return res.data.data;
}
@ -193,6 +200,9 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ
};
}
/**
* Migrated to backend with feature toggle `jaegerBackendMigration`
*/
async testDatasource() {
if (config.featureToggles.jaegerBackendMigration) {
return await super.testDatasource();

Loading…
Cancel
Save