Add support for sending health check to datasource plugins. (#22771)
closes #21519 ref grafana/grafana-plugin-sdk-go#93
This commit is contained in:
@@ -265,6 +265,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||
apiRoute.Any("/datasources/:id/resources", hs.CallDatasourceResource)
|
||||
apiRoute.Any("/datasources/:id/resources/*", hs.CallDatasourceResource)
|
||||
apiRoute.Any("/datasources/:id/health", hs.CheckDatasourceHealth)
|
||||
|
||||
// Folders
|
||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||
|
||||
@@ -323,3 +323,78 @@ func convertModelToDtos(ds *models.DataSource) dtos.DataSource {
|
||||
|
||||
return dto
|
||||
}
|
||||
|
||||
// CheckDatasourceHealth sends a health check request to the plugin datasource
|
||||
// /api/datasource/:id/health
|
||||
func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) {
|
||||
datasourceID := c.ParamsInt64("id")
|
||||
|
||||
ds, err := hs.DatasourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
|
||||
if err != nil {
|
||||
if err == models.ErrDataSourceAccessDenied {
|
||||
c.JsonApiErr(403, "Access denied to datasource", err)
|
||||
return
|
||||
}
|
||||
c.JsonApiErr(500, "Unable to load datasource metadata", err)
|
||||
return
|
||||
}
|
||||
|
||||
plugin, ok := hs.PluginManager.GetDatasource(ds.Type)
|
||||
if !ok {
|
||||
c.JsonApiErr(500, "Unable to find datasource plugin", err)
|
||||
return
|
||||
}
|
||||
|
||||
config := &backendplugin.PluginConfig{
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
DataSourceConfig: &backendplugin.DataSourceConfig{
|
||||
ID: ds.Id,
|
||||
Name: ds.Name,
|
||||
URL: ds.Url,
|
||||
Database: ds.Database,
|
||||
User: ds.User,
|
||||
BasicAuthEnabled: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
JSONData: ds.JsonData,
|
||||
DecryptedSecureJSONData: ds.DecryptedValues(),
|
||||
Updated: ds.Updated,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), config)
|
||||
if err != nil {
|
||||
if err == backendplugin.ErrPluginNotRegistered {
|
||||
c.JsonApiErr(404, "Plugin not found", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return status unknown instead?
|
||||
if err == backendplugin.ErrDiagnosticsNotSupported {
|
||||
c.JsonApiErr(404, "Health check not implemented", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return status unknown or error instead?
|
||||
if err == backendplugin.ErrHealthCheckFailed {
|
||||
c.JsonApiErr(500, "Plugin health check failed", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JsonApiErr(500, "Plugin healthcheck returned an unknown error", err)
|
||||
return
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"status": resp.Status.String(),
|
||||
"message": resp.Message,
|
||||
"jsonDetails": resp.JSONDetails,
|
||||
}
|
||||
|
||||
if resp.Status != backendplugin.HealthStatusOk {
|
||||
c.JSON(503, payload)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, payload)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ type HTTPServer struct {
|
||||
Login *login.LoginService `inject:""`
|
||||
License models.Licensing `inject:""`
|
||||
BackendPluginManager backendplugin.Manager `inject:""`
|
||||
PluginManager *plugins.PluginManager `inject:""`
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Init() error {
|
||||
|
||||
+61
-28
@@ -1,10 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -14,6 +16,41 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// ErrPluginNotFound is returned when an requested plugin is not installed.
|
||||
var ErrPluginNotFound error = errors.New("plugin not found, no installed plugin with that id")
|
||||
|
||||
func (hs *HTTPServer) getPluginConfig(pluginID string, user *models.SignedInUser) (backendplugin.PluginConfig, error) {
|
||||
pluginConfig := backendplugin.PluginConfig{}
|
||||
plugin, exists := plugins.Plugins[pluginID]
|
||||
if !exists {
|
||||
return pluginConfig, ErrPluginNotFound
|
||||
}
|
||||
|
||||
var jsonData *simplejson.Json
|
||||
var decryptedSecureJSONData map[string]string
|
||||
var updated time.Time
|
||||
|
||||
ps, err := hs.getCachedPluginSettings(pluginID, user)
|
||||
if err != nil {
|
||||
if err != models.ErrPluginSettingNotFound {
|
||||
return pluginConfig, errutil.Wrap("Failed to get plugin settings", err)
|
||||
}
|
||||
jsonData = simplejson.New()
|
||||
decryptedSecureJSONData = make(map[string]string)
|
||||
} else {
|
||||
decryptedSecureJSONData = ps.DecryptedValues()
|
||||
updated = ps.Updated
|
||||
}
|
||||
|
||||
return backendplugin.PluginConfig{
|
||||
OrgID: user.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
JSONData: jsonData,
|
||||
DecryptedSecureJSONData: decryptedSecureJSONData,
|
||||
Updated: updated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response {
|
||||
typeFilter := c.Query("type")
|
||||
enabledFilter := c.Query("enabled")
|
||||
@@ -209,7 +246,17 @@ func ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDashboardCommand) R
|
||||
// /api/plugins/:pluginId/health
|
||||
func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
|
||||
pluginID := c.Params("pluginId")
|
||||
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), pluginID)
|
||||
|
||||
config, err := hs.getPluginConfig(pluginID, c.SignedInUser)
|
||||
if err != nil {
|
||||
if err == ErrPluginNotFound {
|
||||
return Error(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
}
|
||||
|
||||
return Error(500, "Failed to get plugin settings", err)
|
||||
}
|
||||
|
||||
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), &config)
|
||||
if err != nil {
|
||||
if err == backendplugin.ErrPluginNotRegistered {
|
||||
return Error(404, "Plugin not found", err)
|
||||
@@ -224,6 +271,8 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
|
||||
if err == backendplugin.ErrHealthCheckFailed {
|
||||
return Error(500, "Plugin health check failed", err)
|
||||
}
|
||||
|
||||
return Error(500, "Plugin healthcheck returned an unknown error", err)
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
@@ -239,39 +288,23 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
|
||||
return JSON(200, payload)
|
||||
}
|
||||
|
||||
// CallResource passes a resource call from a plugin to the backend plugin.
|
||||
//
|
||||
// /api/plugins/:pluginId/resources/*
|
||||
func (hs *HTTPServer) CallResource(c *models.ReqContext) {
|
||||
pluginID := c.Params("pluginId")
|
||||
plugin, exists := plugins.Plugins[pluginID]
|
||||
if !exists {
|
||||
c.JsonApiErr(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
|
||||
config, err := hs.getPluginConfig(pluginID, c.SignedInUser)
|
||||
if err != nil {
|
||||
if err == ErrPluginNotFound {
|
||||
c.JsonApiErr(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
return
|
||||
}
|
||||
|
||||
c.JsonApiErr(500, "Failed to get plugin settings", err)
|
||||
return
|
||||
}
|
||||
|
||||
var jsonData *simplejson.Json
|
||||
var decryptedSecureJSONData map[string]string
|
||||
var updated time.Time
|
||||
|
||||
ps, err := hs.getCachedPluginSettings(pluginID, c.SignedInUser)
|
||||
if err != nil {
|
||||
if err != models.ErrPluginSettingNotFound {
|
||||
c.JsonApiErr(500, "Failed to get plugin settings", err)
|
||||
return
|
||||
}
|
||||
jsonData = simplejson.New()
|
||||
decryptedSecureJSONData = make(map[string]string)
|
||||
} else {
|
||||
decryptedSecureJSONData = ps.DecryptedValues()
|
||||
updated = ps.Updated
|
||||
}
|
||||
|
||||
config := backendplugin.PluginConfig{
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
JSONData: jsonData,
|
||||
DecryptedSecureJSONData: decryptedSecureJSONData,
|
||||
Updated: updated,
|
||||
}
|
||||
hs.BackendPluginManager.CallResource(config, c, c.Params("*"))
|
||||
}
|
||||
|
||||
|
||||
@@ -185,14 +185,27 @@ func (p *BackendPlugin) CollectMetrics(ctx context.Context, ch chan<- prometheus
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *BackendPlugin) checkHealth(ctx context.Context) (*pluginv2.CheckHealthResponse, error) {
|
||||
func (p *BackendPlugin) checkHealth(ctx context.Context, config *PluginConfig) (*pluginv2.CheckHealthResponse, error) {
|
||||
if p.diagnostics == nil || p.client == nil || p.client.Exited() {
|
||||
return &pluginv2.CheckHealthResponse{
|
||||
Status: pluginv2.CheckHealthResponse_UNKNOWN,
|
||||
}, nil
|
||||
}
|
||||
|
||||
res, err := p.diagnostics.CheckHealth(ctx, &pluginv2.CheckHealthRequest{})
|
||||
jsonDataBytes, err := config.JSONData.ToDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pconfig := &pluginv2.PluginConfig{
|
||||
OrgId: config.OrgID,
|
||||
PluginId: config.PluginID,
|
||||
JsonData: jsonDataBytes,
|
||||
DecryptedSecureJsonData: config.DecryptedSecureJSONData,
|
||||
LastUpdatedMS: config.Updated.UnixNano() / int64(time.Millisecond),
|
||||
}
|
||||
|
||||
res, err := p.diagnostics.CheckHealth(ctx, &pluginv2.CheckHealthRequest{Config: pconfig})
|
||||
if err != nil {
|
||||
if st, ok := status.FromError(err); ok {
|
||||
if st.Code() == codes.Unimplemented {
|
||||
|
||||
@@ -101,7 +101,7 @@ func NewRendererPluginDescriptor(pluginID, executablePath string, startFns Plugi
|
||||
}
|
||||
|
||||
type DiagnosticsPlugin interface {
|
||||
plugin.DiagnosticsServer
|
||||
plugin.DiagnosticsClient
|
||||
}
|
||||
|
||||
type ResourcePlugin interface {
|
||||
|
||||
@@ -43,7 +43,7 @@ type Manager interface {
|
||||
// StartPlugin starts a non-managed backend plugin
|
||||
StartPlugin(ctx context.Context, pluginID string) error
|
||||
// CheckHealth checks the health of a registered backend plugin.
|
||||
CheckHealth(ctx context.Context, pluginID string) (*CheckHealthResult, error)
|
||||
CheckHealth(ctx context.Context, pluginConfig *PluginConfig) (*CheckHealthResult, error)
|
||||
// CallResource calls a plugin resource.
|
||||
CallResource(pluginConfig PluginConfig, ctx *models.ReqContext, path string)
|
||||
}
|
||||
@@ -151,9 +151,9 @@ func (m *manager) stop() {
|
||||
}
|
||||
|
||||
// CheckHealth checks the health of a registered backend plugin.
|
||||
func (m *manager) CheckHealth(ctx context.Context, pluginID string) (*CheckHealthResult, error) {
|
||||
func (m *manager) CheckHealth(ctx context.Context, pluginConfig *PluginConfig) (*CheckHealthResult, error) {
|
||||
m.pluginsMu.RLock()
|
||||
p, registered := m.plugins[pluginID]
|
||||
p, registered := m.plugins[pluginConfig.PluginID]
|
||||
m.pluginsMu.RUnlock()
|
||||
|
||||
if !registered {
|
||||
@@ -164,7 +164,7 @@ func (m *manager) CheckHealth(ctx context.Context, pluginID string) (*CheckHealt
|
||||
return nil, ErrDiagnosticsNotSupported
|
||||
}
|
||||
|
||||
res, err := p.checkHealth(ctx)
|
||||
res, err := p.checkHealth(ctx, pluginConfig)
|
||||
if err != nil {
|
||||
p.logger.Error("Failed to check plugin health", "error", err)
|
||||
return nil, ErrHealthCheckFailed
|
||||
|
||||
@@ -188,6 +188,15 @@ func (pm *PluginManager) scan(pluginDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatasource returns a datasource based on passed pluginID if it exists
|
||||
//
|
||||
// This function fetches the datasource from the global variable DataSources in this package.
|
||||
// Rather then refactor all dependencies on the global variable we can use this as an transition.
|
||||
func (pm *PluginManager) GetDatasource(pluginID string) (*DataSourcePlugin, bool) {
|
||||
ds, exist := DataSources[pluginID]
|
||||
return ds, exist
|
||||
}
|
||||
|
||||
func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
|
||||
// We scan all the subfolders for plugin.json (with some exceptions) so that we also load embedded plugins, for
|
||||
// example https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel worldmap panel plugin
|
||||
|
||||
Reference in New Issue
Block a user