Loki: Decouple data source plugin (#107242)

* WIP

* Update yarn.lock

* Align uuid dependency

* Add e2e test and update

* Update cue version

* Fix lint

* Update snapshot test

* Fix test that was importing from coupled module

* Fix lint

* Update public/app/plugins/datasource/loki/package.json

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>

---------

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
Ivana Huckova
2025-06-27 15:09:45 +02:00
committed by GitHub
parent f4ee58db50
commit f7eab21f9f
17 changed files with 280 additions and 49 deletions

View File

@ -101,6 +101,8 @@ linters:
- '**/pkg/tsdb/tempo/**/*'
- '**/pkg/tsdb/cloudwatch/*'
- '**/pkg/tsdb/cloudwatch/**/*'
- '**/pkg/tsdb/loki/*'
- '**/pkg/tsdb/loki/**/*'
deny:
- pkg: github.com/grafana/grafana/pkg/api
desc: Core plugins are not allowed to depend on Grafana core packages

View File

@ -0,0 +1,8 @@
import { test, expect } from '@grafana/plugin-e2e';
test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => {
await createDataSourceConfigPage({ type: 'loki' });
await expect(await page.getByText('Type: Loki', { exact: true })).toBeVisible();
await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible();
});

View File

@ -10,7 +10,7 @@
import * as common from '@grafana/schema';
export const pluginVersion = "12.1.0-pre";
export const pluginVersion = "%VERSION%";
export enum QueryEditorMode {
Builder = 'builder',

View File

@ -209,7 +209,7 @@
"path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_cpu.png"
}
],
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": [
"azure",
@ -880,7 +880,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},
@ -934,7 +934,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": [
"grafana",
@ -1217,7 +1217,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},
@ -1325,12 +1325,12 @@
},
"build": {},
"screenshots": null,
"version": "",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},
"dependencies": {
"grafanaDependency": "",
"grafanaDependency": "\u003e=10.4.0",
"grafanaVersion": "*",
"plugins": [],
"extensions": {
@ -1375,7 +1375,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},
@ -1425,7 +1425,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},
@ -1629,7 +1629,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": [
"grafana",
@ -1734,7 +1734,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},
@ -2042,7 +2042,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},
@ -2092,7 +2092,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},
@ -2445,7 +2445,7 @@
},
"build": {},
"screenshots": null,
"version": "11.6.0-pre",
"version": "12.1.0-pre",
"updated": "",
"keywords": null
},

View File

@ -0,0 +1,40 @@
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"
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
loki "github.com/grafana/grafana/pkg/tsdb/loki"
)
var (
_ backend.QueryDataHandler = (*Datasource)(nil)
_ backend.CheckHealthHandler = (*Datasource)(nil)
_ backend.CallResourceHandler = (*Datasource)(nil)
)
func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return &Datasource{
Service: loki.ProvideService(httpclient.NewProvider(), tracing.DefaultTracer()),
}, nil
}
type Datasource struct {
Service *loki.Service
}
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return d.Service.QueryData(ctx, req)
}
func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return d.Service.CallResource(ctx, req, sender)
}
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
return d.Service.CheckHealth(ctx, req)
}

View File

@ -0,0 +1,23 @@
package main
import (
"os"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)
func main() {
// Start listening to requests sent from Grafana. This call is blocking so
// it won't finish until Grafana shuts down the process or the plugin choose
// to exit by itself using os.Exit. Manage automatically manages life cycle
// of datasource instances. It accepts datasource instance factory as first
// argument. This factory will be automatically called on incoming request
// from Grafana to create different instances of SampleDatasource (per datasource
// ID). When datasource configuration changed Dispose method will be called and
// new datasource instance created using NewSampleDatasource factory.
if err := datasource.Manage("loki", NewDatasource, datasource.ManageOpts{}); err != nil {
log.DefaultLogger.Error(err.Error())
os.Exit(1)
}
}

View File

@ -187,5 +187,14 @@ export default defineConfig<PluginOptions>({
},
dependencies: ['authenticate'],
},
{
name: 'loki',
testDir: path.join(testDirRoot, '/loki'),
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['authenticate'],
},
],
});

View File

