Graphite: Backend tags autocomplete endpoint (#110772)

* 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

* 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-12 23:53:09 +01:00
committed by GitHub
parent 3081ac166a
commit 211c0ca5c3
4 changed files with 157 additions and 4 deletions
+30
View File
@@ -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",
+107 -3
View File
@@ -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
+7
View File
@@ -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"`
}
@@ -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<string[]>('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',