From c25fe34ac0eb65e975d88dfcd188dcc7d6701e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Mon, 19 Dec 2022 17:17:52 +0100 Subject: [PATCH] SQL Datasources: Use health check for config test (#59867) * SQL Datasources: Use health check for config test * Remove unnecessary test * Fix test errors * Revert mysql go driver update * Use transform query error * Use TransformQueryError from sql_engine --- pkg/tsdb/mssql/mssql.go | 16 +++++++ pkg/tsdb/mysql/mysql.go | 19 ++++++++ pkg/tsdb/postgres/postgres.go | 17 +++++++ pkg/tsdb/sqleng/sql_engine.go | 12 +++-- pkg/tsdb/sqleng/sql_engine_test.go | 2 +- .../plugins/sql/datasource/SqlDatasource.ts | 45 ++----------------- .../datasource/postgres/datasource.test.ts | 40 +---------------- 7 files changed, 65 insertions(+), 86 deletions(-) diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index bf1bec7cfcb..47d2bae6a53 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -199,6 +199,22 @@ func (t *mssqlQueryResultTransformer) TransformQueryError(logger log.Logger, err return err } +// CheckHealth pings the connected SQL database +func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + dsHandler, err := s.getDataSourceHandler(req.PluginContext) + if err != nil { + return nil, err + } + + err = dsHandler.Ping() + + if err != nil { + return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: dsHandler.TransformQueryError(logger, err).Error()}, nil + } + + return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil +} + func (t *mssqlQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { return []sqlutil.StringConverter{ { diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index da84655ac93..8d39e7d6447 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -140,6 +140,25 @@ func (s *Service) getDataSourceHandler(pluginCtx backend.PluginContext) (*sqleng return instance, nil } +// CheckHealth pings the connected SQL database +func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + dsHandler, err := s.getDataSourceHandler(req.PluginContext) + if err != nil { + return nil, err + } + + err = dsHandler.Ping() + + if err != nil { + var driverErr *mysql.MySQLError + if errors.As(err, &driverErr) { + return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: dsHandler.TransformQueryError(logger, driverErr).Error()}, nil + } + return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: dsHandler.TransformQueryError(logger, err).Error()}, nil + } + return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil +} + func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { dsHandler, err := s.getDataSourceHandler(req.PluginContext) if err != nil { diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 0a3defa6acf..2dcdff54816 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -196,6 +196,23 @@ func (t *postgresQueryResultTransformer) TransformQueryError(_ log.Logger, err e return err } +// CheckHealth pings the connected SQL database +func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + dsHandler, err := s.getDSInfo(req.PluginContext) + if err != nil { + return nil, err + } + + err = dsHandler.Ping() + + if err != nil { + logger.Error("Check health failed", "error", err) + return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: dsHandler.TransformQueryError(logger, err).Error()}, nil + } + + return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil +} + func (t *postgresQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { return []sqlutil.StringConverter{ { diff --git a/pkg/tsdb/sqleng/sql_engine.go b/pkg/tsdb/sqleng/sql_engine.go index b5d766de214..70c679e225e 100644 --- a/pkg/tsdb/sqleng/sql_engine.go +++ b/pkg/tsdb/sqleng/sql_engine.go @@ -107,7 +107,7 @@ type QueryJson struct { Format string `json:"format"` } -func (e *DataSourceHandler) transformQueryError(logger log.Logger, err error) error { +func (e *DataSourceHandler) TransformQueryError(logger log.Logger, err error) error { // OpError is the error type usually returned by functions in the net // package. It describes the operation, network type, and address of // an error. We log this error rather than return it to the client @@ -173,6 +173,10 @@ func (e *DataSourceHandler) Dispose() { e.log.Debug("Engine disposed") } +func (e *DataSourceHandler) Ping() error { + return e.engine.Ping() +} + func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() ch := make(chan DBDataResponse, len(req.Queries)) @@ -250,14 +254,14 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG // global substitutions interpolatedQuery, err := Interpolate(query, timeRange, e.dsInfo.JsonData.TimeInterval, queryJson.RawSql) if err != nil { - errAppendDebug("interpolation failed", e.transformQueryError(logger, err), interpolatedQuery) + errAppendDebug("interpolation failed", e.TransformQueryError(logger, err), interpolatedQuery) return } // data source specific substitutions interpolatedQuery, err = e.macroEngine.Interpolate(&query, timeRange, interpolatedQuery) if err != nil { - errAppendDebug("interpolation failed", e.transformQueryError(logger, err), interpolatedQuery) + errAppendDebug("interpolation failed", e.TransformQueryError(logger, err), interpolatedQuery) return } @@ -267,7 +271,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG rows, err := db.QueryContext(queryContext, interpolatedQuery) if err != nil { - errAppendDebug("db query error", e.transformQueryError(logger, err), interpolatedQuery) + errAppendDebug("db query error", e.TransformQueryError(logger, err), interpolatedQuery) return } defer func() { diff --git a/pkg/tsdb/sqleng/sql_engine_test.go b/pkg/tsdb/sqleng/sql_engine_test.go index 76ca04c9d3f..81c4db1706b 100644 --- a/pkg/tsdb/sqleng/sql_engine_test.go +++ b/pkg/tsdb/sqleng/sql_engine_test.go @@ -407,7 +407,7 @@ func TestSQLEngine(t *testing.T) { log: log.New("test"), queryResultTransformer: transformer, } - resultErr := dp.transformQueryError(dp.log, tc.err) + resultErr := dp.TransformQueryError(dp.log, tc.err) assert.ErrorIs(t, resultErr, tc.expectedErr) assert.Equal(t, tc.expectQueryResultTransformerWasCalled, transformer.transformQueryErrorWasCalled) } diff --git a/public/app/features/plugins/sql/datasource/SqlDatasource.ts b/public/app/features/plugins/sql/datasource/SqlDatasource.ts index eda97b704f3..1128826b9cd 100644 --- a/public/app/features/plugins/sql/datasource/SqlDatasource.ts +++ b/public/app/features/plugins/sql/datasource/SqlDatasource.ts @@ -1,5 +1,5 @@ -import { lastValueFrom, of } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; +import { lastValueFrom } from 'rxjs'; +import { map } from 'rxjs/operators'; import { DataFrame, @@ -19,7 +19,7 @@ import { getTemplateSrv, TemplateSrv, } from '@grafana/runtime'; -import { toDataQueryResponse, toTestingStatus } from '@grafana/runtime/src/utils/queryResponse'; +import { toDataQueryResponse } from '@grafana/runtime/src/utils/queryResponse'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { VariableWithMultiSupport } from '../../../variables/types'; @@ -170,45 +170,6 @@ export abstract class SqlDatasource extends DataSourceWithBackend { - const refId = 'A'; - return lastValueFrom( - getBackendSrv() - .fetch({ - url: '/api/ds/query', - method: 'POST', - headers: this.getRequestHeaders(), - data: { - from: '5m', - to: 'now', - queries: [ - { - refId: refId, - intervalMs: 1, - maxDataPoints: 1, - datasource: this.getRef(), - datasourceId: this.id, - rawSql: 'SELECT 1', - format: 'table', - }, - ], - }, - }) - .pipe( - map((r) => { - const error = r.data.results[refId].error; - if (error) { - return { status: 'error', message: error }; - } - return { status: 'success', message: 'Database Connection OK' }; - }), - catchError((err) => { - return of(toTestingStatus(err)); - }) - ) - ); - } - targetContainsTemplate(target: SQLQuery) { let queryWithoutMacros = target.rawSql; MACRO_NAMES.forEach((value) => { diff --git a/public/app/plugins/datasource/postgres/datasource.test.ts b/public/app/plugins/datasource/postgres/datasource.test.ts index a609b93b007..e8b8eef1250 100644 --- a/public/app/plugins/datasource/postgres/datasource.test.ts +++ b/public/app/plugins/datasource/postgres/datasource.test.ts @@ -1,4 +1,4 @@ -import { Observable, of, throwError } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { @@ -78,44 +78,6 @@ describe('PostgreSQLDatasource', () => { }); }; - describe('when performing testDatasource call', () => { - it('should return the error from the server', async () => { - setupTestContext( - undefined, - throwError(() => ({ - status: 400, - statusText: 'Bad Request', - data: { - results: { - meta: { - error: 'db query error: pq: password authentication failed for user "postgres"', - frames: [ - { - schema: { - refId: 'meta', - meta: { - executedQueryString: 'SELECT 1', - }, - fields: [], - }, - data: { - values: [], - }, - }, - ], - }, - }, - }, - })) - ); - - const ds = new PostgresDatasource({ name: '', id: 0 } as DataSourceInstanceSettings); - const result = await ds.testDatasource(); - expect(result.status).toEqual('error'); - expect(result.message).toEqual('db query error: pq: password authentication failed for user "postgres"'); - }); - }); - describe('When performing a time series query', () => { it('should transform response correctly', () => { const options: DataQueryRequest = {