Elasticsearch: Implement CheckHealth method in the backend (#81671)

* Elasticsearch: Implement CheckHealth method

* improve logger output

* remove frontend healthcheck

* Revert "remove frontend healthcheck"

This reverts commit 676265f39e.

* adapt test

---------

Co-authored-by: Sven Grossmann <svennergr@gmail.com>
This commit is contained in:
Mikel Vuka 2024-02-02 11:08:52 +01:00 committed by GitHub
parent 6f02d193f6
commit 65e9990a87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 187 additions and 0 deletions

View File

@ -0,0 +1,107 @@
package elasticsearch
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"path"
"time"
"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
}
start := time.Now()
logger.Debug("Sending healthcheck request to Elasticsearch", "url", esUrl.String())
response, err := ds.HTTPClient.Do(request)
if err != nil {
logger.Error("Failed to do healthcheck request", "error", err, "url", esUrl.String())
return &backend.CheckHealthResult{
Status: backend.HealthStatusUnknown,
Message: "Failed to do healthcheck request",
}, err
}
if response.StatusCode == http.StatusRequestTimeout {
return &backend.CheckHealthResult{
Status: backend.HealthStatusError,
Message: "Elasticsearch data source is not healthy",
}, nil
}
logger.Info("Response received from Elasticsearch", "statusCode", response.StatusCode, "status", "ok", "duration", time.Since(start))
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 data source is healthy"
if jsonData["status"] == "red" {
status = backend.HealthStatusError
message = "Elasticsearch data source is not healthy"
}
return &backend.CheckHealthResult{
Status: status,
Message: message,
}, nil
}

View File

@ -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 data source 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 data source is not healthy", res.Message)
}
type FakeRoundTripper struct {
isDsHealthy bool
}
func (fakeRoundTripper *FakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
var res *http.Response
if fakeRoundTripper.isDsHealthy {
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 {
isDsHealthy bool
}
func (fakeInstanceManager *FakeInstanceManager) Get(tx context.Context, pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
httpClient, _ := sdkhttpclient.New(sdkhttpclient.Options{})
httpClient.Transport = &FakeRoundTripper{isDsHealthy: fakeInstanceManager.isDsHealthy}
return es.DatasourceInfo{
HTTPClient: httpClient,
}, nil
}
func (*FakeInstanceManager) Do(_ context.Context, _ backend.PluginContext, _ instancemgmt.InstanceCallbackFunc) error {
return nil
}
func GetMockService(isDsHealthy bool) *Service {
return &Service{
im: &FakeInstanceManager{isDsHealthy: isDsHealthy},
}
}