diff --git a/.betterer.results b/.betterer.results
index af080651f8d..c64fc238700 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -2789,13 +2789,6 @@ exports[`better eslint`] = {
"public/app/features/datasources/components/DataSourcePluginState.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
],
- "public/app/features/datasources/components/DataSourceTestingStatus.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "2"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "3"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "4"]
- ],
"public/app/features/datasources/components/DataSourceTypeCard.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
diff --git a/pkg/tsdb/mysql/sqleng/handler_checkhealth.go b/pkg/tsdb/mysql/sqleng/handler_checkhealth.go
index 9f58da99da8..d224bb9d7ee 100644
--- a/pkg/tsdb/mysql/sqleng/handler_checkhealth.go
+++ b/pkg/tsdb/mysql/sqleng/handler_checkhealth.go
@@ -4,6 +4,9 @@ import (
"context"
"encoding/json"
"errors"
+ "fmt"
+ "net"
+ "strings"
"github.com/go-sql-driver/mysql"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -14,8 +17,8 @@ func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckH
err := e.db.Ping()
if err != nil {
logCheckHealthError(ctx, e.dsInfo, err, e.log)
- if req.PluginContext.User.Role == "Admin" {
- return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: err.Error()}, nil
+ if strings.EqualFold(req.PluginContext.User.Role, "Admin") {
+ return ErrToHealthCheckResult(err)
}
var driverErr *mysql.MySQLError
if errors.As(err, &driverErr) {
@@ -26,7 +29,41 @@ func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckH
return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil
}
-func logCheckHealthError(ctx context.Context, dsInfo DataSourceInfo, err error, logger log.Logger) {
+// ErrToHealthCheckResult converts error into user friendly health check message
+// This should be called with non nil error. If the err parameter is empty, we will send Internal Server Error
+func ErrToHealthCheckResult(err error) (*backend.CheckHealthResult, error) {
+ if err == nil {
+ return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "Internal Server Error"}, nil
+ }
+ res := &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: err.Error()}
+ details := map[string]string{}
+ var opErr *net.OpError
+ if errors.As(err, &opErr) {
+ res.Message = "Network error: Failed to connect to the server"
+ if opErr != nil && opErr.Err != nil {
+ res.Message += fmt.Sprintf(". Error message: %s", opErr.Err.Error())
+ }
+ details["verboseMessage"] = err.Error()
+ details["errorDetailsLink"] = "https://grafana.com/docs/grafana/latest/datasources/mysql/#configure-the-data-source"
+ }
+ var driverErr *mysql.MySQLError
+ if errors.As(err, &driverErr) {
+ res.Message = "Database error: Failed to connect to the MySQL server"
+ if driverErr != nil && driverErr.Number > 0 {
+ res.Message += fmt.Sprintf(". MySQL error number: %d", driverErr.Number)
+ }
+ details["verboseMessage"] = err.Error()
+ details["errorDetailsLink"] = "https://dev.mysql.com/doc/mysql-errors/8.4/en/"
+ }
+ detailBytes, marshalErr := json.Marshal(details)
+ if marshalErr != nil {
+ return res, nil
+ }
+ res.JSONDetails = detailBytes
+ return res, nil
+}
+
+func logCheckHealthError(_ context.Context, dsInfo DataSourceInfo, err error, logger log.Logger) {
configSummary := map[string]any{
"config_url_length": len(dsInfo.URL),
"config_user_length": len(dsInfo.User),
diff --git a/pkg/tsdb/mysql/sqleng/handler_checkhealth_test.go b/pkg/tsdb/mysql/sqleng/handler_checkhealth_test.go
new file mode 100644
index 00000000000..8f96b8b6e6f
--- /dev/null
+++ b/pkg/tsdb/mysql/sqleng/handler_checkhealth_test.go
@@ -0,0 +1,60 @@
+package sqleng
+
+import (
+ "errors"
+ "net"
+ "testing"
+
+ "github.com/go-sql-driver/mysql"
+ "github.com/grafana/grafana-plugin-sdk-go/backend"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestErrToHealthCheckResult(t *testing.T) {
+ tests := []struct {
+ name string
+ err error
+ want *backend.CheckHealthResult
+ }{
+ {
+ name: "without error",
+ want: &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "Internal Server Error"},
+ },
+ {
+ name: "network error",
+ err: errors.Join(errors.New("foo"), &net.OpError{Op: "read", Net: "tcp", Err: errors.New("some op")}),
+ want: &backend.CheckHealthResult{
+ Status: backend.HealthStatusError,
+ Message: "Network error: Failed to connect to the server. Error message: some op",
+ JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/mysql/#configure-the-data-source","verboseMessage":"foo\nread tcp: some op"}`),
+ },
+ },
+ {
+ name: "db error",
+ err: errors.Join(errors.New("foo"), &mysql.MySQLError{Number: uint16(1045), Message: "Access denied for user"}),
+ want: &backend.CheckHealthResult{
+ Status: backend.HealthStatusError,
+ Message: "Database error: Failed to connect to the MySQL server. MySQL error number: 1045",
+ JSONDetails: []byte(`{"errorDetailsLink":"https://dev.mysql.com/doc/mysql-errors/8.4/en/","verboseMessage":"foo\nError 1045: Access denied for user"}`),
+ },
+ },
+ {
+ name: "regular error",
+ err: errors.New("internal server error"),
+ want: &backend.CheckHealthResult{
+ Status: backend.HealthStatusError,
+ Message: "internal server error",
+ JSONDetails: []byte(`{}`),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ErrToHealthCheckResult(tt.err)
+ require.Nil(t, err)
+ assert.Equal(t, string(tt.want.JSONDetails), string(got.JSONDetails))
+ require.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/public/app/features/datasources/components/DataSourceTestingStatus.tsx b/public/app/features/datasources/components/DataSourceTestingStatus.tsx
index a234396e290..ab98012a7c3 100644
--- a/public/app/features/datasources/components/DataSourceTestingStatus.tsx
+++ b/public/app/features/datasources/components/DataSourceTestingStatus.tsx
@@ -5,6 +5,7 @@ import { DataSourceSettings as DataSourceSettingsType, GrafanaTheme2 } from '@gr
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { TestingStatus, config } from '@grafana/runtime';
import { AlertVariant, Alert, useTheme2, Link, useStyles2 } from '@grafana/ui';
+import { Trans } from 'app/core/internationalization';
import { contextSrv } from '../../../core/core';
import { trackCreateDashboardClicked } from '../tracking';
@@ -46,33 +47,77 @@ const AlertSuccessMessage = ({ title, exploreUrl, dataSourceId, onDashboardLinkC
return (
- Next, you can start to visualize data by{' '}
-
- building a dashboard
-
- , or by querying data in the{' '}
-
- Explore view
-
- .
+
+ Next, you can start to visualize data by{' '}
+
+ building a dashboard
+
+ , or by querying data in the{' '}
+
+ Explore view
+
+ .
+