@ -4,14 +4,21 @@ import { merge, uniqueId } from 'lodash';
import { openMenu } from 'react-select-event';
import { Observable } from 'rxjs';
import { TestProvider } from 'test/helpers/TestProvider';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { DataSourceInstanceSettings, SupportedTransformationType } from '@grafana/data';
import { SupportedTransformationType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { BackendSrv, BackendSrvRequest, reportInteraction, setBackendSrv, setAppEvents } from '@grafana/runtime';
import {
BackendSrv,
BackendSrvRequest,
DataSourceSrv,
reportInteraction,
setAppEvents,
setDataSourceSrv,
} from '@grafana/runtime';
import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/services/context_srv';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { configureStore } from 'app/store/configureStore';
import { mockDataSource } from '../alerting/unified/mocks';
@ -23,6 +30,7 @@ import {
createFetchCorrelationsResponse,
createRemoveCorrelationResponse,
createUpdateCorrelationResponse,
MockDataSourceSrv,
} from './__mocks__/useCorrelations.mocks';
import { Correlation, CreateCorrelationParams, OmitUnion } from './types';
@ -30,7 +38,7 @@ import { Correlation, CreateCorrelationParams, OmitUnion } from './types';
setAppEvents(appEvents);
const renderWithContext = async (
datasources: Record<string, DataSourceInstanceSettings>,
datasources: ConstructorParameters<typeof MockDataSourceSrv>[0] = {},
correlations: Correlation[] = []
) => {
const backend = {
@ -90,8 +98,17 @@ const renderWithContext = async (
},
} as unknown as BackendSrv;
const grafanaContext = getGrafanaContextMock({ backend });
setBackendSrv(backend);
setupDataSources(...Object.values(datasources));
const dsServer = new MockDataSourceSrv(datasources) as unknown as DataSourceSrv;
dsServer.get = (name: string) => {
const dsApi = new MockDataSourceApi(name);
// Mock the QueryEditor component
dsApi.components = {
QueryEditor: () => <>{name} query editor</>,
};
return Promise.resolve(dsApi);
};
setDataSourceSrv(dsServer);
const renderResult = render(
<TestProvider store={configureStore({})} grafanaContext={grafanaContext}>
@ -207,7 +224,7 @@ describe('CorrelationsPage', () => {
jsonData: {},
type: 'datasource',
},
{ logs: true, module: 'core:plugin/loki' }
{ logs: true }
),
prometheus: mockDataSource(
{
@ -316,7 +333,6 @@ describe('CorrelationsPage', () => {
},
{
logs: true,
module: 'core:plugin/loki',
}
),
prometheus: mockDataSource(
@ -582,7 +598,6 @@ describe('CorrelationsPage', () => {
},
{
logs: true,
module: 'core:plugin/loki',
}
),
},
@ -678,7 +693,7 @@ describe('CorrelationsPage', () => {
access: 'direct',
type: 'datasource',
},
{ logs: true, module: 'core:plugin/loki' }
{ logs: true }
),
},
correlations

View File

@ -1,6 +1,8 @@
import { merge } from 'lodash';
import { DeepPartial } from 'react-hook-form';
import { DatasourceSrvMock } from 'test/mocks/datasource_srv';
import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data';
import { FetchError, FetchResponse } from '@grafana/runtime';
import { Correlation, CreateCorrelationResponse, RemoveCorrelationResponse, UpdateCorrelationResponse } from '../types';
@ -56,3 +58,17 @@ export function createRemoveCorrelationResponse(): RemoveCorrelationResponse {
message: 'Correlation removed',
};
}
export class MockDataSourceSrv extends DatasourceSrvMock {
private ds: DataSourceInstanceSettings[];
constructor(datasources: Record<string, DataSourceInstanceSettings>) {
super({} as DataSourceApi, {});
this.ds = Object.values(datasources);
}
getList(): DataSourceInstanceSettings[] {
return this.ds;
}
getInstanceSettings(name?: string): DataSourceInstanceSettings | undefined {
return name ? this.ds.find((ds) => ds.name === name) : undefined;
}
}

View File

@ -14,7 +14,6 @@ const grafanaPlugin = async () =>
await import(/* webpackChunkName: "grafanaPlugin" */ 'app/plugins/datasource/grafana/module');
const influxdbPlugin = async () =>
await import(/* webpackChunkName: "influxdbPlugin" */ 'app/plugins/datasource/influxdb/module');
const lokiPlugin = async () => await import(/* webpackChunkName: "lokiPlugin" */ 'app/plugins/datasource/loki/module');
const mixedPlugin = async () =>
await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module');
const prometheusPlugin = async () =>
@ -90,7 +89,6 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
'core:plugin/opentsdb': opentsdbPlugin,
'core:plugin/grafana': grafanaPlugin,
'core:plugin/influxdb': influxdbPlugin,
'core:plugin/loki': lokiPlugin,
'core:plugin/mixed': mixedPlugin,
'core:plugin/prometheus': prometheusPlugin,
'core:plugin/alertmanager': alertmanagerPlugin,

View File

@ -0,0 +1 @@
# Changelog

View File

@ -0,0 +1,52 @@
{
"name": "@grafana-plugins/loki",
"description": "Loki data source plugin for Grafana",
"private": true,
"version": "12.1.0-pre",
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "12.1.0-pre",
"@grafana/lezer-logql": "0.2.7",
"@grafana/llm": "0.22.1",
"@grafana/monaco-logql": "^0.0.8",
"@grafana/runtime": "12.1.0-pre",
"@grafana/schema": "12.1.0-pre",
"@grafana/ui": "12.1.0-pre",
"d3-random": "^3.0.1",
"lodash": "4.17.21",
"micro-memoize": "^4.1.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-select": "5.10.1",
"react-use": "17.6.0",
"rxjs": "7.8.2",
"tslib": "2.8.1",
"uuid": "11.1.0"
},
"devDependencies": {
"@grafana/e2e-selectors": "12.1.0-pre",
"@grafana/plugin-configs": "12.1.0-pre",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
"@types/d3-random": "^3.0.2",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.15",
"@types/node": "22.15.0",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/uuid": "10.0.0",
"ts-node": "10.9.2",
"typescript": "5.7.3",
"webpack": "5.97.1"
},
"peerDependencies": {
"@grafana/runtime": "*"
},
"scripts": {
"build": "webpack -c ./webpack.config.ts --env production",
"build:commit": "webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)",
"dev": "webpack -w -c ./webpack.config.ts --env development"
},
"packageManager": "yarn@4.9.2"
}

View File

@ -14,7 +14,7 @@
"queryOptions": {
"maxDataPoints": true
},
"executable": "gpx_loki",
"info": {
"description": "Like Prometheus but for logs. OSS logging solution from Grafana Labs",
"author": {
@ -42,6 +42,11 @@
"name": "Documentation",
"url": "https://grafana.com/docs/grafana/latest/datasources/loki/"
}
]
],
"version": "%VERSION%"
},
"dependencies": {
"grafanaDependency": ">=10.4.0",
"plugins": []
}
}

