Graphite: Backend functions endpoint (#110771)

* 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

* 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

* Lint

* Lint
This commit is contained in:
Andreas Christou
2025-09-12 23:23:44 +01:00
committed by GitHub
parent cb7abbaa0f
commit 3081ac166a
3 changed files with 195 additions and 50 deletions
+72 -34
View File
@@ -1,6 +1,7 @@
package graphite
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -16,13 +17,14 @@ import (
"go.opentelemetry.io/otel/codes"
)
type resourceHandler[T any] func(context.Context, *datasourceInfo, T) ([]byte, int, error)
type resourceHandler[T any] func(context.Context, *datasourceInfo, *T) ([]byte, int, error)
func (s *Service) newResourceMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/events", handleResourceReq[GraphiteEventsRequest](s.handleEvents, s))
mux.HandleFunc("/metrics/find", handleResourceReq[GraphiteMetricsFindRequest](s.handleMetricsFind, s))
mux.HandleFunc("/metrics/expand", handleResourceReq[GraphiteMetricsFindRequest](s.handleMetricsExpand, s))
mux.HandleFunc("/events", handleResourceReq(s.handleEvents, s))
mux.HandleFunc("/metrics/find", handleResourceReq(s.handleMetricsFind, s))
mux.HandleFunc("/metrics/expand", handleResourceReq(s.handleMetricsExpand, s))
mux.HandleFunc("/functions", handleResourceReq(s.handleFunctions, s))
return mux
}
@@ -39,17 +41,28 @@ func handleResourceReq[T any](handlerFn resourceHandler[T], s *Service) func(rw
}
defer func() {
if err := req.Body.Close(); err != nil {
s.logger.Warn("Failed to close response body", "err", err)
if req.Body != nil {
if err := req.Body.Close(); err != nil {
s.logger.Warn("Failed to close request body", "err", err)
writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
return
}
}
}()
var parsedBody *T
if req.Body != nil {
body, err := io.ReadAll(req.Body)
if err != nil {
s.logger.Error("Failed to read request body", "error", err)
writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
return
}
}()
requestBody, err := io.ReadAll(req.Body)
if err != nil {
s.logger.Error("Failed to read request body", "error", err)
writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
return
parsedBody, err = parseRequestBody[T](body, s.logger)
if err != nil {
writeErrorResponse(rw, http.StatusBadRequest, fmt.Sprintf("failed to parse request body: %v", err))
return
}
}
if handlerFn == nil {
@@ -57,13 +70,7 @@ func handleResourceReq[T any](handlerFn resourceHandler[T], s *Service) func(rw
return
}
parsedBody, err := parseRequestBody[T](requestBody, s.logger)
if err != nil {
writeErrorResponse(rw, http.StatusBadRequest, fmt.Sprintf("failed to parse request body: %v", err))
return
}
response, statusCode, err := handlerFn(ctx, dsInfo, *parsedBody)
response, statusCode, err := handlerFn(ctx, dsInfo, parsedBody)
if err != nil {
writeErrorResponse(rw, statusCode, fmt.Sprintf("failed to handle resource request: %v", err))
return
@@ -78,7 +85,7 @@ func handleResourceReq[T any](handlerFn resourceHandler[T], s *Service) func(rw
}
}
func (s *Service) handleEvents(ctx context.Context, dsInfo *datasourceInfo, eventsRequestJson GraphiteEventsRequest) ([]byte, int, error) {
func (s *Service) handleEvents(ctx context.Context, dsInfo *datasourceInfo, eventsRequestJson *GraphiteEventsRequest) ([]byte, int, error) {
queryParams := map[string]string{
"from": eventsRequestJson.From,
"until": eventsRequestJson.Until,
@@ -96,7 +103,7 @@ func (s *Service) handleEvents(ctx context.Context, dsInfo *datasourceInfo, even
return nil, http.StatusInternalServerError, fmt.Errorf("failed to create events request %v", err)
}
events, statusCode, err := doGraphiteRequest[[]GraphiteEventsResponse](ctx, dsInfo, s.logger, req)
events, _, statusCode, err := doGraphiteRequest[[]GraphiteEventsResponse](ctx, dsInfo, s.logger, req, false)
if err != nil {
return nil, statusCode, fmt.Errorf("events request failed: %v", err)
}
@@ -112,7 +119,7 @@ func (s *Service) handleEvents(ctx context.Context, dsInfo *datasourceInfo, even
return graphiteEventsResponse, statusCode, nil
}
func (s *Service) handleMetricsFind(ctx context.Context, dsInfo *datasourceInfo, metricsFindRequestJson GraphiteMetricsFindRequest) ([]byte, int, error) {
func (s *Service) handleMetricsFind(ctx context.Context, dsInfo *datasourceInfo, metricsFindRequestJson *GraphiteMetricsFindRequest) ([]byte, int, error) {
if metricsFindRequestJson.Query == "" {
return nil, http.StatusBadRequest, fmt.Errorf("query is required")
}
@@ -139,7 +146,7 @@ func (s *Service) handleMetricsFind(ctx context.Context, dsInfo *datasourceInfo,
return nil, http.StatusInternalServerError, fmt.Errorf("failed to create metrics find request %v", err)
}
metrics, statusCode, err := doGraphiteRequest[[]GraphiteMetricsFindResponse](ctx, dsInfo, s.logger, req)
metrics, _, statusCode, err := doGraphiteRequest[[]GraphiteMetricsFindResponse](ctx, dsInfo, s.logger, req, false)
if err != nil {
return nil, statusCode, fmt.Errorf("metrics find request failed: %v", err)
}
@@ -152,7 +159,7 @@ func (s *Service) handleMetricsFind(ctx context.Context, dsInfo *datasourceInfo,
return metricsFindResponse, statusCode, nil
}
func (s *Service) handleMetricsExpand(ctx context.Context, dsInfo *datasourceInfo, metricsExpandRequestJson GraphiteMetricsFindRequest) ([]byte, int, error) {
func (s *Service) handleMetricsExpand(ctx context.Context, dsInfo *datasourceInfo, metricsExpandRequestJson *GraphiteMetricsFindRequest) ([]byte, int, error) {
if metricsExpandRequestJson.Query == "" {
return nil, http.StatusBadRequest, fmt.Errorf("query is required")
}
@@ -176,7 +183,7 @@ func (s *Service) handleMetricsExpand(ctx context.Context, dsInfo *datasourceInf
return nil, http.StatusInternalServerError, fmt.Errorf("failed to create metrics expand request %v", err)
}
metrics, statusCode, err := doGraphiteRequest[GraphiteMetricsExpandResponse](ctx, dsInfo, s.logger, req)
metrics, _, statusCode, err := doGraphiteRequest[GraphiteMetricsExpandResponse](ctx, dsInfo, s.logger, req, false)
if err != nil {
return nil, statusCode, fmt.Errorf("metrics expand request failed: %v", err)
}
@@ -196,7 +203,29 @@ func (s *Service) handleMetricsExpand(ctx context.Context, dsInfo *datasourceInf
return metricsExpandResponse, statusCode, nil
}
func doGraphiteRequest[T any](ctx context.Context, dsInfo *datasourceInfo, logger log.Logger, req *http.Request) (*T, int, error) {
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)
}
_, rawBody, statusCode, err := doGraphiteRequest[map[string]any](ctx, dsInfo, s.logger, req, true)
if err != nil {
return nil, statusCode, fmt.Errorf("version request failed: %v", err)
}
if rawBody == nil {
return []byte{}, statusCode, nil
}
rawBodyReplaced := bytes.ReplaceAll(*rawBody, []byte("\"default\": Infinity"), []byte("\"default\": 1e9999"))
return rawBodyReplaced, statusCode, nil
}
func doGraphiteRequest[T any](ctx context.Context, dsInfo *datasourceInfo, logger log.Logger, req *http.Request, isRaw bool) (*T, *[]byte, int, error) {
_, span := tracing.DefaultTracer().Start(ctx, "graphite request")
defer span.End()
span.SetAttributes(
@@ -209,7 +238,7 @@ func doGraphiteRequest[T any](ctx context.Context, dsInfo *datasourceInfo, logge
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, http.StatusInternalServerError, fmt.Errorf("failed to complete request: %v", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to complete request: %v", err)
}
defer func() {
@@ -218,12 +247,12 @@ func doGraphiteRequest[T any](ctx context.Context, dsInfo *datasourceInfo, logge
}
}()
parsedResponse, err := parseResponse[T](res)
parsedResponse, rawBody, err := parseResponse[T](res, isRaw, logger)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to parse response: %v", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to parse response: %v", err)
}
return parsedResponse, res.StatusCode, nil
return parsedResponse, rawBody, res.StatusCode, nil
}
func parseRequestBody[V any](requestBody []byte, logger log.Logger) (*V, error) {
@@ -236,19 +265,28 @@ func parseRequestBody[V any](requestBody []byte, logger log.Logger) (*V, error)
return requestJson, nil
}
func parseResponse[V any](res *http.Response) (*V, error) {
func parseResponse[V any](res *http.Response, isRaw bool, logger log.Logger) (*V, *[]byte, error) {
encoding := res.Header.Get("Content-Encoding")
body, err := decode(encoding, res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %v", err)
return nil, nil, fmt.Errorf("failed to read response: %v", err)
}
if res.StatusCode/100 != 2 {
logger.Warn("Request failed", "status", res.Status, "body", string(body))
return nil, nil, fmt.Errorf("request failed, status: %d", res.StatusCode)
}
if isRaw {
return nil, &body, nil
}
data := new(V)
err = json.Unmarshal(body, &data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
return nil, nil, fmt.Errorf("failed to unmarshal response: %v", err)
}
return data, nil
return data, nil, nil
}
func writeErrorResponse(rw http.ResponseWriter, code int, msg string) {