Graphite: Backend version endpoint (#110774)

* Add lint rules

* Backend decoupling

- Add standalone files
- Add graphite query type
- Add logger to Service
- Create logger in the ProvideService method
- Use a pointer for the HTTP client provider
- Update logger usage everywhere
- Update tracer type
- Replace simplejson with json
- Add dummy CallResource and CheckHealth methods
- Update tests

* Update ConfigEditor imports

* Update types imports

* Update datasource

- Switch to using semver package
- Update imports

* Update store imports

* Update helper imports and notification creation

* Update context import

* Update version numbers and logic

* Copy array_move from core

* Test updates

* Add required files and update plugin.json

* Update core references and packages

* Remove commented code

* Update wire

* Lint

* Fix import

* Copy null type

* More lint

* Update snapshot

* Refactor backend

- Split query logic into separate file
- Move utils to separate file

* Add health-check logic

- Support backend healthcheck if the FF is enabled

* Remove query import support as unneeded

* Add test

* Add util function for decoding responses

* Add events types

* Add resource handler

* Add events handler and generic resource req handler

* Tests

* Update frontend

- Add types
- Update events function to support backend requests

* Lint and typing

* Lint

* Add metrics find endpoint

- Add types
- Add generic response parser
- Add endpoint
- Tests

* Update FE functoin to use backend endpoint

* Lint

* Simplify request

* Update test

* Metrics expand type

* Extract shared logic and add metric expand endpoint

* Update tests

* Call metric expand from backend

* Rename type for clarity

* Add get resource req handler

* Refactor doGraphiteRequest, parseResponse

Update tests

* Migrate functions endpoint to backend

* Support tags autocomplete in backend

- Add tests
- Add types
- Remove unneeded comments

* Support tag values autocomplete

- Remove unused frontend endpoints
- Add types
- Update tests

* Support the version endpoint

* Add tests

* Review

* Review

* Fix packages

* Format

* Fix merge issues

* Review

* Fix undefined values

* Extract request creation

- Add method for create requests generically with tests
- Replace usage in query method
- Update usages in resource handlers
- Update tests
- Update types
This commit is contained in:
Andreas Christou
2025-09-15 12:56:08 +01:00
committed by GitHub
parent 90f682151a
commit 5d48747fea
3 changed files with 109 additions and 7 deletions
+22 -6
View File
@@ -27,7 +27,7 @@ func (s *Service) newResourceMux() *http.ServeMux {
mux.HandleFunc("/functions", handleResourceReq(s.handleFunctions, s))
mux.HandleFunc("/tags/autoComplete/tags", handleResourceReq(s.handleTagsAutocomplete, s))
mux.HandleFunc("/tags/autoComplete/values", handleResourceReq(s.handleTagValuesAutocomplete, s))
mux.HandleFunc("/version", handleResourceReq(s.handleVersion, s))
return mux
}
@@ -99,7 +99,6 @@ func (s *Service) handleEvents(ctx context.Context, dsInfo *datasourceInfo, even
req, err := s.createRequest(ctx, dsInfo, URLParams{
SubPath: "events/get_data",
Method: http.MethodGet,
QueryParams: queryParams,
})
if err != nil {
@@ -179,7 +178,6 @@ func (s *Service) handleMetricsExpand(ctx context.Context, dsInfo *datasourceInf
req, err := s.createRequest(ctx, dsInfo, URLParams{
SubPath: "metrics/expand",
Method: http.MethodGet,
QueryParams: queryParams,
})
if err != nil {
@@ -215,7 +213,6 @@ func (s *Service) handleTagsAutocomplete(ctx context.Context, dsInfo *datasource
}
req, err := s.createRequest(ctx, dsInfo, URLParams{
SubPath: "tags/autoComplete/tags",
Method: http.MethodGet,
QueryParams: queryParams,
})
if err != nil {
@@ -247,7 +244,6 @@ func (s *Service) handleTagValuesAutocomplete(ctx context.Context, dsInfo *datas
req, err := s.createRequest(ctx, dsInfo, URLParams{
SubPath: "tags/autoComplete/values",
Method: http.MethodGet,
QueryParams: queryParams,
})
if err != nil {
@@ -267,10 +263,30 @@ func (s *Service) handleTagValuesAutocomplete(ctx context.Context, dsInfo *datas
return tagValuesResponse, statusCode, nil
}
func (s *Service) handleVersion(ctx context.Context, dsInfo *datasourceInfo, _ *any) ([]byte, int, error) {
req, err := s.createRequest(ctx, dsInfo, URLParams{
SubPath: "version",
})
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to create version request %v", err)
}
version, _, statusCode, err := doGraphiteRequest[string](ctx, dsInfo, s.logger, req, false)
if err != nil {
return nil, statusCode, fmt.Errorf("version request failed: %v", err)
}
versionResponse, err := json.Marshal(version)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to marshal version response: %s", err)
}
return versionResponse, statusCode, nil
}
func (s *Service) handleFunctions(ctx context.Context, dsInfo *datasourceInfo, _ *any) ([]byte, int, error) {
req, err := s.createRequest(ctx, dsInfo, URLParams{
SubPath: "functions",
Method: http.MethodGet,
})
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to create functions request %v", err)
@@ -620,6 +620,86 @@ func TestHandleTagValuesAutocomplete(t *testing.T) {
})
}
}
func TestHandleVersion(t *testing.T) {
tests := []struct {
name string
responseBody string
statusCode int
expectError bool
errorContains string
expectedData string
}{
{
name: "successful version request",
responseBody: `"1.1.10"`,
statusCode: 200,
expectedData: "1.1.10",
},
{
name: "version with build info",
responseBody: `"1.1.10-pre1"`,
statusCode: 200,
expectedData: "1.1.10-pre1",
},
{
name: "version request server error - invalid JSON causes parse error",
responseBody: `{"error": "internal error"}`,
statusCode: 500,
expectError: true,
errorContains: "version request failed",
},
{
name: "version request not found - invalid JSON causes parse error",
responseBody: `{"error": "not found"}`,
statusCode: 404,
expectError: true,
errorContains: "version request failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockTransport := &mockRoundTripper{
respBody: []byte(tt.responseBody),
status: tt.statusCode,
}
dsInfo := &datasourceInfo{
HTTPClient: &http.Client{Transport: mockTransport},
URL: "http://graphite.example.com",
}
service := &Service{
logger: log.NewNullLogger(),
}
result, statusCode, err := service.handleVersion(context.Background(), dsInfo, nil)
if tt.expectError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.statusCode, statusCode)
var version string
err = json.Unmarshal(result, &version)
assert.NoError(t, err)
assert.Equal(t, tt.expectedData, version)
}
if !tt.expectError {
expectedURL := "http://graphite.example.com/version"
assert.Equal(t, expectedURL, mockTransport.lastRequest.URL.String())
assert.Equal(t, http.MethodGet, mockTransport.lastRequest.Method)
}
})
}
}
func TestHandleFunctions(t *testing.T) {
tests := []struct {
name string
@@ -871,7 +871,7 @@ export class GraphiteDatasource
return lastValueFrom(this.doGraphiteRequest(httpOptions).pipe(mapToTags()));
}
getVersion(optionalOptions: any) {
async getVersion(optionalOptions: any) {
const options = optionalOptions || {};
const httpOptions = {
@@ -880,6 +880,12 @@ export class GraphiteDatasource
requestId: options.requestId,
};
if (config.featureToggles.graphiteBackendMode) {
const version = await this.getResource<string>('version');
const semver = new SemVer(version);
return valid(semver) ? version : '';
}
return lastValueFrom(
this.doGraphiteRequest(httpOptions).pipe(
map((results: FetchResponse) => {