Jaeger: run metadata requests through the backend (#100337)
* run metadata requests throught the backend * fix tests * add tests to backend * fix lint
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user