Compare commits

..

6 Commits

Author SHA1 Message Date
Rafael Paulovic
b0bb71f834 fix: wire 2026-01-14 19:23:10 +01:00
mayor
a7aa55f908 Add search-server target and configurable search mode
- Add SearchServer module target for standalone search service
- Add search_mode config: "", "embedded" (default), "remote"
- Add search_server_address config for remote search server
- Create remote_search.go: gRPC client wrapper for remote search
- Create search_service.go: standalone search gRPC service
- Modify service.go: conditional search mode handling
- Backward compatible: empty/embedded mode works as before

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:49:03 +01:00
mayor
0a61846b5e Initialize backend before search server
The backend's Init() must be called before search.Init() because
buildIndexes() calls storage.GetResourceStats() which requires
the database connection to be established.

Previously, backend.Init() was called by ResourceServer.Init()
through the Lifecycle hooks, but now search.Init() runs before
ResourceServer is created.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:58:30 +01:00
mayor
785cc739ee Fix nil pointer in NewSearchServer - use serverOptions.Backend
The backend may be created inside the function and assigned to
serverOptions.Backend, not opts.Backend. Using opts.Backend caused
a nil pointer dereference in searchSupport.buildIndexes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:22:42 +01:00
mayor
4913baaf04 Fix compilation errors from SearchServer extraction
- Update NewLocalResourceClient to accept SearchServer parameter
- Add SearchClient methods to ResourceClient interface (client needs both)
- Fix directResourceClient to implement new interface with stub methods
- Update search_and_storage.go test to create SearchServer separately

This continues the work of extracting SearchServer from ResourceServer
to enable independent usage by storage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 17:59:46 +01:00
Peter Štibraný
c8f1efe7c7 Initial prototype extracting SearchServer from ResourceServer. 2026-01-12 17:50:39 +01:00
60 changed files with 796 additions and 2420 deletions

View File

@@ -14,9 +14,6 @@ outputs:
frontend:
description: Whether the frontend or self has changed in any way
value: ${{ steps.changed-files.outputs.frontend_any_changed || 'true' }}
frontend-packages:
description: Whether any frontend packages have changed
value: ${{ steps.changed-files.outputs.frontend_packages_any_changed || 'true' }}
e2e:
description: Whether the e2e tests or self have changed in any way
value: ${{ steps.changed-files.outputs.e2e_any_changed == 'true' ||
@@ -100,12 +97,6 @@ runs:
- '.yarn/**'
- 'apps/dashboard/pkg/migration/**'
- '${{ inputs.self }}'
frontend_packages:
- '.github/actions/checkout/**'
- '.github/actions/change-detection/**'
- 'packages/**'
- './scripts/validate-npm-packages.sh'
- '${{ inputs.self }}'
e2e:
- 'e2e/**'
- 'e2e-playwright/**'
@@ -162,8 +153,6 @@ runs:
echo " --> ${{ steps.changed-files.outputs.backend_all_changed_files }}"
echo "Frontend: ${{ steps.changed-files.outputs.frontend_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.frontend_all_changed_files }}"
echo "Frontend packages: ${{ steps.changed-files.outputs.frontend_packages_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.frontend_packages_all_changed_files }}"
echo "E2E: ${{ steps.changed-files.outputs.e2e_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.e2e_all_changed_files }}"
echo " --> ${{ steps.changed-files.outputs.backend_all_changed_files }}"

View File

@@ -4,8 +4,8 @@ description: Sets up a node.js environment with presets for the Grafana reposito
runs:
using: "composite"
steps:
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
cache-dependency-path: 'yarn.lock'

View File

@@ -17,7 +17,6 @@ jobs:
outputs:
changed: ${{ steps.detect-changes.outputs.frontend }}
prettier: ${{ steps.detect-changes.outputs.frontend == 'true' || steps.detect-changes.outputs.docs == 'true' }}
changed-frontend-packages: ${{ steps.detect-changes.outputs.frontend-packages }}
steps:
- uses: actions/checkout@v5
with:
@@ -43,8 +42,11 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- run: yarn install --immutable --check-cache
- run: yarn run prettier:check
- run: yarn run lint
@@ -61,8 +63,11 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Enterprise
uses: ./.github/actions/setup-enterprise
with:
@@ -84,8 +89,11 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- run: yarn install --immutable --check-cache
- run: yarn run typecheck
lint-frontend-typecheck-enterprise:
@@ -101,8 +109,11 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Enterprise
uses: ./.github/actions/setup-enterprise
with:
@@ -122,8 +133,11 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- run: yarn install --immutable --check-cache
- name: Generate API clients
run: |
@@ -150,8 +164,11 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Enterprise
uses: ./.github/actions/setup-enterprise
with:
@@ -170,26 +187,3 @@ jobs:
echo "${uncommited_error_message}"
exit 1
fi
lint-frontend-packed-packages:
needs: detect-changes
permissions:
contents: read
id-token: write
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.changed-frontend-packages == 'true'
name: Verify packed frontend packages
runs-on: ubuntu-latest
steps:
- name: Checkout build commit
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Install dependencies
run: yarn install --immutable
- name: Build and pack packages
run: |
yarn run packages:build
yarn run packages:pack
- name: Validate packages
run: ./scripts/validate-npm-packages.sh

View File

@@ -852,194 +852,6 @@
}
}
}
},
"panel-7": {
"kind": "Panel",
"spec": {
"id": 7,
"title": "Single Dashboard DS Query",
"description": "Panel with a single -- Dashboard -- datasource query",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "stat",
"spec": {
"pluginVersion": "12.1.0-pre",
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-8": {
"kind": "Panel",
"spec": {
"id": 8,
"title": "Multiple Dashboard DS Queries",
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "A",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 2,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "B",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 3,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "C",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "stat",
"spec": {
"pluginVersion": "12.1.0-pre",
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
@@ -1102,24 +914,6 @@
"name": "panel-6"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-7"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-8"
}
}
}
]
}

View File

@@ -879,200 +879,6 @@
}
}
}
},
"panel-7": {
"kind": "Panel",
"spec": {
"id": 7,
"title": "Single Dashboard DS Query",
"description": "Panel with a single -- Dashboard -- datasource query",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "stat",
"version": "12.1.0-pre",
"spec": {
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-8": {
"kind": "Panel",
"spec": {
"id": 8,
"title": "Multiple Dashboard DS Queries",
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"refId": "A",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 2,
"withTransforms": true
}
},
"refId": "B",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 3,
"withTransforms": true
}
},
"refId": "C",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "stat",
"version": "12.1.0-pre",
"spec": {
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
@@ -1167,32 +973,6 @@
"name": "panel-6"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 6,
"width": 8,
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-7"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 8,
"y": 6,
"width": 8,
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-8"
}
}
}
]
}

View File

@@ -711,146 +711,6 @@
],
"title": "Mixed DS WITHOUT REFS",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"description": "Panel with a single -- Dashboard -- datasource query",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 18
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
}
],
"title": "Single Dashboard DS Query",
"type": "stat"
},
{
"datasource": {
"type": "mixed",
"uid": "-- Mixed --"
},
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 18
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "B",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 3,
"refId": "C",
"withTransforms": true
}
],
"title": "Multiple Dashboard DS Queries",
"type": "stat"
}
],
"preload": false,

View File

@@ -711,146 +711,6 @@
],
"title": "Mixed DS WITHOUT REFS",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"description": "Panel with a single -- Dashboard -- datasource query",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 18
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
}
],
"title": "Single Dashboard DS Query",
"type": "stat"
},
{
"datasource": {
"type": "mixed",
"uid": "-- Mixed --"
},
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 18
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "B",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 3,
"refId": "C",
"withTransforms": true
}
],
"title": "Multiple Dashboard DS Queries",
"type": "stat"
}
],
"preload": false,

View File

@@ -879,200 +879,6 @@
}
}
}
},
"panel-7": {
"kind": "Panel",
"spec": {
"id": 7,
"title": "Single Dashboard DS Query",
"description": "Panel with a single -- Dashboard -- datasource query",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "stat",
"version": "12.1.0-pre",
"spec": {
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-8": {
"kind": "Panel",
"spec": {
"id": 8,
"title": "Multiple Dashboard DS Queries",
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"refId": "A",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 2,
"withTransforms": true
}
},
"refId": "B",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 3,
"withTransforms": true
}
},
"refId": "C",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "stat",
"version": "12.1.0-pre",
"spec": {
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
@@ -1135,24 +941,6 @@
"name": "panel-6"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-7"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-8"
}
}
}
]
}

View File

@@ -711,146 +711,6 @@
],
"title": "Mixed DS WITHOUT REFS",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"description": "Panel with a single -- Dashboard -- datasource query",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 3,
"w": 8,
"x": 0,
"y": 6
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
}
],
"title": "Single Dashboard DS Query",
"type": "stat"
},
{
"datasource": {
"type": "mixed",
"uid": "-- Mixed --"
},
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 3,
"w": 8,
"x": 8,
"y": 6
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "B",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 3,
"refId": "C",
"withTransforms": true
}
],
"title": "Multiple Dashboard DS Queries",
"type": "stat"
}
],
"preload": false,

View File

@@ -711,146 +711,6 @@
],
"title": "Mixed DS WITHOUT REFS",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"description": "Panel with a single -- Dashboard -- datasource query",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 3,
"w": 8,
"x": 0,
"y": 6
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
}
],
"title": "Single Dashboard DS Query",
"type": "stat"
},
{
"datasource": {
"type": "mixed",
"uid": "-- Mixed --"
},
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 3,
"w": 8,
"x": 8,
"y": 6
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "B",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 3,
"refId": "C",
"withTransforms": true
}
],
"title": "Multiple Dashboard DS Queries",
"type": "stat"
}
],
"preload": false,

View File

@@ -852,194 +852,6 @@
}
}
}
},
"panel-7": {
"kind": "Panel",
"spec": {
"id": 7,
"title": "Single Dashboard DS Query",
"description": "Panel with a single -- Dashboard -- datasource query",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "stat",
"spec": {
"pluginVersion": "12.1.0-pre",
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-8": {
"kind": "Panel",
"spec": {
"id": 8,
"title": "Multiple Dashboard DS Queries",
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "A",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 2,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "B",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 3,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "C",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "stat",
"spec": {
"pluginVersion": "12.1.0-pre",
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
@@ -1134,32 +946,6 @@
"name": "panel-6"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 6,
"width": 8,
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-7"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 8,
"y": 6,
"width": 8,
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-8"
}
}
}
]
}

View File

@@ -1195,36 +1195,16 @@ func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, quer
// getPanelDatasource determines the panel-level datasource for V1.
// Returns:
// - Mixed datasource reference if queries use different datasources
// - Mixed datasource reference if multiple queries use Dashboard datasource (they fetch from different panels)
// - Dashboard datasource reference if a single query uses Dashboard datasource
// - First query's datasource if all queries use the same datasource
// - nil if no queries exist
// Compares based on V2 input without runtime resolution:
// - If query has explicit datasource.uid → use that UID and type
// - Else → use query.Kind as type (empty UID)
func getPanelDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} {
const sharedDashboardQuery = "-- Dashboard --"
if len(queries) == 0 {
return nil
}
// Count how many queries use Dashboard datasource
// Multiple dashboard queries need mixed mode because they fetch from different panels
// which may have different underlying datasources
dashboardDsQueryCount := 0
for _, query := range queries {
if query.Spec.Datasource != nil && query.Spec.Datasource.Uid != nil && *query.Spec.Datasource.Uid == sharedDashboardQuery {
dashboardDsQueryCount++
}
}
if dashboardDsQueryCount > 1 {
return map[string]interface{}{
"type": "mixed",
"uid": "-- Mixed --",
}
}
var firstUID, firstType string
var hasFirst bool
@@ -1259,16 +1239,6 @@ func getPanelDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[stri
}
}
// Handle case when a single query uses Dashboard datasource.
// This is needed for the frontend to properly activate and fetch data from source panels.
// See DashboardDatasourceBehaviour.tsx for more details.
if firstUID == sharedDashboardQuery {
return map[string]interface{}{
"type": "datasource",
"uid": sharedDashboardQuery,
}
}
// Not mixed - return the first query's datasource so the panel has a datasource set.
// This is required because the frontend's legacy PanelModel.PanelQueryRunner.run uses panel.datasource
// to resolve the datasource, and if undefined, it falls back to the default datasource

View File

@@ -24,7 +24,6 @@ metaV0Alpha1: {
translations?: [string]: string
// +listType=atomic
children?: [...string]
aliasIds?: [...string]
}
}
}

View File

@@ -219,7 +219,6 @@ type MetaSpec struct {
Translations map[string]string `json:"translations,omitempty"`
// +listType=atomic
Children []string `json:"children,omitempty"`
AliasIds []string `json:"aliasIds,omitempty"`
}
// NewMetaSpec creates a new MetaSpec object.

File diff suppressed because one or more lines are too long

View File

@@ -573,8 +573,6 @@ func pluginStorePluginToMeta(plugin pluginstore.Plugin, loadingStrategy plugins.
metaSpec.Translations = plugin.Translations
}
metaSpec.AliasIds = plugin.AliasIDs
return metaSpec
}
@@ -678,8 +676,6 @@ func pluginToMetaSpec(plugin *plugins.Plugin) pluginsv0alpha1.MetaSpec {
metaSpec.Translations = plugin.Translations
}
metaSpec.AliasIds = plugin.AliasIDs
return metaSpec
}

View File

@@ -99,27 +99,12 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#query-and-resource-caching
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#query-and-resource-caching
mssql-troubleshoot:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/troubleshooting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/troubleshooting/
postgres:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/postgres/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/postgres/
mysql:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/
---
# Microsoft SQL Server (MSSQL) data source
Grafana ships with built-in support for Microsoft SQL Server (MSSQL).
You can query and visualize data from any Microsoft SQL Server 2005 or newer, including Microsoft Azure SQL Database.
You can query and visualize data from any Microsoft SQL Server 2005 or newer, including the Microsoft Azure SQL Database.
Use this data source to create dashboards, explore SQL data, and monitor MSSQL-based workloads in real time.
@@ -128,33 +113,10 @@ The following documentation helps you get started working with the Microsoft SQL
- [Configure the Microsoft SQL Server data source](ref:configure-mssql-data-source)
- [Microsoft SQL Server query editor](ref:mssql-query-editor)
- [Microsoft SQL Server template variables](ref:mssql-template-variables)
- [Troubleshoot Microsoft SQL Server data source issues](ref:mssql-troubleshoot)
## Supported versions
## Get the most out of the data source
This data source supports the following Microsoft SQL Server versions:
- Microsoft SQL Server 2005 and newer
- Microsoft Azure SQL Database
- Azure SQL Managed Instance
Grafana recommends using the latest available service pack for your SQL Server version for optimal compatibility.
## Key capabilities
The Microsoft SQL Server data source supports:
- **Time series queries:** Visualize metrics over time using the built-in time grouping macros.
- **Table queries:** Display query results in table format for any valid SQL query.
- **Template variables:** Create dynamic dashboards with variable-driven queries.
- **Annotations:** Overlay events from SQL Server on your dashboard graphs.
- **Alerting:** Create alerts based on SQL Server query results.
- **Stored procedures:** Execute stored procedures and visualize results.
- **Macros:** Simplify queries with built-in macros for time filtering and grouping.
## Additional resources
After configuring the Microsoft SQL Server data source, you can:
After installing and configuring the Microsoft SQL Server data source, you can:
- Create a wide variety of [visualizations](ref:visualizations)
- Configure and use [templates and variables](ref:variables)
@@ -162,8 +124,3 @@ After configuring the Microsoft SQL Server data source, you can:
- Add [annotations](ref:annotate-visualizations)
- Set up [alerting](ref:alerting)
- Optimize performance with [query caching](ref:query-caching)
## Related data sources
- [PostgreSQL](ref:postgres) - For PostgreSQL databases.
- [MySQL](ref:mysql) - For MySQL and MariaDB databases.

View File

@@ -89,26 +89,6 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana
mssql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/query-editor/
mssql-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/template-variables/
alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
mssql-troubleshoot:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/troubleshooting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/troubleshooting/
---
# Configure the Microsoft SQL Server data source
@@ -117,28 +97,13 @@ This document provides instructions for configuring the Microsoft SQL Server dat
## Before you begin
Before configuring the Microsoft SQL Server data source, ensure you have the following:
- Grafana comes with a built-in MSSQL data source plugin, eliminating the need to install a plugin.
- **Grafana permissions:** You must have the `Organization administrator` role to configure data sources. Organization administrators can also [configure the data source via YAML](#provision-the-data-source) with the Grafana provisioning system.
- You must have the `Organization administrator` role to configure the MSSQL data source. Organization administrators can also [configure the data source via YAML](#provision-the-data-source) with the Grafana provisioning system.
- **A running SQL Server instance:** Microsoft SQL Server 2005 or newer, Azure SQL Database, or Azure SQL Managed Instance.
- Familiarize yourself with your MSSQL security configuration and gather any necessary security certificates and client keys.
- **Network access:** Grafana must be able to reach your SQL Server. The default port is `1433`.
- **Authentication credentials:** Depending on your authentication method, you need one of:
- SQL Server login credentials (username and password).
- Windows/Kerberos credentials and configuration (not supported in Grafana Cloud).
- Azure Entra ID app registration or managed identity.
- **Security certificates:** If using encrypted connections, gather any necessary TLS/SSL certificates.
{{< admonition type="note" >}}
Grafana ships with a built-in Microsoft SQL Server data source plugin. No additional installation is required.
{{< /admonition >}}
{{< admonition type="tip" >}}
**Grafana Cloud users:** If your SQL Server is in a private network, you can configure [Private data source connect](ref:private-data-source-connect) to establish connectivity.
{{< /admonition >}}
- Verify that data from MSSQL is being written to your Grafana instance.
## Add the MSSQL data source
@@ -417,48 +382,3 @@ datasources:
secureJsonData:
password: 'Password!'
```
### Configure with Terraform
You can configure the Microsoft SQL Server data source using [Terraform](https://www.terraform.io/) with the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs).
For more information about provisioning resources with Terraform, refer to the [Grafana as code using Terraform](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/) documentation.
#### Terraform example
The following example creates a basic Microsoft SQL Server data source:
```hcl
resource "grafana_data_source" "mssql" {
name = "MSSQL"
type = "mssql"
url = "localhost:1433"
user = "grafana"
json_data_encoded = jsonencode({
database = "grafana"
maxOpenConns = 100
maxIdleConns = 100
maxIdleConnsAuto = true
connMaxLifetime = 14400
connectionTimeout = 0
encrypt = "false"
})
secure_json_data_encoded = jsonencode({
password = "Password!"
})
}
```
For all available configuration options, refer to the [Grafana provider data source resource documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source).
## Next steps
After configuring your Microsoft SQL Server data source, you can:
- [Write queries](ref:mssql-query-editor) using the query editor to explore and visualize your data
- [Create template variables](ref:mssql-template-variables) to build dynamic, reusable dashboards
- [Add annotations](ref:annotate-visualizations) to overlay SQL Server events on your graphs
- [Set up alerting](ref:alerting) to create alert rules based on your SQL Server data
- [Troubleshoot issues](ref:mssql-troubleshoot) if you encounter problems with your data source

View File

@@ -1,333 +0,0 @@
---
description: Troubleshoot common problems with the Microsoft SQL Server data source in Grafana
keywords:
- grafana
- MSSQL
- Microsoft
- SQL
- troubleshooting
- errors
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Troubleshooting
title: Troubleshoot Microsoft SQL Server data source issues
weight: 400
refs:
configure-mssql-data-source:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/configure/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/configure/
mssql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/query-editor/
private-data-source-connect:
- pattern: /docs/grafana/
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
---
# Troubleshoot Microsoft SQL Server data source issues
This document provides solutions to common issues you may encounter when configuring or using the Microsoft SQL Server (MSSQL) data source in Grafana.
## Connection errors
These errors occur when Grafana cannot establish or maintain a connection to the Microsoft SQL Server.
### Unable to connect to the server
**Error message:** "Unable to open tcp connection" or "dial tcp: connection refused"
**Cause:** Grafana cannot establish a network connection to the SQL Server.
**Solution:**
1. Verify that the SQL Server is running and accessible.
1. Check that the host and port are correct in the data source configuration. The default SQL Server port is `1433`.
1. Ensure there are no firewall rules blocking the connection between Grafana and SQL Server.
1. Verify that SQL Server is configured to allow remote connections.
1. For Grafana Cloud, ensure you have configured [Private data source connect](ref:private-data-source-connect) if your SQL Server instance is not publicly accessible.
### Connection timeout
**Error message:** "Connection timed out" or "I/O timeout"
**Cause:** The connection to SQL Server timed out before receiving a response.
**Solution:**
1. Check the network latency between Grafana and SQL Server.
1. Verify that SQL Server is not overloaded or experiencing performance issues.
1. Increase the **Connection timeout** setting in the data source configuration under **Additional settings**.
1. Check if any network devices (load balancers, proxies) are timing out the connection.
### Encryption-related connection failures
**Error message:** "TLS handshake failed" or "certificate verify failed"
**Cause:** There is a mismatch between the encryption settings in Grafana and what the SQL Server supports or requires.
**Solution:**
1. For older versions of SQL Server (2008, 2008R2), set the **Encrypt** option to **Disable** or **False** in the data source configuration.
1. Verify that the SQL Server has a valid SSL certificate if encryption is enabled.
1. Check that the certificate is trusted by the Grafana server.
1. Ensure you're using the latest available service pack for your SQL Server version for optimal compatibility.
### Named instance connection issues
**Error message:** "Cannot connect to named instance" or connection fails when using instance name
**Cause:** Grafana cannot resolve the SQL Server named instance.
**Solution:**
1. Use the format `hostname\instancename` or `hostname\instancename,port` in the **Host** field.
1. Verify that the SQL Server Browser service is running on the SQL Server machine.
1. If the Browser service is unavailable, specify the port number directly: `hostname,port`.
1. Check that UDP port 1434 is open if using the SQL Server Browser service.
## Authentication errors
These errors occur when there are issues with authentication credentials or permissions.
### Login failed for user
**Error message:** "Login failed for user 'username'" or "Authentication failed"
**Cause:** The authentication credentials are invalid or the user doesn't have permission to access the database.
**Solution:**
1. Verify that the username and password are correct.
1. Check that the user exists in SQL Server and is enabled.
1. Ensure the user has access to the specified database.
1. For Windows Authentication, verify that the credentials are in the correct format (`DOMAIN\User`).
1. Check that the SQL Server authentication mode allows the type of login you're using (SQL Server Authentication, Windows Authentication, or Mixed Mode).
### Access denied to database
**Error message:** "Cannot open database 'dbname' requested by the login"
**Cause:** The authenticated user doesn't have permission to access the specified database.
**Solution:**
1. Verify that the database name is correct in the data source configuration.
1. Ensure the user is mapped to the database with appropriate permissions.
1. Grant at least `SELECT` permission on the required tables:
```sql
USE [your_database]
GRANT SELECT ON dbo.YourTable TO [your_user]
```
1. Check that the user doesn't have any conflicting permissions from the public role.
### Windows Authentication (Kerberos) issues
**Error message:** "Kerberos authentication failed" or "Cannot initialize Kerberos"
**Cause:** Kerberos configuration is incorrect or incomplete.
**Solution:**
1. Verify that the Kerberos configuration file (`krb5.conf`) path is correct in the data source settings.
1. For keytab authentication, ensure the keytab file exists and is readable by Grafana.
1. Check that the realm and KDC settings are correct in the Kerberos configuration.
1. Verify DNS is correctly resolving the KDC servers.
1. Ensure the service principal name (SPN) is registered for the SQL Server instance.
{{< admonition type="note" >}}
Kerberos authentication is not supported in Grafana Cloud.
{{< /admonition >}}
### Azure Entra ID authentication errors
**Error message:** "AADSTS error codes" or "Azure AD authentication failed"
**Cause:** Azure Entra ID (formerly Azure AD) authentication is misconfigured.
**Solution:**
1. For **App Registration** authentication:
- Verify the tenant ID, client ID, and client secret are correct.
- Ensure the app registration has been added as a user in the Azure SQL database.
- Check that the client secret hasn't expired.
1. For **Managed Identity** authentication:
- Verify `managed_identity_enabled = true` is set in the Grafana server configuration.
- Ensure the managed identity has been added to the Azure SQL database.
- Confirm the Azure resource hosting Grafana has managed identity enabled.
1. For **Current User** authentication:
- Ensure `user_identity_enabled = true` is set in the Grafana server configuration.
- Verify the app registration is configured to issue both Access Tokens and ID Tokens.
- Check that the required API permissions are configured (`user_impersonation` for Azure SQL).
For detailed Azure authentication configuration, refer to [Configure the Microsoft SQL Server data source](ref:configure-mssql-data-source).
## Query errors
These errors occur when there are issues with query syntax or configuration.
### Time column not found or invalid
**Error message:** "Could not find time column" or time series visualization shows no data
**Cause:** The query doesn't return a properly formatted `time` column for time series visualization.
**Solution:**
1. Ensure your query includes a column named `time` when using the **Time series** format.
1. Use the `$__time()` macro to rename your date column: `$__time(your_date_column)`.
1. Verify the time column is of a valid SQL date/time type (`datetime`, `datetime2`, `date`) or contains Unix epoch values.
1. Ensure the result set is sorted by the time column using `ORDER BY`.
### Macro expansion errors
**Error message:** "Error parsing query" or macros appear unexpanded in the query
**Cause:** Grafana macros are being used incorrectly.
**Solution:**
1. Verify macro syntax: use `$__timeFilter(column)` not `$_timeFilter(column)`.
1. Macros don't work inside stored procedures—use explicit date parameters instead.
1. Check that the column name passed to macros exists in your table.
1. View the expanded query by clicking **Generated SQL** after running the query to debug macro expansion.
### Timezone and time shift issues
**Cause:** Time series data appears shifted or doesn't align with expected times.
**Solution:**
1. Store timestamps in UTC in your database to avoid timezone issues.
1. Time macros (`$__time`, `$__timeFilter`, etc.) always expand to UTC values.
1. If your timestamps are stored in local time, convert them to UTC in your query:
```sql
SELECT
your_datetime_column AT TIME ZONE 'Your Local Timezone' AT TIME ZONE 'UTC' AS time,
value
FROM your_table
```
1. Don't pass timezone parameters to time macros—they're not supported.
### Query returns too many rows
**Error message:** "Result set too large" or browser becomes unresponsive
**Cause:** The query returns more data than can be efficiently processed.
**Solution:**
1. Add time filters using `$__timeFilter(column)` to limit data to the dashboard time range.
1. Use aggregations (`AVG`, `SUM`, `COUNT`) with `GROUP BY` instead of returning raw rows.
1. Add a `TOP` clause to limit results: `SELECT TOP 1000 ...`.
1. Use the `$__timeGroup()` macro to aggregate data into time intervals.
### Stored procedure returns no data
**Cause:** Stored procedure output isn't being captured correctly.
**Solution:**
1. Ensure the stored procedure uses `SELECT` statements, not just variable assignments.
1. Remove `SET NOCOUNT ON` if present, or ensure it's followed by a `SELECT` statement.
1. Verify the stored procedure parameters are being passed correctly.
1. Test the stored procedure directly in SQL Server Management Studio with the same parameters.
For more information on using stored procedures, refer to the [query editor documentation](ref:mssql-query-editor).
## Performance issues
These issues relate to slow queries or high resource usage.
### Slow query execution
**Cause:** Queries take a long time to execute.
**Solution:**
1. Reduce the dashboard time range to limit data volume.
1. Add indexes to columns used in `WHERE` clauses and time filters.
1. Use aggregations instead of returning individual rows.
1. Increase the **Min time interval** setting to reduce the number of data points.
1. Review the query execution plan in SQL Server Management Studio to identify bottlenecks.
### Connection pool exhaustion
**Error message:** "Too many connections" or "Connection pool exhausted"
**Cause:** Too many concurrent connections to the database.
**Solution:**
1. Increase the **Max open** connection limit in the data source configuration.
1. Enable **Auto max idle** to automatically manage idle connections.
1. Reduce the number of panels querying the same data source simultaneously.
1. Check for long-running queries that might be holding connections.
## Other common issues
The following issues don't produce specific error messages but are commonly encountered.
### System databases appear in queries
**Cause:** Queries accidentally access system databases.
**Solution:**
1. The query editor automatically excludes `tempdb`, `model`, `msdb`, and `master` from the database dropdown.
1. Always specify the database in your data source configuration to restrict access.
1. Ensure the database user only has permissions on the intended database.
### Template variable queries fail
**Cause:** Variable queries return unexpected results or errors.
**Solution:**
1. Verify the variable query syntax is valid SQL that returns a single column.
1. Check that the data source connection is working.
1. Ensure the user has permission to access the tables referenced in the variable query.
1. Test the query in the query editor before using it as a variable query.
### Data appears incorrect or misaligned
**Cause:** Data formatting or type conversion issues.
**Solution:**
1. Use explicit column aliases to ensure consistent naming: `SELECT value AS metric`.
1. Verify numeric columns are actually numeric types, not strings.
1. Check for `NULL` values that might affect aggregations.
1. Use the `FILL` option in `$__timeGroup()` macro to handle missing data points.
## Get additional help
If you continue to experience issues after following this troubleshooting guide:
1. Check the [Grafana community forums](https://community.grafana.com/) for similar issues.
1. Review the [Grafana GitHub issues](https://github.com/grafana/grafana/issues) for known bugs.
1. Enable debug logging in Grafana to capture detailed error information.
1. Check SQL Server logs for additional error details.
1. Contact Grafana Support if you're an Enterprise or Cloud customer.
When reporting issues, include:
- Grafana version
- SQL Server version
- Error messages (redact sensitive information)
- Steps to reproduce
- Relevant query examples (redact sensitive data)

View File

@@ -9,7 +9,6 @@ import { FieldColorModeId } from '../types/fieldColor';
import { FieldConfigPropertyItem, FieldConfigSource } from '../types/fieldOverrides';
import { InterpolateFunction } from '../types/panel';
import { ThresholdsMode } from '../types/thresholds';
import { MappingType } from '../types/valueMapping';
import { Registry } from '../utils/Registry';
import { locationUtil } from '../utils/location';
import { mockStandardProperties } from '../utils/tests/mockStandardProperties';
@@ -1000,45 +999,6 @@ describe('setDynamicConfigValue', () => {
expect(config.custom.property3).toEqual({});
expect(config.displayName).toBeUndefined();
});
it('works correctly with multiple value mappings in the same override', () => {
const config: FieldConfig = {
mappings: [{ type: MappingType.ValueToText, options: { existing: { text: 'existing' } } }],
};
setDynamicConfigValue(
config,
{
id: 'mappings',
value: [{ type: MappingType.ValueToText, options: { first: { text: 'first' } } }],
},
{
fieldConfigRegistry: customFieldRegistry,
data: [],
field: { type: FieldType.number } as Field,
dataFrameIndex: 0,
}
);
setDynamicConfigValue(
config,
{
id: 'mappings',
value: [{ type: MappingType.ValueToText, options: { second: { text: 'second' } } }],
},
{
fieldConfigRegistry: customFieldRegistry,
data: [],
field: { type: FieldType.number } as Field,
dataFrameIndex: 0,
}
);
expect(config.mappings).toHaveLength(3);
expect(config.mappings![0]).toEqual({ type: MappingType.ValueToText, options: { existing: { text: 'existing' } } });
expect(config.mappings![1]).toEqual({ type: MappingType.ValueToText, options: { first: { text: 'first' } } });
expect(config.mappings![2]).toEqual({ type: MappingType.ValueToText, options: { second: { text: 'second' } } });
});
});
describe('getLinksSupplier', () => {

View File

@@ -341,7 +341,7 @@ export function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigV
return;
}
let val = item.process(value.value, context, item.settings);
const val = item.process(value.value, context, item.settings);
const remove = val === undefined || val === null;
@@ -352,15 +352,6 @@ export function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigV
unset(config, item.path);
}
} else {
// Merge arrays (e.g. mappings) when multiple overrides target the same field
if (Array.isArray(val)) {
const existingValue = item.isCustom ? get(config.custom, item.path) : get(config, item.path);
if (Array.isArray(existingValue)) {
val = [...existingValue, ...val];
}
}
if (item.isCustom) {
if (!config.custom) {
config.custom = {};

View File

@@ -527,6 +527,10 @@ export interface FeatureToggles {
*/
dashboardTemplates?: boolean;
/**
* Sets the logs table as default visualisation in logs explore
*/
logsExploreTableDefaultVisualization?: boolean;
/**
* Enables the new alert list view design
*/
alertingListViewV2?: boolean;

View File

@@ -29,6 +29,7 @@
"@grafana-app/source": "./src/internal/index.ts"
},
"./eslint-plugin": {
"@grafana-app/source": "./src/eslint/index.cjs",
"types": "./src/eslint/index.d.ts",
"default": "./src/eslint/index.cjs"
}

View File

@@ -10,6 +10,7 @@ const (
SearchServerRing string = "search-server-ring"
SearchServerDistributor string = "search-server-distributor"
StorageServer string = "storage-server"
SearchServer string = "search-server"
ZanzanaServer string = "zanzana-server"
InstrumentationServer string = "instrumentation-server"
FrontendServer string = "frontend-server"
@@ -21,6 +22,7 @@ var dependencyMap = map[string][]string{
SearchServerRing: {InstrumentationServer, MemberlistKV},
GrafanaAPIServer: {InstrumentationServer},
StorageServer: {InstrumentationServer, SearchServerRing},
SearchServer: {InstrumentationServer, SearchServerRing},
ZanzanaServer: {InstrumentationServer},
SearchServerDistributor: {InstrumentationServer, MemberlistKV, SearchServerRing},
Core: {},

View File

@@ -38,9 +38,9 @@ func (d *directResourceClient) GetBlob(ctx context.Context, in *resourcepb.GetBl
return d.server.GetBlob(ctx, in)
}
// GetStats implements ResourceClient.
// GetStats implements ResourceClient (SearchClient).
func (d *directResourceClient) GetStats(ctx context.Context, in *resourcepb.ResourceStatsRequest, opts ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) {
return d.server.GetStats(ctx, in)
return nil, fmt.Errorf("GetStats not supported with direct resource client")
}
// IsHealthy implements ResourceClient.
@@ -53,12 +53,14 @@ func (d *directResourceClient) List(ctx context.Context, in *resourcepb.ListRequ
return d.server.List(ctx, in)
}
// ListManagedObjects implements ResourceClient (SearchClient).
func (d *directResourceClient) ListManagedObjects(ctx context.Context, in *resourcepb.ListManagedObjectsRequest, opts ...grpc.CallOption) (*resourcepb.ListManagedObjectsResponse, error) {
return d.server.ListManagedObjects(ctx, in)
return nil, fmt.Errorf("ListManagedObjects not supported with direct resource client")
}
// CountManagedObjects implements ResourceClient (SearchClient).
func (d *directResourceClient) CountManagedObjects(ctx context.Context, in *resourcepb.CountManagedObjectsRequest, opts ...grpc.CallOption) (*resourcepb.CountManagedObjectsResponse, error) {
return d.server.CountManagedObjects(ctx, in)
return nil, fmt.Errorf("CountManagedObjects not supported with direct resource client")
}
// PutBlob implements ResourceClient.
@@ -71,9 +73,9 @@ func (d *directResourceClient) Read(ctx context.Context, in *resourcepb.ReadRequ
return d.server.Read(ctx, in)
}
// Search implements ResourceClient.
// Search implements ResourceClient (SearchClient).
func (d *directResourceClient) Search(ctx context.Context, in *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
return d.server.Search(ctx, in)
return nil, fmt.Errorf("Search not supported with direct resource client")
}
// Update implements ResourceClient.

View File

@@ -11,6 +11,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/utils"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"

View File

@@ -71,6 +71,7 @@ type cachingDatasourceProvider struct {
}
func (q *cachingDatasourceProvider) GetDatasourceProvider(pluginJson plugins.JSONData) PluginDatasourceProvider {
group, _ := plugins.GetDatasourceGroupNameFromPluginID(pluginJson.ID)
return &scopedDatasourceProvider{
plugin: pluginJson,
dsService: q.dsService,
@@ -80,7 +81,7 @@ func (q *cachingDatasourceProvider) GetDatasourceProvider(pluginJson plugins.JSO
mapper: q.converter.mapper,
plugin: pluginJson.ID,
alias: pluginJson.AliasIDs,
group: pluginJson.ID,
group: group,
},
}
}

View File

@@ -37,11 +37,6 @@ var (
_ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil)
)
type DataSourceAPIBuilderConfig struct {
LoadQueryTypes bool
UseDualWriter bool
}
// DataSourceAPIBuilder is used just so wire has something unique to return
type DataSourceAPIBuilder struct {
datasourceResourceInfo utils.ResourceInfo
@@ -51,7 +46,7 @@ type DataSourceAPIBuilder struct {
contextProvider PluginContextWrapper
accessControl accesscontrol.AccessControl
queryTypes *queryV0.QueryTypeDefinitionList
cfg DataSourceAPIBuilderConfig
configCrudUseNewApis bool
dataSourceCRUDMetric *prometheus.HistogramVec
}
@@ -94,24 +89,20 @@ func RegisterAPIService(
return nil, fmt.Errorf("plugin client is not a PluginClient: %T", pluginClient)
}
groupName := pluginJSON.ID + ".datasource.grafana.app"
builder, err = NewDataSourceAPIBuilder(
groupName,
pluginJSON,
client,
datasources.GetDatasourceProvider(pluginJSON),
contextProvider,
accessControl,
DataSourceAPIBuilderConfig{
//nolint:staticcheck // not yet migrated to OpenFeature
LoadQueryTypes: features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
UseDualWriter: false,
},
//nolint:staticcheck // not yet migrated to OpenFeature
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
//nolint:staticcheck // not yet migrated to OpenFeature
features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections),
)
if err != nil {
return nil, err
}
builder.SetDataSourceCRUDMetrics(dataSourceCRUDMetric)
apiRegistrar.RegisterAPI(builder)
@@ -129,27 +120,31 @@ type PluginClient interface {
}
func NewDataSourceAPIBuilder(
groupName string,
plugin plugins.JSONData,
client PluginClient,
datasources PluginDatasourceProvider,
contextProvider PluginContextWrapper,
accessControl accesscontrol.AccessControl,
cfg DataSourceAPIBuilderConfig,
loadQueryTypes bool,
configCrudUseNewApis bool,
) (*DataSourceAPIBuilder, error) {
group, err := plugins.GetDatasourceGroupNameFromPluginID(plugin.ID)
if err != nil {
return nil, err
}
builder := &DataSourceAPIBuilder{
datasourceResourceInfo: datasourceV0.DataSourceResourceInfo.WithGroupAndShortName(groupName, plugin.ID),
datasourceResourceInfo: datasourceV0.DataSourceResourceInfo.WithGroupAndShortName(group, plugin.ID),
pluginJSON: plugin,
client: client,
datasources: datasources,
contextProvider: contextProvider,
accessControl: accessControl,
cfg: cfg,
configCrudUseNewApis: configCrudUseNewApis,
}
var err error
if cfg.LoadQueryTypes {
if loadQueryTypes {
// In the future, this will somehow come from the plugin
builder.queryTypes, err = getHardcodedQueryTypes(groupName)
builder.queryTypes, err = getHardcodedQueryTypes(group)
}
return builder, err
}
@@ -159,9 +154,9 @@ func getHardcodedQueryTypes(group string) (*queryV0.QueryTypeDefinitionList, err
var err error
var raw json.RawMessage
switch group {
case "testdata.datasource.grafana.app", "grafana-testdata-datasource":
case "testdata.datasource.grafana.app":
raw, err = kinds.QueryTypeDefinitionListJSON()
case "prometheus.datasource.grafana.app", "prometheus":
case "prometheus.datasource.grafana.app":
raw, err = models.QueryTypeDefinitionListJSON()
}
if err != nil {
@@ -238,7 +233,7 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
storage["connections"] = &noopREST{} // hidden from openapi
storage["connections/query"] = storage[ds.StoragePath("query")] // deprecated in openapi
if b.cfg.UseDualWriter {
if b.configCrudUseNewApis {
legacyStore := &legacyStorage{
datasources: b.datasources,
resourceInfo: &ds,

View File

@@ -37,7 +37,6 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
@@ -74,7 +73,7 @@ func RegisterAPIService(cfg *setting.Cfg,
acService accesscontrol.Service,
accessClient authlib.AccessClient,
registerer prometheus.Registerer,
unified resource.ResourceClient,
unified resourcepb.ResourceIndexClient,
zanzanaClient zanzana.Client,
) *FolderAPIBuilder {
builder := &FolderAPIBuilder{
@@ -93,7 +92,7 @@ func RegisterAPIService(cfg *setting.Cfg,
return builder
}
func NewAPIService(ac authlib.AccessClient, searcher resource.ResourceClient, features featuremgmt.FeatureToggles, zanzanaClient zanzana.Client, resourcePermissionsSvc *dynamic.NamespaceableResourceInterface) *FolderAPIBuilder {
func NewAPIService(ac authlib.AccessClient, searcher resourcepb.ResourceIndexClient, features featuremgmt.FeatureToggles, zanzanaClient zanzana.Client, resourcePermissionsSvc *dynamic.NamespaceableResourceInterface) *FolderAPIBuilder {
return &FolderAPIBuilder{
features: features,
accessClient: ac,

View File

@@ -742,7 +742,7 @@ func NewLocalStore(resourceInfo utils.ResourceInfo, scheme *runtime.Scheme, defa
return nil, err
}
client := resource.NewLocalResourceClient(server)
client := resource.NewLocalResourceClient(server, nil)
optsGetter := apistore.NewRESTOptionsGetterForClient(client, nil, defaultOpts.StorageConfig.Config, nil)
store, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter)

View File

@@ -205,6 +205,14 @@ func (s *ModuleServer) Run() error {
return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.storageMetrics, s.indexMetrics, s.searchServerRing, s.MemberlistKVConfig, s.httpServerRouter, s.storageBackend)
})
m.RegisterModule(modules.SearchServer, func() (services.Service, error) {
docBuilders, err := InitializeDocumentBuilders(s.cfg)
if err != nil {
return nil, err
}
return sql.ProvideSearchGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.indexMetrics, s.searchServerRing, s.MemberlistKVConfig, s.storageBackend)
})
m.RegisterModule(modules.ZanzanaServer, func() (services.Service, error) {
return authz.ProvideZanzanaService(s.cfg, s.features, s.registerer)
})

View File

@@ -65,6 +65,7 @@ import (
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
search2 "github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/grafana/grafana/pkg/storage/unified/search/builders"
"github.com/grafana/grafana/pkg/storage/unified/sql"
@@ -146,6 +147,7 @@ var wireExtsBasicSet = wire.NewSet(
wire.Bind(new(sandbox.Sandbox), new(*sandbox.Service)),
wire.Struct(new(unified.Options), "*"),
unified.ProvideUnifiedStorageClient,
wire.Bind(new(resourcepb.ResourceIndexClient), new(resource.ResourceClient)),
sql.ProvideStorageBackend,
builder.ProvideDefaultBuildHandlerChainFuncFromBuilders,
aggregatorrunner.ProvideNoopAggregatorConfigurator,

View File

@@ -872,6 +872,13 @@ var (
Owner: grafanaSharingSquad,
FrontendOnly: false,
},
{
Name: "logsExploreTableDefaultVisualization",
Description: "Sets the logs table as default visualisation in logs explore",
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityLogsSquad,
FrontendOnly: true,
},
{
Name: "alertingListViewV2",
Description: "Enables the new alert list view design",

View File

@@ -120,6 +120,7 @@ queryLibrary,preview,@grafana/sharing-squad,false,false,false
dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false
suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false
dashboardTemplates,preview,@grafana/sharing-squad,false,false,false
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
120 dashboardLibrary experimental @grafana/sharing-squad false false false
121 suggestedDashboards experimental @grafana/sharing-squad false false false
122 dashboardTemplates preview @grafana/sharing-squad false false false
123 logsExploreTableDefaultVisualization experimental @grafana/observability-logs false false true
124 alertingListViewV2 privatePreview @grafana/alerting-squad false false true
125 alertingSavedSearches experimental @grafana/alerting-squad false false true
126 alertingDisableSendAlertsExternal experimental @grafana/alerting-squad false false false

View File

@@ -2246,8 +2246,7 @@
"metadata": {
"name": "logsExploreTableDefaultVisualization",
"resourceVersion": "1764664939750",
"creationTimestamp": "2024-05-02T15:28:15Z",
"deletionTimestamp": "2026-01-12T14:11:46Z"
"creationTimestamp": "2024-05-02T15:28:15Z"
},
"spec": {
"description": "Sets the logs table as default visualisation in logs explore",

View File

@@ -623,6 +623,8 @@ type Cfg struct {
OverridesFilePath string
OverridesReloadInterval time.Duration
EnableSQLKVBackend bool
SearchMode string // "", "embedded", "remote" - empty defaults to embedded
SearchServerAddress string // gRPC address for remote search server
// Secrets Management
SecretsManagement SecretsManagerSettings

View File

@@ -136,6 +136,10 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
// use sqlkv (resource/sqlkv) instead of the sql backend (sql/backend) as the StorageServer
cfg.EnableSQLKVBackend = section.Key("enable_sqlkv_backend").MustBool(false)
// search mode: "", "embedded", "remote" - empty defaults to embedded for backward compatibility
cfg.SearchMode = section.Key("search_mode").MustString("")
cfg.SearchServerAddress = section.Key("search_server_address").String()
cfg.MaxFileIndexAge = section.Key("max_file_index_age").MustDuration(0)
cfg.MinFileIndexBuildVersion = section.Key("min_file_index_build_version").MustString("")
}

View File

@@ -79,7 +79,7 @@ func NewRESTOptionsGetterMemory(originalStorageConfig storagebackend.Config, sec
}
return NewRESTOptionsGetterForClient(
resource.NewLocalResourceClient(server),
resource.NewLocalResourceClient(server, nil),
secrets,
originalStorageConfig,
nil,
@@ -118,7 +118,7 @@ func NewRESTOptionsGetterForFileXX(path string,
}
return NewRESTOptionsGetterForClient(
resource.NewLocalResourceClient(server),
resource.NewLocalResourceClient(server, nil),
nil, // secrets
originalStorageConfig,
nil,

View File

@@ -156,7 +156,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte
default:
t.Fatalf("unsupported storage type: %s", setupOpts.storageType)
}
client := resource.NewLocalResourceClient(server)
client := resource.NewLocalResourceClient(server, nil)
config := storagebackend.NewDefaultConfig(setupOpts.prefix, setupOpts.codec)
store, destroyFunc, err := apistore.NewStorage(

View File

@@ -21,6 +21,7 @@ import (
"github.com/grafana/dskit/grpcclient"
"github.com/grafana/dskit/middleware"
"github.com/grafana/dskit/services"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
infraDB "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -135,7 +136,7 @@ func newClient(opts options.StorageOptions,
if err != nil {
return nil, err
}
return resource.NewLocalResourceClient(server), nil
return resource.NewLocalResourceClient(server, nil), nil
case options.StorageTypeUnifiedGrpc:
if opts.Address == "" {
@@ -224,11 +225,11 @@ func newClient(opts options.StorageOptions,
serverOptions.OverridesService = overridesSvc
}
server, err := sql.NewResourceServer(serverOptions)
server, searchServer, err := sql.NewResourceServer(serverOptions)
if err != nil {
return nil, err
}
return resource.NewLocalResourceClient(server), nil
return resource.NewLocalResourceClient(server, searchServer), nil
}
}

View File

@@ -31,15 +31,20 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
type SearchClient interface {
resourcepb.ResourceIndexClient
resourcepb.ManagedObjectIndexClient
}
//go:generate mockery --name ResourceClient --structname MockResourceClient --inpackage --filename client_mock.go --with-expecter
type ResourceClient interface {
resourcepb.ResourceStoreClient
resourcepb.ResourceIndexClient
resourcepb.ManagedObjectIndexClient
resourcepb.BulkStoreClient
resourcepb.BlobStoreClient
resourcepb.DiagnosticsClient
resourcepb.QuotasClient
// SearchClient methods are included for convenience - the client typically needs both
SearchClient
}
// Internal implementation
@@ -92,16 +97,15 @@ func NewLegacyResourceClient(channel grpc.ClientConnInterface, indexChannel grpc
return newResourceClient(cc, cci)
}
func NewLocalResourceClient(server ResourceServer) ResourceClient {
func NewLocalResourceClient(server ResourceServer, searchServer SearchServer) ResourceClient {
// scenario: local in-proc
channel := &inprocgrpc.Channel{}
indexChannel := &inprocgrpc.Channel{}
tracer := otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/resource")
grpcAuthInt := grpcutils.NewUnsafeAuthenticator(tracer)
for _, desc := range []*grpc.ServiceDesc{
&resourcepb.ResourceStore_ServiceDesc,
&resourcepb.ResourceIndex_ServiceDesc,
&resourcepb.ManagedObjectIndex_ServiceDesc,
&resourcepb.BlobStore_ServiceDesc,
&resourcepb.BulkStore_ServiceDesc,
&resourcepb.Diagnostics_ServiceDesc,
@@ -117,13 +121,31 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient {
)
}
// Register search services on the index channel if searchServer is provided
if searchServer != nil {
for _, desc := range []*grpc.ServiceDesc{
&resourcepb.ResourceIndex_ServiceDesc,
&resourcepb.ManagedObjectIndex_ServiceDesc,
} {
indexChannel.RegisterService(
grpchan.InterceptServer(
desc,
grpcAuth.UnaryServerInterceptor(grpcAuthInt),
grpcAuth.StreamServerInterceptor(grpcAuthInt),
),
searchServer,
)
}
}
clientInt := authnlib.NewGrpcClientInterceptor(
ProvideInProcExchanger(),
authnlib.WithClientInterceptorIDTokenExtractor(idTokenExtractor),
)
cc := grpchan.InterceptClientConn(channel, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor)
return newResourceClient(cc, cc)
cci := grpchan.InterceptClientConn(indexChannel, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor)
return newResourceClient(cc, cci)
}
type RemoteResourceClientConfig struct {

View File

@@ -127,7 +127,10 @@ type SearchBackend interface {
GetOpenIndexes() []NamespacedResource
}
// This supports indexing+search regardless of implementation
var _ SearchServer = &searchSupport{}
// This supports indexing+search regardless of implementation.
// Implements SearchServer interface.
type searchSupport struct {
log log.Logger
storage StorageBackend
@@ -160,6 +163,10 @@ var (
_ resourcepb.ManagedObjectIndexServer = (*searchSupport)(nil)
)
func NewSearchServer(opts SearchOptions, storage StorageBackend, access types.AccessClient, blob BlobSupport, indexMetrics *BleveIndexMetrics, ownsIndexFn func(key NamespacedResource) (bool, error)) (SearchServer, error) {
return newSearchSupport(opts, storage, access, blob, indexMetrics, ownsIndexFn)
}
func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.AccessClient, blob BlobSupport, indexMetrics *BleveIndexMetrics, ownsIndexFn func(key NamespacedResource) (bool, error)) (support *searchSupport, err error) {
// No backend search support
if opts.Backend == nil {
@@ -598,6 +605,15 @@ func (s *searchSupport) buildIndexes(ctx context.Context) (int, error) {
return totalBatchesIndexed, nil
}
func (s *searchSupport) Init(ctx context.Context) error {
return s.init(ctx)
}
func (s *searchSupport) Stop(_ context.Context) error {
s.stop()
return nil
}
func (s *searchSupport) init(ctx context.Context) error {
origCtx := ctx

View File

@@ -60,7 +60,7 @@ func ProvideSearchDistributorServer(cfg *setting.Cfg, features featuremgmt.Featu
}
type RingClient struct {
Client ResourceClient
Client SearchClient
grpc_health_v1.HealthClient
Conn *grpc.ClientConn
}
@@ -99,7 +99,7 @@ var (
func (ds *distributorServer) Search(ctx context.Context, r *resourcepb.ResourceSearchRequest) (*resourcepb.ResourceSearchResponse, error) {
ctx, span := ds.tracing.Start(ctx, "distributor.Search")
defer span.End()
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Options.Key.Namespace, "Search")
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Options.Key.Namespace)
if err != nil {
return nil, err
}
@@ -110,7 +110,7 @@ func (ds *distributorServer) Search(ctx context.Context, r *resourcepb.ResourceS
func (ds *distributorServer) GetStats(ctx context.Context, r *resourcepb.ResourceStatsRequest) (*resourcepb.ResourceStatsResponse, error) {
ctx, span := ds.tracing.Start(ctx, "distributor.GetStats")
defer span.End()
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Namespace, "GetStats")
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Namespace)
if err != nil {
return nil, err
}
@@ -215,7 +215,7 @@ func (ds *distributorServer) RebuildIndexes(ctx context.Context, r *resourcepb.R
func (ds *distributorServer) CountManagedObjects(ctx context.Context, r *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) {
ctx, span := ds.tracing.Start(ctx, "distributor.CountManagedObjects")
defer span.End()
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Namespace, "CountManagedObjects")
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Namespace)
if err != nil {
return nil, err
}
@@ -226,7 +226,7 @@ func (ds *distributorServer) CountManagedObjects(ctx context.Context, r *resourc
func (ds *distributorServer) ListManagedObjects(ctx context.Context, r *resourcepb.ListManagedObjectsRequest) (*resourcepb.ListManagedObjectsResponse, error) {
ctx, span := ds.tracing.Start(ctx, "distributor.ListManagedObjects")
defer span.End()
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Namespace, "ListManagedObjects")
ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Namespace)
if err != nil {
return nil, err
}
@@ -234,7 +234,7 @@ func (ds *distributorServer) ListManagedObjects(ctx context.Context, r *resource
return client.ListManagedObjects(ctx, r)
}
func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, namespace string, methodName string) (context.Context, ResourceClient, error) {
func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, namespace string) (context.Context, SearchClient, error) {
ringHasher := fnv.New32a()
_, err := ringHasher.Write([]byte(namespace))
if err != nil {

View File

@@ -34,12 +34,17 @@ import (
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/resource")
type SearchServer interface {
LifecycleHooks
resourcepb.ResourceIndexServer
resourcepb.ManagedObjectIndexServer
}
// ResourceServer implements all gRPC services
type ResourceServer interface {
resourcepb.ResourceStoreServer
resourcepb.BulkStoreServer
resourcepb.ResourceIndexServer
resourcepb.ManagedObjectIndexServer
resourcepb.BlobStoreServer
resourcepb.DiagnosticsServer
resourcepb.QuotasServer
@@ -222,7 +227,8 @@ type ResourceServerOptions struct {
Blob BlobConfig
// Search options
Search SearchOptions
SearchOptions SearchOptions // TODO: needed?
Search SearchServer
// Quota service
OverridesService *OverridesService
@@ -251,16 +257,12 @@ type ResourceServerOptions struct {
storageMetrics *StorageMetrics
IndexMetrics *BleveIndexMetrics
// MaxPageSizeBytes is the maximum size of a page in bytes.
MaxPageSizeBytes int
// QOSQueue is the quality of service queue used to enqueue
QOSQueue QOSEnqueuer
QOSConfig QueueConfig
OwnsIndexFn func(key NamespacedResource) (bool, error)
}
func NewResourceServer(opts ResourceServerOptions) (*server, error) {
@@ -343,23 +345,25 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) {
ctx: ctx,
cancel: cancel,
storageMetrics: opts.storageMetrics,
indexMetrics: opts.IndexMetrics,
maxPageSizeBytes: opts.MaxPageSizeBytes,
reg: opts.Reg,
queue: opts.QOSQueue,
queueConfig: opts.QOSConfig,
overridesService: opts.OverridesService,
search: opts.Search,
artificialSuccessfulWriteDelay: opts.Search.IndexMinUpdateInterval,
artificialSuccessfulWriteDelay: opts.SearchOptions.IndexMinUpdateInterval,
}
if opts.Search.Resources != nil {
var err error
s.search, err = newSearchSupport(opts.Search, s.backend, s.access, s.blob, opts.IndexMetrics, opts.OwnsIndexFn)
if err != nil {
return nil, err
/*
if opts.Search.Resources != nil {
var err error
s.search, err = newSearchSupport(opts.Search, s.backend, s.access, s.blob, opts.IndexMetrics, opts.OwnsIndexFn)
if err != nil {
return nil, err
}
}
}
*/
err := s.Init(ctx)
if err != nil {
@@ -377,7 +381,7 @@ type server struct {
backend StorageBackend
blob BlobSupport
secure secrets.InlineSecureValueSupport
search *searchSupport
search SearchServer
diagnostics resourcepb.DiagnosticsServer
access claims.AccessClient
writeHooks WriteAccessHooks
@@ -424,11 +428,6 @@ func (s *server) Init(ctx context.Context) error {
s.initErr = s.overridesService.init(ctx)
}
// initialize the search index
if s.initErr == nil && s.search != nil {
s.initErr = s.search.init(ctx)
}
// Start watching for changes
if s.initErr == nil {
s.initErr = s.initWatcher()
@@ -453,10 +452,6 @@ func (s *server) Stop(ctx context.Context) error {
}
}
if s.search != nil {
s.search.stop()
}
if s.overridesService != nil {
if err := s.overridesService.stop(ctx); err != nil {
stopFailed = true
@@ -1372,47 +1367,6 @@ func (s *server) Watch(req *resourcepb.WatchRequest, srv resourcepb.ResourceStor
}
}
func (s *server) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest) (*resourcepb.ResourceSearchResponse, error) {
if s.search == nil {
return nil, fmt.Errorf("search index not configured")
}
return s.search.Search(ctx, req)
}
// GetStats implements ResourceServer.
func (s *server) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest) (*resourcepb.ResourceStatsResponse, error) {
if err := s.Init(ctx); err != nil {
return nil, err
}
if s.search == nil {
// If the backend implements "GetStats", we can use it
srv, ok := s.backend.(resourcepb.ResourceIndexServer)
if ok {
return srv.GetStats(ctx, req)
}
return nil, fmt.Errorf("search index not configured")
}
return s.search.GetStats(ctx, req)
}
func (s *server) ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest) (*resourcepb.ListManagedObjectsResponse, error) {
if s.search == nil {
return nil, fmt.Errorf("search index not configured")
}
return s.search.ListManagedObjects(ctx, req)
}
func (s *server) CountManagedObjects(ctx context.Context, req *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) {
if s.search == nil {
return nil, fmt.Errorf("search index not configured")
}
return s.search.CountManagedObjects(ctx, req)
}
// IsHealthy implements ResourceServer.
func (s *server) IsHealthy(ctx context.Context, req *resourcepb.HealthCheckRequest) (*resourcepb.HealthCheckResponse, error) {
return s.diagnostics.IsHealthy(ctx, req)

View File

@@ -0,0 +1,83 @@
package sql
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
var _ resource.SearchServer = (*remoteSearchClient)(nil)
// remoteSearchClient wraps gRPC search clients to implement the SearchServer interface.
// This allows the storage server to delegate search operations to a remote search server.
type remoteSearchClient struct {
conn *grpc.ClientConn
index resourcepb.ResourceIndexClient
moiClient resourcepb.ManagedObjectIndexClient
}
// newRemoteSearchClient creates a new remote search client that connects to a search server at the given address.
func newRemoteSearchClient(address string) (*remoteSearchClient, error) {
if address == "" {
return nil, fmt.Errorf("search server address is required for remote search mode")
}
conn, err := grpc.NewClient(address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)
if err != nil {
return nil, fmt.Errorf("failed to create gRPC connection to search server: %w", err)
}
return &remoteSearchClient{
conn: conn,
index: resourcepb.NewResourceIndexClient(conn),
moiClient: resourcepb.NewManagedObjectIndexClient(conn),
}, nil
}
// Init implements resource.LifecycleHooks.
// For remote search, there's nothing to initialize locally.
func (r *remoteSearchClient) Init(ctx context.Context) error {
return nil
}
// Stop implements resource.LifecycleHooks.
// Closes the gRPC connection.
func (r *remoteSearchClient) Stop(ctx context.Context) error {
if r.conn != nil {
return r.conn.Close()
}
return nil
}
// Search implements resourcepb.ResourceIndexServer.
func (r *remoteSearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest) (*resourcepb.ResourceSearchResponse, error) {
return r.index.Search(ctx, req)
}
// GetStats implements resourcepb.ResourceIndexServer.
func (r *remoteSearchClient) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest) (*resourcepb.ResourceStatsResponse, error) {
return r.index.GetStats(ctx, req)
}
// RebuildIndexes implements resourcepb.ResourceIndexServer.
func (r *remoteSearchClient) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest) (*resourcepb.RebuildIndexesResponse, error) {
return r.index.RebuildIndexes(ctx, req)
}
// CountManagedObjects implements resourcepb.ManagedObjectIndexServer.
func (r *remoteSearchClient) CountManagedObjects(ctx context.Context, req *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) {
return r.moiClient.CountManagedObjects(ctx, req)
}
// ListManagedObjects implements resourcepb.ManagedObjectIndexServer.
func (r *remoteSearchClient) ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest) (*resourcepb.ListManagedObjectsResponse, error) {
return r.moiClient.ListManagedObjects(ctx, req)
}

View File

@@ -0,0 +1,353 @@
package sql
import (
"context"
"errors"
"fmt"
"hash/fnv"
"net"
"os"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/health/grpc_health_v1"
"github.com/grafana/dskit/kv"
"github.com/grafana/dskit/netutil"
"github.com/grafana/dskit/ring"
"github.com/grafana/dskit/services"
infraDB "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/modules"
"github.com/grafana/grafana/pkg/services/authz"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/grpcserver"
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search"
)
var (
_ SearchGrpcService = (*searchService)(nil)
)
// SearchGrpcService is the interface for the standalone search gRPC service.
type SearchGrpcService interface {
services.NamedService
// GetAddress returns the address where this service is running
GetAddress() string
}
type searchService struct {
*services.BasicService
// Subservices manager
subservices *services.Manager
subservicesWatcher *services.FailureWatcher
hasSubservices bool
cfg *setting.Cfg
features featuremgmt.FeatureToggles
db infraDB.DB
stopCh chan struct{}
stoppedCh chan error
handler grpcserver.Provider
tracing trace.Tracer
authenticator func(ctx context.Context) (context.Context, error)
log log.Logger
reg prometheus.Registerer
indexMetrics *resource.BleveIndexMetrics
docBuilders resource.DocumentBuilderSupplier
searchRing *ring.Ring
ringLifecycler *ring.BasicLifecycler
backend resource.StorageBackend
}
// ProvideSearchGrpcService creates a standalone search gRPC service.
// This is used when running search-server as a separate target.
func ProvideSearchGrpcService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
db infraDB.DB,
log log.Logger,
reg prometheus.Registerer,
docBuilders resource.DocumentBuilderSupplier,
indexMetrics *resource.BleveIndexMetrics,
searchRing *ring.Ring,
memberlistKVConfig kv.Config,
backend resource.StorageBackend,
) (SearchGrpcService, error) {
tracer := otel.Tracer("search-server")
authn := NewAuthenticatorWithFallback(cfg, reg, tracer, func(ctx context.Context) (context.Context, error) {
auth := grpc.Authenticator{Tracer: tracer}
return auth.Authenticate(ctx)
})
s := &searchService{
cfg: cfg,
features: features,
stopCh: make(chan struct{}),
stoppedCh: make(chan error, 1),
authenticator: authn,
tracing: tracer,
db: db,
log: log,
reg: reg,
docBuilders: docBuilders,
indexMetrics: indexMetrics,
searchRing: searchRing,
subservicesWatcher: services.NewFailureWatcher(),
backend: backend,
}
subservices := []services.Service{}
if cfg.EnableSharding {
ringStore, err := kv.NewClient(
memberlistKVConfig,
ring.GetCodec(),
kv.RegistererWithKVName(reg, resource.RingName),
log,
)
if err != nil {
return nil, fmt.Errorf("failed to create KV store client: %s", err)
}
lifecyclerCfg, err := toSearchLifecyclerConfig(cfg, log)
if err != nil {
return nil, fmt.Errorf("failed to initialize search-ring lifecycler config: %s", err)
}
delegate := ring.BasicLifecyclerDelegate(ring.NewInstanceRegisterDelegate(ring.JOINING, resource.RingNumTokens))
delegate = ring.NewLeaveOnStoppingDelegate(delegate, log)
delegate = ring.NewAutoForgetDelegate(resource.RingHeartbeatTimeout*2, delegate, log)
s.ringLifecycler, err = ring.NewBasicLifecycler(
lifecyclerCfg,
resource.RingName,
resource.RingKey,
ringStore,
delegate,
log,
reg,
)
if err != nil {
return nil, fmt.Errorf("failed to initialize search-ring lifecycler: %s", err)
}
s.ringLifecycler.SetKeepInstanceInTheRingOnShutdown(true)
subservices = append(subservices, s.ringLifecycler)
}
if len(subservices) > 0 {
s.hasSubservices = true
var err error
s.subservices, err = services.NewManager(subservices...)
if err != nil {
return nil, fmt.Errorf("failed to create subservices manager: %w", err)
}
}
s.BasicService = services.NewBasicService(s.starting, s.running, s.stopping).WithName(modules.SearchServer)
return s, nil
}
func (s *searchService) OwnsIndex(key resource.NamespacedResource) (bool, error) {
if s.searchRing == nil {
return true, nil
}
if st := s.searchRing.State(); st != services.Running {
return false, fmt.Errorf("ring is not Running: %s", st)
}
ringHasher := fnv.New32a()
_, err := ringHasher.Write([]byte(key.Namespace))
if err != nil {
return false, fmt.Errorf("error hashing namespace: %w", err)
}
rs, err := s.searchRing.GetWithOptions(ringHasher.Sum32(), searchOwnerRead, ring.WithReplicationFactor(s.searchRing.ReplicationFactor()))
if err != nil {
return false, fmt.Errorf("error getting replicaset from ring: %w", err)
}
return rs.Includes(s.ringLifecycler.GetInstanceAddr()), nil
}
func (s *searchService) starting(ctx context.Context) error {
if s.hasSubservices {
s.subservicesWatcher.WatchManager(s.subservices)
if err := services.StartManagerAndAwaitHealthy(ctx, s.subservices); err != nil {
return fmt.Errorf("failed to start subservices: %w", err)
}
}
authzClient, err := authz.ProvideStandaloneAuthZClient(s.cfg, s.features, s.tracing, s.reg)
if err != nil {
return err
}
searchOptions, err := search.NewSearchOptions(s.features, s.cfg, s.docBuilders, s.indexMetrics, s.OwnsIndex)
if err != nil {
return err
}
// Create search server
searchServer, err := resource.NewSearchServer(searchOptions, s.backend, authzClient, nil, s.indexMetrics, s.OwnsIndex)
if err != nil {
return fmt.Errorf("failed to create search server: %w", err)
}
if err := searchServer.Init(ctx); err != nil {
return fmt.Errorf("failed to initialize search server: %w", err)
}
s.handler, err = grpcserver.ProvideService(s.cfg, s.features, interceptors.AuthenticatorFunc(s.authenticator), s.tracing, prometheus.DefaultRegisterer)
if err != nil {
return err
}
srv := s.handler.GetServer()
resourcepb.RegisterResourceIndexServer(srv, searchServer)
resourcepb.RegisterManagedObjectIndexServer(srv, searchServer)
grpc_health_v1.RegisterHealthServer(srv, &searchHealthService{searchServer: searchServer})
// register reflection service
_, err = grpcserver.ProvideReflectionService(s.cfg, s.handler)
if err != nil {
return err
}
if s.cfg.EnableSharding {
s.log.Info("waiting until search server is JOINING in the ring")
lfcCtx, cancel := context.WithTimeout(context.Background(), s.cfg.ResourceServerJoinRingTimeout)
defer cancel()
if err := ring.WaitInstanceState(lfcCtx, s.searchRing, s.ringLifecycler.GetInstanceID(), ring.JOINING); err != nil {
return fmt.Errorf("error switching to JOINING in the ring: %s", err)
}
s.log.Info("search server is JOINING in the ring")
if err := s.ringLifecycler.ChangeState(ctx, ring.ACTIVE); err != nil {
return fmt.Errorf("error switching to ACTIVE in the ring: %s", err)
}
s.log.Info("search server is ACTIVE in the ring")
}
// start the gRPC server
go func() {
err := s.handler.Run(ctx)
if err != nil {
s.stoppedCh <- err
} else {
s.stoppedCh <- nil
}
}()
return nil
}
func (s *searchService) GetAddress() string {
return s.handler.GetAddress()
}
func (s *searchService) running(ctx context.Context) error {
select {
case err := <-s.stoppedCh:
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
case err := <-s.subservicesWatcher.Chan():
return fmt.Errorf("subservice failure: %w", err)
case <-ctx.Done():
close(s.stopCh)
}
return nil
}
func (s *searchService) stopping(_ error) error {
if s.hasSubservices {
err := services.StopManagerAndAwaitStopped(context.Background(), s.subservices)
if err != nil {
return fmt.Errorf("failed to stop subservices: %w", err)
}
}
return nil
}
func toSearchLifecyclerConfig(cfg *setting.Cfg, logger log.Logger) (ring.BasicLifecyclerConfig, error) {
instanceAddr, err := ring.GetInstanceAddr(cfg.MemberlistBindAddr, netutil.PrivateNetworkInterfacesWithFallback([]string{"eth0", "en0"}, logger), logger, true)
if err != nil {
return ring.BasicLifecyclerConfig{}, err
}
instanceId := cfg.InstanceID
if instanceId == "" {
hostname, err := os.Hostname()
if err != nil {
return ring.BasicLifecyclerConfig{}, err
}
instanceId = hostname
}
_, grpcPortStr, err := net.SplitHostPort(cfg.GRPCServer.Address)
if err != nil {
return ring.BasicLifecyclerConfig{}, fmt.Errorf("could not get grpc port from grpc server address: %s", err)
}
grpcPort, err := strconv.Atoi(grpcPortStr)
if err != nil {
return ring.BasicLifecyclerConfig{}, fmt.Errorf("error converting grpc address port to int: %s", err)
}
return ring.BasicLifecyclerConfig{
Addr: fmt.Sprintf("%s:%d", instanceAddr, grpcPort),
ID: instanceId,
HeartbeatPeriod: 15 * time.Second,
HeartbeatTimeout: resource.RingHeartbeatTimeout,
TokensObservePeriod: 0,
NumTokens: resource.RingNumTokens,
}, nil
}
// searchHealthService implements the health check for the search service.
type searchHealthService struct {
searchServer resource.SearchServer
}
func (h *searchHealthService) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
return &grpc_health_v1.HealthCheckResponse{
Status: grpc_health_v1.HealthCheckResponse_SERVING,
}, nil
}
func (h *searchHealthService) Watch(req *grpc_health_v1.HealthCheckRequest, server grpc_health_v1.Health_WatchServer) error {
return fmt.Errorf("watch not implemented")
}
func (h *searchHealthService) List(ctx context.Context, req *grpc_health_v1.HealthListRequest) (*grpc_health_v1.HealthListResponse, error) {
check, err := h.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
if err != nil {
return nil, err
}
return &grpc_health_v1.HealthListResponse{
Statuses: map[string]*grpc_health_v1.HealthCheckResponse{
"": check,
},
}, nil
}

View File

@@ -48,7 +48,7 @@ type ServerOptions struct {
OwnsIndexFn func(key resource.NamespacedResource) (bool, error)
}
func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) {
func NewResourceServer(opts ServerOptions) (resource.ResourceServer, resource.SearchServer, error) {
apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver")
if opts.SecureValues == nil && opts.Cfg != nil && opts.Cfg.SecretsManagement.GrpcClientEnable {
@@ -59,7 +59,7 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) {
nil, // not needed for gRPC client mode
)
if err != nil {
return nil, fmt.Errorf("failed to create inline secure value service: %w", err)
return nil, nil, fmt.Errorf("failed to create inline secure value service: %w", err)
}
opts.SecureValues = inlineSecureValueService
}
@@ -79,7 +79,7 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) {
dir := strings.Replace(serverOptions.Blob.URL, "./data", opts.Cfg.DataPath, 1)
err := os.MkdirAll(dir, 0700)
if err != nil {
return nil, err
return nil, nil, err
}
serverOptions.Blob.URL = "file:///" + dir
}
@@ -96,13 +96,13 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) {
} else {
eDB, err := dbimpl.ProvideResourceDB(opts.DB, opts.Cfg, opts.Tracer)
if err != nil {
return nil, err
return nil, nil, err
}
if opts.Cfg.EnableSQLKVBackend {
sqlkv, err := resource.NewSQLKV(eDB)
if err != nil {
return nil, fmt.Errorf("error creating sqlkv: %s", err)
return nil, nil, fmt.Errorf("error creating sqlkv: %s", err)
}
kvBackendOpts := resource.KVBackendOptions{
@@ -114,12 +114,12 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) {
ctx := context.Background()
dbConn, err := eDB.Init(ctx)
if err != nil {
return nil, fmt.Errorf("error initializing DB: %w", err)
return nil, nil, fmt.Errorf("error initializing DB: %w", err)
}
dialect := sqltemplate.DialectForDriver(dbConn.DriverName())
if dialect == nil {
return nil, fmt.Errorf("unsupported database driver: %s", dbConn.DriverName())
return nil, nil, fmt.Errorf("unsupported database driver: %s", dbConn.DriverName())
}
rvManager, err := rvmanager.NewResourceVersionManager(rvmanager.ResourceManagerOptions{
@@ -127,14 +127,14 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) {
DB: dbConn,
})
if err != nil {
return nil, fmt.Errorf("failed to create resource version manager: %w", err)
return nil, nil, fmt.Errorf("failed to create resource version manager: %w", err)
}
// TODO add config to decide whether to pass RvManager or not
kvBackendOpts.RvManager = rvManager
kvBackend, err := resource.NewKVStorageBackend(kvBackendOpts)
if err != nil {
return nil, fmt.Errorf("error creating kv backend: %s", err)
return nil, nil, fmt.Errorf("error creating kv backend: %s", err)
}
serverOptions.Backend = kvBackend
@@ -151,7 +151,7 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) {
LastImportTimeMaxAge: opts.SearchOptions.MaxIndexAge, // No need to keep last_import_times older than max index age.
})
if err != nil {
return nil, err
return nil, nil, err
}
serverOptions.Backend = backend
serverOptions.Diagnostics = backend
@@ -159,13 +159,31 @@ func NewResourceServer(opts ServerOptions) (resource.ResourceServer, error) {
}
}
serverOptions.Search = opts.SearchOptions
serverOptions.IndexMetrics = opts.IndexMetrics
// Initialize the backend before creating search server (it needs the DB connection)
if serverOptions.Lifecycle != nil {
if err := serverOptions.Lifecycle.Init(context.Background()); err != nil {
return nil, nil, fmt.Errorf("failed to initialize backend: %w", err)
}
}
search, err := resource.NewSearchServer(opts.SearchOptions, serverOptions.Backend, opts.AccessClient, nil, opts.IndexMetrics, opts.OwnsIndexFn)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize search: %w", err)
}
if err := search.Init(context.Background()); err != nil {
return nil, nil, fmt.Errorf("failed to initialize search: %w", err)
}
serverOptions.Search = search
serverOptions.QOSQueue = opts.QOSQueue
serverOptions.OwnsIndexFn = opts.OwnsIndexFn
serverOptions.OverridesService = opts.OverridesService
return resource.NewResourceServer(serverOptions)
rs, err := resource.NewResourceServer(serverOptions)
if err != nil {
_ = search.Stop(context.Background())
}
return rs, search, err
}
// isHighAvailabilityEnabled determines if high availability mode should

View File

@@ -292,10 +292,50 @@ func (s *service) starting(ctx context.Context) error {
serverOptions.OverridesService = overridesSvc
}
server, err := NewResourceServer(serverOptions)
if err != nil {
return err
// Handle search mode: "", "embedded", or "remote"
// Empty string defaults to "embedded" for backward compatibility
var searchServer resource.SearchServer
registerSearchServices := true
switch s.cfg.SearchMode {
case "remote":
// Use remote search client - don't register search services locally
s.log.Info("Using remote search server", "address", s.cfg.SearchServerAddress)
remoteSearch, err := newRemoteSearchClient(s.cfg.SearchServerAddress)
if err != nil {
return fmt.Errorf("failed to create remote search client: %w", err)
}
searchServer = remoteSearch
registerSearchServices = false
case "", "embedded":
// Default: create local search server (backward compatible)
s.log.Info("Using embedded search server")
// SearchOptions are already configured, NewResourceServer will create the search server
default:
return fmt.Errorf("invalid search_mode: %s (valid values: \"\", \"embedded\", \"remote\")", s.cfg.SearchMode)
}
var server resource.ResourceServer
if searchServer != nil {
// Remote search mode: pass the remote search client to the resource server
// Clear local search options since we're using remote search
serverOptions.SearchOptions = resource.SearchOptions{}
var err error
server, _, err = NewResourceServer(serverOptions)
if err != nil {
return err
}
} else {
// Embedded mode: create both server and search server together
var err error
server, searchServer, err = NewResourceServer(serverOptions)
if err != nil {
return err
}
}
s.handler, err = grpcserver.ProvideService(s.cfg, s.features, interceptors.AuthenticatorFunc(s.authenticator), s.tracing, prometheus.DefaultRegisterer)
if err != nil {
return err
@@ -309,8 +349,11 @@ func (s *service) starting(ctx context.Context) error {
srv := s.handler.GetServer()
resourcepb.RegisterResourceStoreServer(srv, server)
resourcepb.RegisterBulkStoreServer(srv, server)
resourcepb.RegisterResourceIndexServer(srv, server)
resourcepb.RegisterManagedObjectIndexServer(srv, server)
// Only register search services if running in embedded mode
if registerSearchServices {
resourcepb.RegisterResourceIndexServer(srv, searchServer)
resourcepb.RegisterManagedObjectIndexServer(srv, searchServer)
}
resourcepb.RegisterBlobStoreServer(srv, server)
resourcepb.RegisterDiagnosticsServer(srv, server)
resourcepb.RegisterQuotasServer(srv, server)

View File

@@ -32,6 +32,7 @@ func RunTestSearchAndStorage(t *testing.T, ctx context.Context, backend resource
nsPrefix := "test-ns"
var server resource.ResourceServer
var searchServer resource.SearchServer
t.Run("Create initial resources in storage", func(t *testing.T) {
initialResources := []struct {
@@ -96,25 +97,35 @@ func RunTestSearchAndStorage(t *testing.T, ctx context.Context, backend resource
})
t.Run("Create a resource server with both backends", func(t *testing.T) {
// Create a resource server with both backends
// Create search server first
var err error
server, err = resource.NewResourceServer(resource.ResourceServerOptions{
Backend: backend,
Search: resource.SearchOptions{
Backend: searchBackend,
Resources: &resource.TestDocumentBuilderSupplier{
GroupsResources: map[string]string{
"test.grafana.app": "testresources",
},
searchOpts := resource.SearchOptions{
Backend: searchBackend,
Resources: &resource.TestDocumentBuilderSupplier{
GroupsResources: map[string]string{
"test.grafana.app": "testresources",
},
},
}
searchServer, err = resource.NewSearchServer(searchOpts, backend, nil, nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, searchServer)
// Initialize the search server
err = searchServer.Init(ctx)
require.NoError(t, err)
// Create a resource server with the search server
server, err = resource.NewResourceServer(resource.ResourceServerOptions{
Backend: backend,
Search: searchServer,
})
require.NoError(t, err)
})
t.Run("Search for initial resources", func(t *testing.T) {
// Test 1: Search for initial resources
searchResp, err := server.Search(ctx, &resourcepb.ResourceSearchRequest{
searchResp, err := searchServer.Search(ctx, &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: "test.grafana.app",
@@ -194,7 +205,7 @@ func RunTestSearchAndStorage(t *testing.T, ctx context.Context, backend resource
})
t.Run("Search for documents", func(t *testing.T) {
searchResp, err := server.Search(ctx, &resourcepb.ResourceSearchRequest{
searchResp, err := searchServer.Search(ctx, &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: "test.grafana.app",
@@ -212,7 +223,7 @@ func RunTestSearchAndStorage(t *testing.T, ctx context.Context, backend resource
})
t.Run("Search with tags", func(t *testing.T) {
searchResp, err := server.Search(ctx, &resourcepb.ResourceSearchRequest{
searchResp, err := searchServer.Search(ctx, &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: "test.grafana.app",
@@ -231,7 +242,7 @@ func RunTestSearchAndStorage(t *testing.T, ctx context.Context, backend resource
require.Equal(t, int64(0), searchResp.TotalHits)
// this is the correct way of searching by tag
searchResp, err = server.Search(ctx, &resourcepb.ResourceSearchRequest{
searchResp, err = searchServer.Search(ctx, &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: "test.grafana.app",
@@ -253,7 +264,7 @@ func RunTestSearchAndStorage(t *testing.T, ctx context.Context, backend resource
})
t.Run("Search with specific tag", func(t *testing.T) {
searchResp, err := server.Search(ctx, &resourcepb.ResourceSearchRequest{
searchResp, err := searchServer.Search(ctx, &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: "test.grafana.app",
@@ -272,7 +283,7 @@ func RunTestSearchAndStorage(t *testing.T, ctx context.Context, backend resource
require.Equal(t, int64(0), searchResp.TotalHits)
// this is the correct way of searching by tag
searchResp, err = server.Search(ctx, &resourcepb.ResourceSearchRequest{
searchResp, err = searchServer.Search(ctx, &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Group: "test.grafana.app",

View File

@@ -62,7 +62,7 @@ func TestIntegrationTestDatasource(t *testing.T) {
t.Run("Admin configs", func(t *testing.T) {
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
Group: "grafana-testdata-datasource.datasource.grafana.app",
Group: "testdata.datasource.grafana.app",
Version: "v0alpha1",
Resource: "datasources",
}).Namespace("default")
@@ -92,7 +92,7 @@ func TestIntegrationTestDatasource(t *testing.T) {
t.Run("Call subresources", func(t *testing.T) {
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
Group: "grafana-testdata-datasource.datasource.grafana.app",
Group: "testdata.datasource.grafana.app",
Version: "v0alpha1",
Resource: "datasources",
}).Namespace("default")
@@ -128,7 +128,7 @@ func TestIntegrationTestDatasource(t *testing.T) {
raw := apis.DoRequest[any](helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: "GET",
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test/resource",
Path: "/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test/resource",
}, nil)
// endpoint is disabled currently because it has not been
// sufficiently tested.

View File

@@ -2,10 +2,10 @@
"openapi": "3.0.0",
"info": {
"description": "Generates test data in different forms",
"title": "grafana-testdata-datasource.datasource.grafana.app/v0alpha1"
"title": "testdata.datasource.grafana.app/v0alpha1"
},
"paths": {
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/": {
"/apis/testdata.datasource.grafana.app/v0alpha1/": {
"get": {
"tags": [
"API Discovery"
@@ -36,7 +36,7 @@
}
}
},
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/query": {
"/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/query": {
"post": {
"tags": [
"Connections (deprecated)"
@@ -68,7 +68,7 @@
"deprecated": true,
"x-kubernetes-action": "connect",
"x-kubernetes-group-version-kind": {
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"version": "v0alpha1",
"kind": "QueryDataResponse"
}
@@ -96,7 +96,7 @@
}
]
},
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources": {
"/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources": {
"get": {
"tags": [
"DataSource"
@@ -137,7 +137,7 @@
},
"x-kubernetes-action": "list",
"x-kubernetes-group-version-kind": {
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"version": "v0alpha1",
"kind": "DataSource"
}
@@ -254,7 +254,7 @@
}
]
},
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}": {
"/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}": {
"get": {
"tags": [
"DataSource"
@@ -285,7 +285,7 @@
},
"x-kubernetes-action": "get",
"x-kubernetes-group-version-kind": {
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"version": "v0alpha1",
"kind": "DataSource"
}
@@ -322,7 +322,7 @@
}
]
},
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}/health": {
"/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}/health": {
"get": {
"tags": [
"DataSource"
@@ -343,7 +343,7 @@
},
"x-kubernetes-action": "connect",
"x-kubernetes-group-version-kind": {
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"version": "v0alpha1",
"kind": "HealthCheckResult"
}
@@ -371,7 +371,7 @@
}
]
},
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}/query": {
"/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}/query": {
"post": {
"tags": [
"DataSource"
@@ -401,7 +401,7 @@
},
"x-kubernetes-action": "connect",
"x-kubernetes-group-version-kind": {
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"version": "v0alpha1",
"kind": "QueryDataResponse"
}
@@ -429,7 +429,7 @@
}
]
},
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}/resource": {
"/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}/resource": {
"get": {
"tags": [
"DataSource"
@@ -450,7 +450,7 @@
},
"x-kubernetes-action": "connect",
"x-kubernetes-group-version-kind": {
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"version": "v0alpha1",
"kind": "Status"
}
@@ -478,7 +478,7 @@
}
]
},
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/queryconvert/{name}": {
"/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/{namespace}/queryconvert/{name}": {
"post": {
"tags": [
"QueryDataRequest"
@@ -499,7 +499,7 @@
},
"x-kubernetes-action": "connect",
"x-kubernetes-group-version-kind": {
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"version": "v0alpha1",
"kind": "QueryDataRequest"
}
@@ -620,7 +620,7 @@
"apiVersion": {
"type": "string",
"enum": [
"grafana-testdata-datasource.datasource.grafana.app/v0alpha1"
"testdata.datasource.grafana.app/v0alpha1"
]
},
"kind": {
@@ -660,7 +660,7 @@
},
"x-kubernetes-group-version-kind": [
{
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"kind": "DataSource",
"version": "v0alpha1"
}
@@ -703,7 +703,7 @@
},
"x-kubernetes-group-version-kind": [
{
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"kind": "DataSourceList",
"version": "v0alpha1"
}
@@ -744,7 +744,7 @@
},
"x-kubernetes-group-version-kind": [
{
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"kind": "HealthCheckResult",
"version": "v0alpha1"
}
@@ -833,7 +833,7 @@
},
"x-kubernetes-group-version-kind": [
{
"group": "grafana-testdata-datasource.datasource.grafana.app",
"group": "testdata.datasource.grafana.app",
"kind": "QueryDataResponse",
"version": "v0alpha1"
}

View File

@@ -124,7 +124,7 @@ func TestIntegrationOpenAPIs(t *testing.T) {
Group: "shorturl.grafana.app",
Version: "v1beta1",
}, {
Group: "grafana-testdata-datasource.datasource.grafana.app",
Group: "testdata.datasource.grafana.app",
Version: "v0alpha1",
}, {
Group: "logsdrilldown.grafana.app",

View File

@@ -28,17 +28,18 @@ describe('DatasourceAPIVersions', () => {
it('get', async () => {
const getMock = jest.fn().mockResolvedValue({
groups: [
{ name: 'grafana-testdata-datasource.datasource.grafana.app', preferredVersion: { version: 'v1' } },
{ name: 'testdata.datasource.grafana.app', preferredVersion: { version: 'v1' } },
{ name: 'prometheus.datasource.grafana.app', preferredVersion: { version: 'v2' } },
{ name: 'myorg-myplugin.datasource.grafana.app', preferredVersion: { version: 'v3' } },
],
});
getBackendSrv().get = getMock;
const apiVersions = new DatasourceAPIVersions();
expect(await apiVersions.get('testdata')).toBe('v1');
expect(await apiVersions.get('grafana-testdata-datasource')).toBe('v1');
expect(await apiVersions.get('prometheus')).toBe('v2');
expect(await apiVersions.get('graphite')).toBeUndefined();
expect(await apiVersions.get('myorg-myplugin')).toBe('v3');
expect(await apiVersions.get('myorg-myplugin-datasource')).toBe('v3');
expect(getMock).toHaveBeenCalledTimes(1);
expect(getMock).toHaveBeenCalledWith('/apis');
});

View File

@@ -162,6 +162,17 @@ export class DatasourceAPIVersions {
if (group.name.includes('datasource.grafana.app')) {
const id = group.name.split('.')[0];
apiVersions[id] = group.preferredVersion.version;
// workaround for plugins that don't append '-datasource' for the group name
// e.g. org-plugin-datasource uses org-plugin.datasource.grafana.app
if (!id.endsWith('-datasource')) {
if (!id.includes('-')) {
// workaroud for Grafana plugins that don't include the org either
// e.g. testdata uses testdata.datasource.grafana.app
apiVersions[`grafana-${id}-datasource`] = group.preferredVersion.version;
} else {
apiVersions[`${id}-datasource`] = group.preferredVersion.version;
}
}
}
});
this.apiVersions = apiVersions;

View File

@@ -4,7 +4,6 @@ import { useAsyncRetry } from 'react-use';
import { GrafanaTheme2, store } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
import { Button, CollapsableSection, Spinner, Stack, Text, useStyles2, Grid } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
@@ -36,18 +35,10 @@ export function RecentlyViewedDashboards() {
const { foldersByUid } = useDashboardLocationInfo(recentDashboards.length > 0);
const handleClearHistory = () => {
reportInteraction('grafana_recently_viewed_dashboards_clear_history');
store.set(recentDashboardsKey, JSON.stringify([]));
retry();
};
const handleSectionToggle = () => {
reportInteraction('grafana_recently_viewed_dashboards_toggle_section', {
expanded: !isOpen,
});
setIsOpen(!isOpen);
};
if (!evaluateBooleanFlag('recentlyViewedDashboards', false) || recentDashboards.length === 0) {
return null;
}
@@ -57,7 +48,7 @@ export function RecentlyViewedDashboards() {
headerDataTestId="browseDashboardsRecentlyViewedTitle"
label={
<Stack direction="row" justifyContent="space-between" alignItems="baseline" width="100%">
<Text variant="h5" element="h3" onClick={handleSectionToggle}>
<Text variant="h5" element="h3" onClick={() => setIsOpen(!isOpen)}>
<Trans i18nKey="browse-dashboards.recently-viewed.title">Recently viewed</Trans>
</Text>
<Button icon="times" size="xs" variant="secondary" fill="text" onClick={handleClearHistory}>
@@ -89,10 +80,9 @@ export function RecentlyViewedDashboards() {
{!loading && recentDashboards.length > 0 && (
<ul className={styles.list}>
<Grid columns={{ xs: 1, sm: 2, md: 3, lg: 5 }} gap={2}>
{recentDashboards.map((dash, idx) => (
{recentDashboards.map((dash) => (
<li key={dash.uid} className={styles.listItem}>
<DashListItem
order={idx + 1}
key={dash.uid}
dashboard={dash}
url={dash.url}

View File

@@ -1,13 +1,6 @@
import {
defaultDataQueryKind,
defaultPanelSpec,
PanelKind,
PanelQueryKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { defaultDataQueryKind, PanelQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { ensureUniqueRefIds, getPanelDataSource, getRuntimePanelDataSource } from './utils';
import { ensureUniqueRefIds, getRuntimePanelDataSource } from './utils';
describe('getRuntimePanelDataSource', () => {
it('should return uid and type when explicit datasource UID is provided', () => {
@@ -148,159 +141,6 @@ describe('getRuntimePanelDataSource', () => {
});
});
describe('getPanelDataSource', () => {
const createPanelWithQueries = (queries: PanelQueryKind[]): PanelKind => ({
kind: 'Panel',
spec: {
...defaultPanelSpec(),
id: 1,
title: 'Test Panel',
data: {
kind: 'QueryGroup',
spec: {
queries,
queryOptions: {},
transformations: [],
},
},
},
});
const createQuery = (datasourceName: string, group: string, refId = 'A'): PanelQueryKind => ({
kind: 'PanelQuery',
spec: {
refId,
hidden: false,
query: {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group,
datasource: {
name: datasourceName,
},
spec: {},
},
},
});
const createQueryWithoutDatasourceName = (group: string, refId = 'A'): PanelQueryKind => ({
kind: 'PanelQuery',
spec: {
refId,
hidden: false,
query: {
kind: 'DataQuery',
version: defaultDataQueryKind().version,
group,
spec: {},
},
},
});
it('should return undefined when panel has no queries', () => {
const panel = createPanelWithQueries([]);
const result = getPanelDataSource(panel);
expect(result).toBeUndefined();
});
it('should return undefined for a single query with specific datasource (not mixed)', () => {
const panel = createPanelWithQueries([createQuery('prometheus-uid', 'prometheus')]);
const result = getPanelDataSource(panel);
expect(result).toBeUndefined();
});
it('should return undefined for multiple queries with the same datasource', () => {
const panel = createPanelWithQueries([
createQuery('prometheus-uid', 'prometheus', 'A'),
createQuery('prometheus-uid', 'prometheus', 'B'),
createQuery('prometheus-uid', 'prometheus', 'C'),
]);
const result = getPanelDataSource(panel);
expect(result).toBeUndefined();
});
it('should return mixed datasource when queries use different datasource UIDs', () => {
const panel = createPanelWithQueries([
createQuery('prometheus-uid', 'prometheus', 'A'),
createQuery('loki-uid', 'loki', 'B'),
]);
const result = getPanelDataSource(panel);
expect(result).toEqual({ type: 'mixed', uid: MIXED_DATASOURCE_NAME });
});
it('should return mixed datasource when queries use different datasource types', () => {
const panel = createPanelWithQueries([
createQuery('ds-uid', 'prometheus', 'A'),
createQuery('ds-uid', 'loki', 'B'),
]);
const result = getPanelDataSource(panel);
expect(result).toEqual({ type: 'mixed', uid: MIXED_DATASOURCE_NAME });
});
it('should return mixed datasource when multiple queries use Dashboard datasource', () => {
const panel = createPanelWithQueries([
createQuery(SHARED_DASHBOARD_QUERY, 'datasource', 'A'),
createQuery(SHARED_DASHBOARD_QUERY, 'datasource', 'B'),
createQuery(SHARED_DASHBOARD_QUERY, 'datasource', 'C'),
]);
const result = getPanelDataSource(panel);
expect(result).toEqual({ type: 'mixed', uid: MIXED_DATASOURCE_NAME });
});
it('should return Dashboard datasource when single query uses Dashboard datasource', () => {
const panel = createPanelWithQueries([createQuery(SHARED_DASHBOARD_QUERY, 'datasource')]);
const result = getPanelDataSource(panel);
expect(result).toEqual({ type: 'datasource', uid: SHARED_DASHBOARD_QUERY });
});
it('should return mixed when Dashboard datasource is mixed with other datasources', () => {
const panel = createPanelWithQueries([
createQuery(SHARED_DASHBOARD_QUERY, 'datasource', 'A'),
createQuery('prometheus-uid', 'prometheus', 'B'),
]);
const result = getPanelDataSource(panel);
expect(result).toEqual({ type: 'mixed', uid: MIXED_DATASOURCE_NAME });
});
it('should return undefined when queries have no explicit datasource name but same type', () => {
const panel = createPanelWithQueries([
createQueryWithoutDatasourceName('prometheus', 'A'),
createQueryWithoutDatasourceName('prometheus', 'B'),
]);
const result = getPanelDataSource(panel);
expect(result).toBeUndefined();
});
it('should return mixed when queries have no explicit datasource name but different types', () => {
const panel = createPanelWithQueries([
createQueryWithoutDatasourceName('prometheus', 'A'),
createQueryWithoutDatasourceName('loki', 'B'),
]);
const result = getPanelDataSource(panel);
expect(result).toEqual({ type: 'mixed', uid: MIXED_DATASOURCE_NAME });
});
});
describe('ensureUniqueRefIds', () => {
const createQuery = (refId: string): PanelQueryKind => ({
kind: 'PanelQuery',

View File

@@ -23,7 +23,6 @@ import {
DataQueryKind,
defaultPanelQueryKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { ConditionalRenderingGroup } from '../../conditional-rendering/group/ConditionalRenderingGroup';
@@ -229,45 +228,29 @@ export function createPanelDataProvider(panelKind: PanelKind): SceneDataProvider
* This ensures v2→Scene→v1 conversion produces the same output as the Go backend,
* which does NOT add panel-level datasource for non-mixed panels.
*/
export function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
const queries = panel.spec.data?.spec.queries;
if (!queries?.length) {
function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
if (!panel.spec.data?.spec.queries?.length) {
return undefined;
}
// Check if multiple queries use Dashboard datasource - this needs mixed mode
const dashboardDsQueryCount = queries.filter((q) => q.spec.query.datasource?.name === SHARED_DASHBOARD_QUERY).length;
if (dashboardDsQueryCount > 1) {
return { type: 'mixed', uid: MIXED_DATASOURCE_NAME };
}
let firstDatasource: DataSourceRef | undefined = undefined;
let isMixedDatasource = false;
// Get all datasources from queries
const datasources = queries.map((query) =>
query.spec.query.datasource?.name
panel.spec.data.spec.queries.forEach((query) => {
const queryDs = query.spec.query.datasource?.name
? { uid: query.spec.query.datasource.name, type: query.spec.query.group }
: getRuntimePanelDataSource(query.spec.query)
);
: getRuntimePanelDataSource(query.spec.query);
const firstDatasource = datasources[0];
// Check if queries use different datasources
const isMixedDatasource = datasources.some(
(ds) => ds?.uid !== firstDatasource?.uid || ds?.type !== firstDatasource?.type
);
if (isMixedDatasource) {
return { type: 'mixed', uid: MIXED_DATASOURCE_NAME };
}
// Handle case where all queries use Dashboard datasource - needs to set datasource for proper data fetching
// See DashboardDatasourceBehaviour.tsx for more details
if (firstDatasource?.uid === SHARED_DASHBOARD_QUERY) {
return { type: 'datasource', uid: SHARED_DASHBOARD_QUERY };
}
if (!firstDatasource) {
firstDatasource = queryDs;
} else if (firstDatasource.uid !== queryDs?.uid || firstDatasource.type !== queryDs?.type) {
isMixedDatasource = true;
}
});
// Only return mixed datasource - for non-mixed panels, each query already has its own datasource
// This matches the Go backend behavior which doesn't add panel.datasource for non-mixed panels
return undefined;
return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : undefined;
}
/**

View File

@@ -149,6 +149,9 @@ const getDefaultVisualisationType = (): LogsVisualisationType => {
if (visualisationType === 'logs') {
return 'logs';
}
if (config.featureToggles.logsExploreTableDefaultVisualization) {
return 'table';
}
return 'logs';
};
@@ -444,6 +447,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
reportInteraction('grafana_explore_logs_visualisation_changed', {
newVisualizationType: visualisation,
datasourceType: props.datasourceType ?? 'unknown',
defaultVisualisationType: config.featureToggles.logsExploreTableDefaultVisualization ? 'table' : 'logs',
});
},
[panelState?.logs, props.datasourceType, updatePanelState]

View File

@@ -1,4 +1,3 @@
import { reportInteraction } from '@grafana/runtime';
import { Box, Card, Icon, Link, Stack, Text, useStyles2 } from '@grafana/ui';
import { LocationInfo } from 'app/features/search/service/types';
import { StarToolbarButton } from 'app/features/stars/StarToolbarButton';
@@ -12,26 +11,11 @@ interface Props {
showFolderNames: boolean;
locationInfo?: LocationInfo;
layoutMode: 'list' | 'card';
order?: number; // for rudderstack analytics to track position in card list
onStarChange?: (id: string, isStarred: boolean) => void;
}
export function DashListItem({
dashboard,
url,
showFolderNames,
locationInfo,
layoutMode,
order,
onStarChange,
}: Props) {
export function DashListItem({ dashboard, url, showFolderNames, locationInfo, layoutMode, onStarChange }: Props) {
const css = useStyles2(getStyles);
const onCardLinkClick = () => {
reportInteraction('grafana_recently_viewed_dashboards_click_card', {
cardOrder: order,
});
};
return (
<>
{layoutMode === 'list' ? (
@@ -55,9 +39,7 @@ export function DashListItem({
) : (
<Card className={css.dashlistCard} noMargin>
<Stack justifyContent="space-between" alignItems="center">
<Link href={url} onClick={onCardLinkClick}>
{dashboard.name}
</Link>
<Link href={url}>{dashboard.name}</Link>
<StarToolbarButton
title={dashboard.name}
group="dashboard.grafana.app"

View File

@@ -11,7 +11,13 @@ failed_checks=()
for file in "$ARTIFACTS_DIR"/*.tgz; do
echo "🔍 Checking NPM package: $file"
if ! NODE_OPTIONS="-C @grafana-app/source" yarn attw "$file" --ignore-rules "false-cjs" --profile "node16"; then
# TODO: Fix the error with @grafana/i18n/eslint-resolution
if [[ "$file" == *"@grafana-i18n"* ]]; then
ATTW_FLAGS="--profile node16"
fi
# shellcheck disable=SC2086
if ! NODE_OPTIONS="-C @grafana-app/source" yarn attw "$file" --ignore-rules "false-cjs" $ATTW_FLAGS; then
echo "attw check failed for $file"
echo ""
failed_checks+=("$file - yarn attw")