* 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 tests * Review * Review * Fix packages * Fix merge issues
156 lines
4.8 KiB
Go
156 lines
4.8 KiB
Go
package graphite
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
)
|
|
|
|
type resourceHandler func(context.Context, *datasourceInfo, []byte) ([]byte, int, error)
|
|
|
|
func (s *Service) newResourceMux() *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/events", s.handleResourceReq(s.handleEvents))
|
|
return mux
|
|
}
|
|
|
|
func (s *Service) handleResourceReq(handlerFn resourceHandler) func(rw http.ResponseWriter, req *http.Request) {
|
|
return func(rw http.ResponseWriter, req *http.Request) {
|
|
s.logger.Debug("Received resource call", "url", req.URL.String(), "method", req.Method)
|
|
|
|
pluginCtx := backend.PluginConfigFromContext(req.Context())
|
|
ctx := req.Context()
|
|
dsInfo, err := s.getDSInfo(ctx, pluginCtx)
|
|
if err != nil {
|
|
writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
|
|
return
|
|
}
|
|
|
|
defer func() {
|
|
if err := req.Body.Close(); err != nil {
|
|
s.logger.Warn("Failed to close response body", "err", 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 events request body", "error", err)
|
|
writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("unexpected error %v", err))
|
|
return
|
|
}
|
|
|
|
if handlerFn == nil {
|
|
writeErrorResponse(rw, http.StatusInternalServerError, "responseFn should not be nil")
|
|
return
|
|
}
|
|
|
|
response, statusCode, err := handlerFn(ctx, dsInfo, requestBody)
|
|
if err != nil {
|
|
writeErrorResponse(rw, statusCode, fmt.Sprintf("failed to handle resource request: %v", err))
|
|
return
|
|
}
|
|
|
|
rw.WriteHeader(statusCode)
|
|
_, err = rw.Write(response)
|
|
if err != nil {
|
|
writeErrorResponse(rw, http.StatusInternalServerError, fmt.Sprintf("failed to write events response: %v", err))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) handleEvents(ctx context.Context, dsInfo *datasourceInfo, requestBody []byte) ([]byte, int, error) {
|
|
eventsRequestJson := GraphiteEventsRequest{}
|
|
err := json.Unmarshal(requestBody, &eventsRequestJson)
|
|
if err != nil {
|
|
s.logger.Error("Failed to unmarshal events request body to JSON", "error", err)
|
|
return nil, http.StatusInternalServerError, fmt.Errorf("unexpected error %v", err)
|
|
}
|
|
|
|
eventsUrl, err := url.Parse(fmt.Sprintf("%s/events/get_data", dsInfo.URL))
|
|
if err != nil {
|
|
return nil, http.StatusInternalServerError, fmt.Errorf("unexpected error %v", err)
|
|
}
|
|
|
|
queryValues := eventsUrl.Query()
|
|
queryValues.Set("from", eventsRequestJson.From)
|
|
queryValues.Set("until", eventsRequestJson.Until)
|
|
if eventsRequestJson.Tags != "" {
|
|
queryValues.Set("tags", eventsRequestJson.Tags)
|
|
}
|
|
|
|
eventsUrl.RawQuery = queryValues.Encode()
|
|
|
|
p := eventsUrl.String()
|
|
graphiteReq, err := http.NewRequestWithContext(ctx, http.MethodGet, p, nil)
|
|
if err != nil {
|
|
s.logger.Info("Failed to create request", "error", err)
|
|
return nil, http.StatusInternalServerError, fmt.Errorf("failed to create request: %v", err)
|
|
}
|
|
|
|
_, span := tracing.DefaultTracer().Start(ctx, "graphite events")
|
|
defer span.End()
|
|
span.SetAttributes(
|
|
attribute.Int64("datasource_id", dsInfo.Id),
|
|
)
|
|
res, err := dsInfo.HTTPClient.Do(graphiteReq)
|
|
if res != nil {
|
|
span.SetAttributes(attribute.Int("graphite.response.code", res.StatusCode))
|
|
}
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return nil, http.StatusInternalServerError, fmt.Errorf("failed to complete events request: %v", err)
|
|
}
|
|
|
|
defer func() {
|
|
err := res.Body.Close()
|
|
if err != nil {
|
|
s.logger.Warn("Failed to close response body", "error", err)
|
|
}
|
|
}()
|
|
|
|
encoding := res.Header.Get("Content-Encoding")
|
|
body, err := decode(encoding, res.Body)
|
|
if err != nil {
|
|
return nil, res.StatusCode, fmt.Errorf("failed to read events response: %v", err)
|
|
}
|
|
|
|
events := []GraphiteEventsResponse{}
|
|
err = json.Unmarshal(body, &events)
|
|
if err != nil {
|
|
return nil, http.StatusInternalServerError, fmt.Errorf("failed to unmarshal events response: %v", err)
|
|
}
|
|
|
|
// We construct this struct to avoid frontend changes.
|
|
graphiteEventsResponse, err := json.Marshal(map[string][]GraphiteEventsResponse{
|
|
"data": events,
|
|
})
|
|
if err != nil {
|
|
return nil, http.StatusInternalServerError, fmt.Errorf("failed to marshal events response: %s", err)
|
|
}
|
|
|
|
return graphiteEventsResponse, res.StatusCode, nil
|
|
}
|
|
|
|
func writeErrorResponse(rw http.ResponseWriter, code int, msg string) {
|
|
rw.WriteHeader(code)
|
|
errorBody := map[string]string{
|
|
"error": msg,
|
|
}
|
|
jsonRes, _ := json.Marshal(errorBody)
|
|
_, err := rw.Write(jsonRes)
|
|
if err != nil {
|
|
backend.Logger.Error("Unable to write HTTP response", "error", err)
|
|
}
|
|
}
|