From 5d48747fea5d02a7d23b4d7e8135a3d928906979 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Mon, 15 Sep 2025 13:56:08 +0200 Subject: [PATCH] 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 --- pkg/tsdb/graphite/resource_handler.go | 28 +++++-- pkg/tsdb/graphite/resource_handler_test.go | 80 +++++++++++++++++++ .../plugins/datasource/graphite/datasource.ts | 8 +- 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/graphite/resource_handler.go b/pkg/tsdb/graphite/resource_handler.go index dfd1a3c6568..2651c320d65 100644 --- a/pkg/tsdb/graphite/resource_handler.go +++ b/pkg/tsdb/graphite/resource_handler.go @@ -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) diff --git a/pkg/tsdb/graphite/resource_handler_test.go b/pkg/tsdb/graphite/resource_handler_test.go index c4ba2e201d2..66777f95072 100644 --- a/pkg/tsdb/graphite/resource_handler_test.go +++ b/pkg/tsdb/graphite/resource_handler_test.go @@ -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 diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 2c09afd4b83..93fa556c428 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -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('version'); + const semver = new SemVer(version); + return valid(semver) ? version : ''; + } + return lastValueFrom( this.doGraphiteRequest(httpOptions).pipe( map((results: FetchResponse) => {