View File

@ -0,0 +1,9 @@
{
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"tags": ["scope:plugin", "type:datasource"],
"targets": {
"build": {},
"dev": {}
}
}

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"jsx": "react-jsx"
},
"extends": "@grafana/plugin-configs/tsconfig.json",
"include": ["."]
}

View File

@ -0,0 +1,4 @@
import config from '@grafana/plugin-configs/webpack.config';
// eslint-disable-next-line no-barrel-files/no-barrel-files
export default config;

View File

@ -2713,6 +2713,48 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana-plugins/loki@workspace:public/app/plugins/datasource/loki":
version: 0.0.0-use.local
resolution: "@grafana-plugins/loki@workspace:public/app/plugins/datasource/loki"
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:12.1.0-pre"
"@grafana/e2e-selectors": "npm:12.1.0-pre"
"@grafana/lezer-logql": "npm:0.2.7"
"@grafana/llm": "npm:0.22.1"
"@grafana/monaco-logql": "npm:^0.0.8"
"@grafana/plugin-configs": "npm:12.1.0-pre"
"@grafana/runtime": "npm:12.1.0-pre"
"@grafana/schema": "npm:12.1.0-pre"
"@grafana/ui": "npm:12.1.0-pre"
"@testing-library/dom": "npm:10.4.0"
"@testing-library/react": "npm:16.2.0"
"@testing-library/user-event": "npm:14.6.1"
"@types/d3-random": "npm:^3.0.2"
"@types/jest": "npm:29.5.14"
"@types/lodash": "npm:4.17.15"
"@types/node": "npm:22.15.0"
"@types/react": "npm:18.3.18"
"@types/react-dom": "npm:18.3.5"
"@types/uuid": "npm:10.0.0"
d3-random: "npm:^3.0.1"
lodash: "npm:4.17.21"
micro-memoize: "npm:^4.1.2"
react: "npm:18.3.1"
react-dom: "npm:18.3.1"
react-select: "npm:5.10.1"
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.7.3"
uuid: "npm:11.1.0"
webpack: "npm:5.97.1"
peerDependencies:
"@grafana/runtime": "*"
languageName: unknown
linkType: soft
"@grafana-plugins/mssql@workspace:public/app/plugins/datasource/mssql":
version: 0.0.0-use.local
resolution: "@grafana-plugins/mssql@workspace:public/app/plugins/datasource/mssql"