diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index f0f12cce1b6..b80fa49f815 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -68,6 +68,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `tabularNumbers` | Use fixed-width numbers globally in the UI | | | `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | Yes | | `tempoSearchBackendMigration` | Run search queries through the tempo backend | | +| `opentsdbBackendMigration` | Run queries through the data source backend | | ## Public preview feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 33680ca348d..f95bb2d4356 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1190,6 +1190,11 @@ export interface FeatureToggles { */ transformationsEmptyPlaceholder?: boolean; /** + * Run queries through the data source backend + * @default false + */ + opentsdbBackendMigration?: boolean; + /** * Enable TTL plugin instance manager */ ttlPluginInstanceManager?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index bda80ace800..b121f58cb42 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1961,6 +1961,14 @@ var ( FrontendOnly: true, Owner: grafanaDataProSquad, }, + { + Name: "opentsdbBackendMigration", + Description: "Run queries through the data source backend", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaOSSBigTent, + Expression: "false", + RequiresRestart: true, + }, { Name: "ttlPluginInstanceManager", Description: "Enable TTL plugin instance manager", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b8323314f48..1287a38870e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -266,6 +266,7 @@ panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true +opentsdbBackendMigration,GA,@grafana/oss-big-tent,false,true,false ttlPluginInstanceManager,experimental,@grafana/plugins-platform-backend,false,false,true lokiQueryLimitsContext,experimental,@grafana/observability-logs,false,false,true rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 3183c587efd..afc599d4eb8 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -762,6 +762,10 @@ const ( // Enables http proxy settings for aws datasources FlagAwsDatasourcesHttpProxy = "awsDatasourcesHttpProxy" + // FlagOpentsdbBackendMigration + // Run queries through the data source backend + FlagOpentsdbBackendMigration = "opentsdbBackendMigration" + // FlagKubernetesAlertingHistorian // Adds support for Kubernetes alerting historian APIs FlagKubernetesAlertingHistorian = "kubernetesAlertingHistorian" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 035edaa9a8d..ec1bce7682b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2499,6 +2499,20 @@ "expression": "true" } }, + { + "metadata": { + "name": "opentsdbBackendMigration", + "resourceVersion": "1763456634837", + "creationTimestamp": "2025-11-18T09:03:54Z" + }, + "spec": { + "description": "Run queries through the data source backend", + "stage": "GA", + "codeowner": "@grafana/oss-big-tent", + "requiresRestart": true, + "expression": "false" + } + }, { "metadata": { "name": "otelLogsFormatting", diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index df614c3f456..8b34b21cbaf 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -94,6 +94,66 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins } } +func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + logger := logger.FromContext(ctx) + + dsInfo, err := s.getDSInfo(ctx, req.PluginContext) + if err != nil { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: err.Error(), + }, nil + } + + u, err := url.Parse(dsInfo.URL) + if err != nil { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: err.Error(), + }, nil + } + + u.Path = path.Join(u.Path, "api/suggest") + query := u.Query() + query.Set("q", "cpu") + query.Set("type", "metrics") + u.RawQuery = query.Encode() + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: err.Error(), + }, nil + } + + res, err := dsInfo.HTTPClient.Do(httpReq) + if err != nil { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: err.Error(), + }, nil + } + + defer func() { + if err := res.Body.Close(); err != nil { + logger.Error("Failed to close response body", "error", err) + } + }() + + if res.StatusCode != 200 { + return &backend.CheckHealthResult{ + Status: backend.HealthStatusError, + Message: fmt.Sprintf("OpenTSDB suggest endpoint returned status %d", res.StatusCode), + }, nil + } + + return &backend.CheckHealthResult{ + Status: backend.HealthStatusOk, + Message: "Data source is working", + }, nil +} + func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { logger := logger.FromContext(ctx) diff --git a/pkg/tsdb/opentsdb/opentsdb_test.go b/pkg/tsdb/opentsdb/opentsdb_test.go index cef8a003301..b959e9efa26 100644 --- a/pkg/tsdb/opentsdb/opentsdb_test.go +++ b/pkg/tsdb/opentsdb/opentsdb_test.go @@ -18,6 +18,58 @@ import ( "github.com/stretchr/testify/require" ) +func TestCheckHealth(t *testing.T) { + tests := []struct { + name string + httpStatusCode int + expectedStatus backend.HealthStatus + expectedMessage string + }{ + { + name: "successful health check", + httpStatusCode: 200, + expectedStatus: backend.HealthStatusOk, + expectedMessage: "Data source is working", + }, + { + name: "http error", + httpStatusCode: 500, + expectedStatus: backend.HealthStatusError, + expectedMessage: "OpenTSDB suggest endpoint returned status 500", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/suggest", r.URL.Path) + assert.Equal(t, "cpu", r.URL.Query().Get("q")) + assert.Equal(t, "metrics", r.URL.Query().Get("type")) + w.WriteHeader(tt.httpStatusCode) + })) + defer server.Close() + + pluginCtx := backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + URL: server.URL, + JSONData: []byte(`{}`), + }, + } + + im := datasource.NewInstanceManager(newInstanceSettings(httpclient.NewProvider())) + service := &Service{im: im} + ctx := backend.WithPluginContext(context.Background(), pluginCtx) + result, err := service.CheckHealth(ctx, &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + }) + + assert.NoError(t, err) + assert.Equal(t, tt.expectedStatus, result.Status) + assert.Contains(t, result.Message, tt.expectedMessage) + }) + } +} + func TestOpenTsdbExecutor(t *testing.T) { service := &Service{} diff --git a/pkg/tsdb/opentsdb/standalone/datasource.go b/pkg/tsdb/opentsdb/standalone/datasource.go index 7ca315aabb5..c2eacaf1d53 100644 --- a/pkg/tsdb/opentsdb/standalone/datasource.go +++ b/pkg/tsdb/opentsdb/standalone/datasource.go @@ -10,7 +10,8 @@ import ( ) var ( - _ backend.QueryDataHandler = (*Datasource)(nil) + _ backend.QueryDataHandler = (*Datasource)(nil) + _ backend.CheckHealthHandler = (*Datasource)(nil) ) type Datasource struct { @@ -26,3 +27,7 @@ func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instanc func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { return d.Service.QueryData(ctx, req) } + +func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + return d.Service.CheckHealth(ctx, req) +} diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts index 2b70dfb605c..14c24b5c34d 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.ts +++ b/public/app/plugins/datasource/opentsdb/datasource.ts @@ -20,19 +20,25 @@ import { AnnotationEvent, DataQueryRequest, DataQueryResponse, - DataSourceApi, dateMath, DateTime, ScopedVars, toDataFrame, } from '@grafana/data'; -import { FetchResponse, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; +import { + config, + DataSourceWithBackend, + FetchResponse, + getBackendSrv, + getTemplateSrv, + TemplateSrv, +} from '@grafana/runtime'; import { AnnotationEditor } from './components/AnnotationEditor'; import { prepareAnnotation } from './migrations'; import { OpenTsdbFilter, OpenTsdbOptions, OpenTsdbQuery } from './types'; -export default class OpenTsDatasource extends DataSourceApi { +export default class OpenTsDatasource extends DataSourceWithBackend { type: 'opentsdb'; url: string; name: string; @@ -397,7 +403,11 @@ export default class OpenTsDatasource extends DataSourceApi {