From 00521cc803ca8c941d967ac0b737a4cfc3cfa0e6 Mon Sep 17 00:00:00 2001 From: Mikel Vuka Date: Wed, 31 Jan 2024 23:38:29 +0100 Subject: [PATCH] Elasticsearch: Implement CheckHealth method --- pkg/tsdb/elasticsearch/healthcheck.go | 105 +++++++++++++++++++++ pkg/tsdb/elasticsearch/healthcheck_test.go | 80 ++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 pkg/tsdb/elasticsearch/healthcheck.go create mode 100644 pkg/tsdb/elasticsearch/healthcheck_test.go diff --git a/pkg/tsdb/elasticsearch/healthcheck.go b/pkg/tsdb/elasticsearch/healthcheck.go new file mode 100644 index 00000000000..750344bf481 --- /dev/null +++ b/pkg/tsdb/elasticsearch/healthcheck.go @@ -0,0 +1,105 @@ +package elasticsearch + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "path" + + "github.com/grafana/grafana-plugin-sdk-go/backend" +) + +func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + logger := eslog.FromContext(ctx) + + ds, err := s.getDSInfo(ctx, req.PluginContext) + if err != nil { + logger.Error("Failed to get data source info", "error", err) + return &backend.CheckHealthResult{ + Status: backend.HealthStatusUnknown, + Message: "Failed to get data source info", + }, err + } + + esUrl, err := url.Parse(ds.URL) + if err != nil { + logger.Error("Failed to parse data source URL", "error", err, "url", ds.URL) + return &backend.CheckHealthResult{ + Status: backend.HealthStatusUnknown, + Message: "Failed to parse data source URL", + }, err + } + + esUrl.Path = path.Join(esUrl.Path, "_cluster/health") + esUrl.RawQuery = "wait_for_status=yellow" + + request, err := http.NewRequestWithContext(ctx, "GET", esUrl.String(), nil) + if err != nil { + logger.Error("Failed to create request", "error", err, "url", esUrl.String()) + return &backend.CheckHealthResult{ + Status: backend.HealthStatusUnknown, + Message: "Failed to create request", + }, err + } + + logger.Debug("Sending health request to Elasticsearch") + response, err := ds.HTTPClient.Do(request) + + if err != nil { + logger.Error("Failed to Do request", "error", err, "url", esUrl.String()) + return &backend.CheckHealthResult{ + Status: backend.HealthStatusUnknown, + Message: "Failed to Do request", + }, err + } + + if response.StatusCode == http.StatusRequestTimeout { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: "Elasticsearch cluster is not healthy", + }, nil + } + + logger.Info("Response received from Elasticsearch", "statusCode", response.StatusCode, "status", "ok") + + defer func() { + if err := response.Body.Close(); err != nil { + logger.Warn("Failed to close response body", "error", err) + } + }() + + body, err := io.ReadAll(response.Body) + if err != nil { + logger.Error("Error reading response body bytes", "error", err) + return &backend.CheckHealthResult{ + Status: backend.HealthStatusUnknown, + Message: "Failed to read response", + }, err + } + + jsonData := map[string]any{} + + err = json.Unmarshal(body, &jsonData) + if err != nil { + logger.Error("Error during json unmarshal of the body", "error", err) + return &backend.CheckHealthResult{ + Status: backend.HealthStatusUnknown, + Message: "Failed to unmarshal response", + }, err + } + + status := backend.HealthStatusOk + message := "Elasticsearch cluster is healthy" + + if jsonData["status"] == "red" { + status = backend.HealthStatusError + message = "Elasticsearch cluster is not healthy" + } + + return &backend.CheckHealthResult{ + Status: status, + Message: message, + }, nil +} diff --git a/pkg/tsdb/elasticsearch/healthcheck_test.go b/pkg/tsdb/elasticsearch/healthcheck_test.go new file mode 100644 index 00000000000..370c08f3ef0 --- /dev/null +++ b/pkg/tsdb/elasticsearch/healthcheck_test.go @@ -0,0 +1,80 @@ +package elasticsearch + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" + "github.com/stretchr/testify/assert" +) + +func Test_Healthcheck_OK(t *testing.T) { + service := GetMockService(true) + res, _ := service.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{}, + Headers: nil, + }) + assert.Equal(t, backend.HealthStatusOk, res.Status) + assert.Equal(t, "Elasticsearch cluster is healthy", res.Message) +} + +func Test_Healthcheck_Timeout(t *testing.T) { + service := GetMockService(false) + res, _ := service.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + PluginContext: backend.PluginContext{}, + Headers: nil, + }) + assert.Equal(t, backend.HealthStatusError, res.Status) + assert.Equal(t, "Elasticsearch cluster is not healthy", res.Message) +} + +type FakeRoundTripper struct { + isClusterHealthy bool +} + +func (fakeRoundTripper *FakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + var res *http.Response + if fakeRoundTripper.isClusterHealthy { + res = &http.Response{ + StatusCode: http.StatusOK, + Status: "200 OK", + Body: io.NopCloser(bytes.NewBufferString("{\"status\":\"green\"}")), + } + } else { + res = &http.Response{ + StatusCode: http.StatusRequestTimeout, + Status: "408 Request Timeout", + Body: io.NopCloser(bytes.NewBufferString("{\"status\":\"red\"}")), + } + } + return res, nil +} + +type FakeInstanceManager struct { + isClusterHealthy bool +} + +func (fakeInstanceManager *FakeInstanceManager) Get(tx context.Context, pluginContext backend.PluginContext) (instancemgmt.Instance, error) { + httpClient, _ := sdkhttpclient.New(sdkhttpclient.Options{}) + httpClient.Transport = &FakeRoundTripper{isClusterHealthy: fakeInstanceManager.isClusterHealthy} + + return es.DatasourceInfo{ + HTTPClient: httpClient, + }, nil +} + +func (*FakeInstanceManager) Do(_ context.Context, _ backend.PluginContext, _ instancemgmt.InstanceCallbackFunc) error { + return nil +} + +func GetMockService(isClusterHealthy bool) *Service { + return &Service{ + im: &FakeInstanceManager{isClusterHealthy: isClusterHealthy}, + } +}