diff --git a/pkg/tsdb/graphite/resource_handler.go b/pkg/tsdb/graphite/resource_handler.go index 99199c4de75..d8a1c3508ec 100644 --- a/pkg/tsdb/graphite/resource_handler.go +++ b/pkg/tsdb/graphite/resource_handler.go @@ -25,6 +25,7 @@ func (s *Service) newResourceMux() *http.ServeMux { mux.HandleFunc("/metrics/find", handleResourceReq(s.handleMetricsFind, s)) mux.HandleFunc("/metrics/expand", handleResourceReq(s.handleMetricsExpand, s)) mux.HandleFunc("/functions", handleResourceReq(s.handleFunctions, s)) + mux.HandleFunc("/tags/autoComplete/tags", handleResourceReq(s.handleTagsAutocomplete, s)) return mux } @@ -203,6 +204,35 @@ func (s *Service) handleMetricsExpand(ctx context.Context, dsInfo *datasourceInf return metricsExpandResponse, statusCode, nil } +func (s *Service) handleTagsAutocomplete(ctx context.Context, dsInfo *datasourceInfo, tagsAutocompleteRequestJson *GraphiteTagsRequest) ([]byte, int, error) { + queryParams := map[string]string{ + "from": tagsAutocompleteRequestJson.From, + "until": tagsAutocompleteRequestJson.Until, + "limit": fmt.Sprintf("%d", tagsAutocompleteRequestJson.Limit), + "tagPrefix": tagsAutocompleteRequestJson.TagPrefix, + } + req, err := s.createRequest(ctx, dsInfo, URLParams{ + SubPath: "tags/autoComplete/tags", + Method: http.MethodGet, + QueryParams: queryParams, + }) + if err != nil { + return nil, http.StatusInternalServerError, fmt.Errorf("failed to create metrics expand request %v", err) + } + + tags, _, statusCode, err := doGraphiteRequest[[]string](ctx, dsInfo, s.logger, req, false) + if err != nil { + return nil, statusCode, fmt.Errorf("tags autocomplete request failed: %v", err) + } + + tagsResponse, err := json.Marshal(tags) + if err != nil { + return nil, http.StatusInternalServerError, fmt.Errorf("failed to marshal tags autocomplete response: %s", err) + } + + return tagsResponse, statusCode, nil +} + func (s *Service) handleFunctions(ctx context.Context, dsInfo *datasourceInfo, _ *any) ([]byte, int, error) { req, err := s.createRequest(ctx, dsInfo, URLParams{ SubPath: "functions", diff --git a/pkg/tsdb/graphite/resource_handler_test.go b/pkg/tsdb/graphite/resource_handler_test.go index 4352b34cc79..c331c2d2cab 100644 --- a/pkg/tsdb/graphite/resource_handler_test.go +++ b/pkg/tsdb/graphite/resource_handler_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -94,7 +95,7 @@ func TestHandleEvents(t *testing.T) { name: "Invalid URL", dsInfo: &datasourceInfo{ Id: 1, - URL: "ht tp://invalid url", // Invalid URL + URL: "ht tp://invalid url", }, request: GraphiteEventsRequest{From: "now-1h", Until: "now"}, expectedStatus: http.StatusInternalServerError, @@ -211,7 +212,7 @@ func TestHandleMetricsFind(t *testing.T) { name: "Invalid URL", dsInfo: &datasourceInfo{ Id: 1, - URL: "ht tp://invalid url", // Invalid URL + URL: "ht tp://invalid url", }, request: GraphiteMetricsFindRequest{Query: "app.grafana.*"}, expectedStatus: http.StatusInternalServerError, @@ -321,7 +322,7 @@ func TestHandleMetricsExpand(t *testing.T) { name: "Invalid URL", dsInfo: &datasourceInfo{ Id: 1, - URL: "ht tp://invalid url", // Invalid URL + URL: "ht tp://invalid url", }, request: GraphiteMetricsFindRequest{Query: "app.grafana.*"}, expectedStatus: http.StatusInternalServerError, @@ -394,6 +395,109 @@ func TestHandleMetricsExpand(t *testing.T) { } } +func TestHandleTagsAutocomplete(t *testing.T) { + tests := []struct { + name string + request GraphiteTagsRequest + responseBody string + statusCode int + expectError bool + errorContains string + expectedData []string + }{ + { + name: "successful tags autocomplete request", + request: GraphiteTagsRequest{ + From: "1h", + Until: "now", + Limit: 10, + TagPrefix: "app", + }, + responseBody: `["app", "application", "app_name"]`, + statusCode: 200, + expectedData: []string{"app", "application", "app_name"}, + }, + { + name: "tags autocomplete with minimal request", + request: GraphiteTagsRequest{}, + responseBody: `["tag1", "tag2"]`, + statusCode: 200, + expectedData: []string{"tag1", "tag2"}, + }, + { + name: "tags autocomplete with empty response", + request: GraphiteTagsRequest{ + TagPrefix: "nonexistent", + }, + responseBody: `[]`, + statusCode: 200, + expectedData: []string{}, + }, + { + name: "tags autocomplete server error - invalid JSON causes marshal error", + request: GraphiteTagsRequest{ + From: "invalid", + }, + responseBody: `invalid json response`, + statusCode: 400, + expectError: true, + errorContains: "tags autocomplete 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.handleTagsAutocomplete(context.Background(), dsInfo, &tt.request) + + 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 tags []string + err = json.Unmarshal(result, &tags) + assert.NoError(t, err) + assert.Equal(t, tt.expectedData, tags) + } + + if !tt.expectError { + expectedURL := "http://graphite.example.com/tags/autoComplete/tags" + assert.Contains(t, mockTransport.lastRequest.URL.String(), expectedURL) + + if tt.request.From != "" { + assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("from=%s", tt.request.From)) + } + if tt.request.Until != "" { + assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("until=%s", tt.request.Until)) + } + if tt.request.Limit != 0 { + assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("limit=%d", tt.request.Limit)) + } + if tt.request.TagPrefix != "" { + assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("tagPrefix=%s", tt.request.TagPrefix)) + } + } + }) + } +} func TestHandleFunctions(t *testing.T) { tests := []struct { name string diff --git a/pkg/tsdb/graphite/types.go b/pkg/tsdb/graphite/types.go index 30d872f375a..2e427f1d327 100644 --- a/pkg/tsdb/graphite/types.go +++ b/pkg/tsdb/graphite/types.go @@ -59,3 +59,10 @@ type GraphiteMetricsFindResponse struct { type GraphiteMetricsExpandResponse struct { Results []string `json:"results"` } + +type GraphiteTagsRequest struct { + From string `json:"from"` + Until string `json:"until"` + Limit int `json:"limit,omitempty"` + TagPrefix string `json:"tagPrefix,omitempty"` +} diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index fc2e818233f..14b1139f3ad 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -854,7 +854,7 @@ export class GraphiteDatasource ); } - getTagsAutoComplete(expressions: string[], tagPrefix?: string, optionalOptions?: any) { + async getTagsAutoComplete(expressions: string[], tagPrefix?: string, optionalOptions?: any) { const options = optionalOptions || {}; const params: BackendSrvRequest['params'] = { expr: _map(expressions, (expression) => this.templateSrv.replace((expression || '').trim())), @@ -871,6 +871,18 @@ export class GraphiteDatasource params.until = this.translateTime(options.range.to, true, options.timezone); } + if (config.featureToggles.graphiteBackendMode) { + const tags = await this.postResource('tags/autoComplete/tags', { + from: typeof params.from === 'string' ? params.from : `${params.from}`, + until: typeof params.until === 'string' ? params.until : `${params.until}`, + tagPrefix, + limit: options.limit, + }); + return tags.map((tag) => ({ + text: tag, + })); + } + const httpOptions: BackendSrvRequest = { method: 'GET', url: '/tags/autoComplete/tags',