decouple the opentsdb data source from core (#113588)

* enable linting rules for opentsdb

* remove core imports

* update plugin.json

* write backend standalone files

* remove frontend core imports

* add yarn workspace

* remove core import for the plugin

* update grafana dependency

* update package.json

* add jest config
This commit is contained in:
Gareth
2025-11-12 22:33:17 +09:00
committed by GitHub
parent ed19a92a2a
commit 4e050267c7
19 changed files with 235 additions and 51 deletions
+2
View File
@@ -105,6 +105,8 @@ linters:
- '**/pkg/tsdb/graphite/**/*'
- '**/pkg/tsdb/mysql/*'
- '**/pkg/tsdb/mysql/**/*'
- '**/pkg/tsdb/opentsdb/*'
- '**/pkg/tsdb/opentsdb/**/*'
- '**/pkg/tsdb/parca/*'
- '**/pkg/tsdb/parca/**/*'
- '**/pkg/tsdb/tempo/*'
+1
View File
@@ -434,6 +434,7 @@ module.exports = [
'public/app/plugins/datasource/loki/**/*.{ts,tsx}',
'public/app/plugins/datasource/loki/**/*.{ts,tsx}',
'public/app/plugins/datasource/mysql/**/*.{ts,tsx}',
'public/app/plugins/datasource/opentsdb/**/*.{ts,tsx}',
'public/app/plugins/datasource/parca/**/*.{ts,tsx}',
'public/app/plugins/datasource/tempo/**/*.{ts,tsx}',
'public/app/plugins/datasource/zipkin/**/*.{ts,tsx}',
@@ -1580,7 +1580,7 @@
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
"grafanaDependency": "\u003e=10.3.0-0",
"grafanaVersion": "*",
"plugins": [],
"extensions": {
+40 -37
View File
@@ -15,22 +15,19 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
)
var logger = log.New("tsdb.opentsdb")
var logger = backend.NewLoggerWith("tsdb.opentsdb")
type Service struct {
im instancemgmt.InstanceManager
}
func ProvideService(httpClientProvider httpclient.Provider) *Service {
func ProvideService(httpClientProvider *httpclient.Provider) *Service {
return &Service{
im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
}
@@ -52,7 +49,22 @@ type JSONData struct {
LookupLimit int32 `json:"lookupLimit"`
}
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
type QueryModel struct {
Metric string `json:"metric"`
Aggregator string `json:"aggregator"`
DownsampleInterval string `json:"downsampleInterval"`
DownsampleAggregator string `json:"downsampleAggregator"`
DownsampleFillPolicy string `json:"downsampleFillPolicy"`
DisableDownsampling bool `json:"disableDownsampling"`
Filters []any `json:"filters"`
Tags map[string]interface{} `json:"tags"`
ShouldComputeRate bool `json:"shouldComputeRate"`
IsCounter bool `json:"isCounter"`
CounterMax float64 `json:"counterMax"`
CounterResetValue float64 `json:"counterResetValue"`
}
func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
opts, err := settings.HTTPClientOptions(ctx)
if err != nil {
@@ -102,10 +114,6 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
},
}
if setting.Env == setting.Dev {
logger.Debug("OpenTsdb request", "refId", query.RefID, "params", tsdbQuery)
}
httpReq, err := s.createRequest(ctx, logger, dsInfo, tsdbQuery)
if err != nil {
return nil, err
@@ -274,47 +282,44 @@ func (s *Service) parseResponse(logger log.Logger, res *http.Response, refID str
func (s *Service) buildMetric(query backend.DataQuery) map[string]any {
metric := make(map[string]any)
model, err := simplejson.NewJson(query.JSON)
if err != nil {
var model QueryModel
if err := json.Unmarshal(query.JSON, &model); err != nil {
return nil
}
// Setting metric and aggregator
metric["metric"] = model.Get("metric").MustString()
metric["aggregator"] = model.Get("aggregator").MustString()
metric["metric"] = model.Metric
metric["aggregator"] = model.Aggregator
// Setting downsampling options
disableDownsampling := model.Get("disableDownsampling").MustBool()
if !disableDownsampling {
downsampleInterval := model.Get("downsampleInterval").MustString()
if !model.DisableDownsampling {
downsampleInterval := model.DownsampleInterval
if downsampleInterval == "" {
downsampleInterval = "1m" // default value for blank
}
downsample := downsampleInterval + "-" + model.Get("downsampleAggregator").MustString()
if model.Get("downsampleFillPolicy").MustString() != "none" {
metric["downsample"] = downsample + "-" + model.Get("downsampleFillPolicy").MustString()
downsample := downsampleInterval + "-" + model.DownsampleAggregator
if model.DownsampleFillPolicy != "none" {
metric["downsample"] = downsample + "-" + model.DownsampleFillPolicy
} else {
metric["downsample"] = downsample
}
}
// Setting rate options
if model.Get("shouldComputeRate").MustBool() {
if model.ShouldComputeRate {
metric["rate"] = true
rateOptions := make(map[string]any)
rateOptions["counter"] = model.Get("isCounter").MustBool()
rateOptions["counter"] = model.IsCounter
counterMax, counterMaxCheck := model.CheckGet("counterMax")
if counterMaxCheck {
rateOptions["counterMax"] = counterMax.MustFloat64()
if model.CounterMax != 0 {
rateOptions["counterMax"] = model.CounterMax
}
resetValue, resetValueCheck := model.CheckGet("counterResetValue")
if resetValueCheck {
rateOptions["resetValue"] = resetValue.MustFloat64()
if model.CounterResetValue != 0 {
rateOptions["resetValue"] = model.CounterResetValue
}
if !counterMaxCheck && (!resetValueCheck || resetValue.MustFloat64() == 0) {
if model.CounterMax == 0 && (model.CounterResetValue == 0) {
rateOptions["dropResets"] = true
}
@@ -322,15 +327,13 @@ func (s *Service) buildMetric(query backend.DataQuery) map[string]any {
}
// Setting tags
tags, tagsCheck := model.CheckGet("tags")
if tagsCheck && len(tags.MustMap()) > 0 {
metric["tags"] = tags.MustMap()
if len(model.Tags) > 0 {
metric["tags"] = model.Tags
}
// Setting filters
filters, filtersCheck := model.CheckGet("filters")
if filtersCheck && len(filters.MustArray()) > 0 {
metric["filters"] = filters.MustArray()
if len(model.Filters) > 0 {
metric["filters"] = model.Filters
}
return metric
+1 -1
View File
@@ -12,8 +12,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -0,0 +1,28 @@
package main
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
opentsdb "github.com/grafana/grafana/pkg/tsdb/opentsdb"
)
var (
_ backend.QueryDataHandler = (*Datasource)(nil)
)
type Datasource struct {
Service *opentsdb.Service
}
func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return &Datasource{
Service: opentsdb.ProvideService(httpclient.NewProvider()),
}, nil
}
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return d.Service.QueryData(ctx, req)
}
+15
View File
@@ -0,0 +1,15 @@
package main
import (
"os"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)
func main() {
if err := datasource.Manage("opentsdb", NewDatasource, datasource.ManageOpts{}); err != nil {
log.DefaultLogger.Error(err.Error())
os.Exit(1)
}
}
@@ -6,8 +6,6 @@ const dashboardDSPlugin = async () =>
await import(/* webpackChunkName "dashboardDSPlugin" */ 'app/plugins/datasource/dashboard/module');
const elasticsearchPlugin = async () =>
await import(/* webpackChunkName: "elasticsearchPlugin" */ 'app/plugins/datasource/elasticsearch/module');
const opentsdbPlugin = async () =>
await import(/* webpackChunkName: "opentsdbPlugin" */ 'app/plugins/datasource/opentsdb/module');
const grafanaPlugin = async () =>
await import(/* webpackChunkName: "grafanaPlugin" */ 'app/plugins/datasource/grafana/module');
const influxdbPlugin = async () =>
@@ -78,7 +76,6 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
'core:plugin/cloudwatch': cloudwatchPlugin,
'core:plugin/dashboard': dashboardDSPlugin,
'core:plugin/elasticsearch': elasticsearchPlugin,
'core:plugin/opentsdb': opentsdbPlugin,
'core:plugin/grafana': grafanaPlugin,
'core:plugin/influxdb': influxdbPlugin,
'core:plugin/mixed': mixedPlugin,
@@ -0,0 +1 @@
# Changelog
@@ -26,8 +26,7 @@ import {
ScopedVars,
toDataFrame,
} from '@grafana/data';
import { FetchResponse, getBackendSrv } from '@grafana/runtime';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import { FetchResponse, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { AnnotationEditor } from './components/AnnotationEditor';
import { prepareAnnotation } from './migrations';
@@ -0,0 +1 @@
import '@grafana/plugin-configs/jest/jest-setup';
@@ -0,0 +1,3 @@
import defaultConfig from '@grafana/plugin-configs/jest/jest.config.js';
export default defaultConfig;
@@ -0,0 +1,52 @@
{
"name": "@grafana-plugins/opentsdb",
"description": "OpenTSDB plugin for Grafana",
"private": true,
"version": "12.4.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
"debounce-promise": "3.1.2",
"lodash": "4.17.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-select": "5.10.2",
"react-use": "17.6.0",
"rxjs": "7.8.2",
"tslib": "2.8.1",
"uuid": "11.1.0"
},
"devDependencies": {
"@grafana/e2e-selectors": "workspace:*",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.6.4",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/debounce-promise": "3.1.9",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.20",
"@types/node": "22.17.0",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/uuid": "10.0.0",
"jest": "29.7.0",
"ts-node": "10.9.2",
"typescript": "5.9.2",
"webpack": "5.101.0"
},
"peerDependencies": {
"@grafana/runtime": "*"
},
"scripts": {
"build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production",
"build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)",
"dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development",
"test": "jest --watch --onlyChanged",
"test:ci": "jest --maxWorkers 4"
},
"packageManager": "yarn@4.9.4"
}
@@ -3,6 +3,7 @@
"name": "OpenTSDB",
"id": "opentsdb",
"category": "tsdb",
"executable": "gpx_opentsdb",
"metrics": true,
"defaultMatchFormat": "pipe",
@@ -23,6 +24,10 @@
"links": [
{ "name": "Raise issue", "url": "https://github.com/grafana/grafana/issues/new" },
{ "name": "Documentation", "url": "https://grafana.com/docs/grafana/latest/datasources/opentsdb/" }
]
],
"version": "%VERSION%"
},
"dependencies": {
"grafanaDependency": ">=10.3.0-0"
}
}
@@ -0,0 +1,9 @@
{
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"tags": ["scope:plugin", "type:datasource"],
"targets": {
"build": {},
"dev": {}
}
}
@@ -1,16 +1,32 @@
import { of } from 'rxjs';
import { DataQueryRequest, dateTime } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TemplateSrv } from 'app/features/templating/template_srv';
import { BackendSrv, FetchResponse, TemplateSrv } from '@grafana/runtime';
import { createFetchResponse } from '../../../../../test/helpers/createFetchResponse';
import OpenTsDatasource from '../datasource';
import { OpenTsdbQuery } from '../types';
export function createFetchResponse<T>(data: T): FetchResponse<T> {
return {
data,
status: 200,
url: 'http://localhost:3000/api/ds/query',
config: { url: 'http://localhost:3000/api/ds/query' },
type: 'basic',
statusText: 'Ok',
redirected: false,
headers: new Headers(),
ok: true,
};
}
const mockBackendSrv = {
fetch: jest.fn(),
} as unknown as BackendSrv;
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
getBackendSrv: () => mockBackendSrv,
}));
const metricFindQueryData = [
@@ -26,7 +42,7 @@ const metricFindQueryData = [
describe('opentsdb', () => {
function getTestcontext({ data = metricFindQueryData }: { data?: unknown } = {}) {
jest.clearAllMocks();
const fetchMock = jest.spyOn(backendSrv, 'fetch');
const fetchMock = jest.spyOn(mockBackendSrv, 'fetch');
fetchMock.mockImplementation(() => of(createFetchResponse(data)));
const instanceSettings = { url: '', jsonData: { tsdbVersion: 1 } };
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"types": ["node", "jest", "@testing-library/jest-dom"]
},
"extends": "@grafana/plugin-configs/tsconfig.json",
"include": ["."]
}
@@ -0,0 +1,4 @@
import config from '@grafana/plugin-configs/webpack.config.ts';
// eslint-disable-next-line no-barrel-files/no-barrel-files
export default config;
+41 -1
View File
@@ -2784,6 +2784,46 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana-plugins/opentsdb@workspace:public/app/plugins/datasource/opentsdb":
version: 0.0.0-use.local
resolution: "@grafana-plugins/opentsdb@workspace:public/app/plugins/datasource/opentsdb"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "workspace:*"
"@grafana/e2e-selectors": "workspace:*"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/schema": "workspace:*"
"@grafana/ui": "workspace:*"
"@testing-library/dom": "npm:10.4.1"
"@testing-library/jest-dom": "npm:6.6.4"
"@testing-library/react": "npm:16.3.0"
"@testing-library/user-event": "npm:14.6.1"
"@types/debounce-promise": "npm:3.1.9"
"@types/jest": "npm:29.5.14"
"@types/lodash": "npm:4.17.20"
"@types/node": "npm:22.17.0"
"@types/react": "npm:18.3.18"
"@types/react-dom": "npm:18.3.5"
"@types/uuid": "npm:10.0.0"
debounce-promise: "npm:3.1.2"
jest: "npm:29.7.0"
lodash: "npm:4.17.21"
react: "npm:18.3.1"
react-dom: "npm:18.3.1"
react-select: "npm:5.10.2"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.2"
ts-node: "npm:10.9.2"
tslib: "npm:2.8.1"
typescript: "npm:5.9.2"
uuid: "npm:11.1.0"
webpack: "npm:5.101.0"
peerDependencies:
"@grafana/runtime": "*"
languageName: unknown
linkType: soft
"@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca":
version: 0.0.0-use.local
resolution: "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca"
@@ -10234,7 +10274,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4":
"@types/node@npm:*, @types/node@npm:22.17.0, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=13.7.4":
version: 22.17.0
resolution: "@types/node@npm:22.17.0"
dependencies: