mirror of
https://github.com/grafana/grafana.git
synced 2026-01-14 21:25:50 +00:00
Compare commits
6 Commits
wb/plugins
...
feature/ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0bb71f834 | ||
|
|
a7aa55f908 | ||
|
|
0a61846b5e | ||
|
|
785cc739ee | ||
|
|
4913baaf04 | ||
|
|
c8f1efe7c7 |
11
.github/actions/change-detection/action.yml
vendored
11
.github/actions/change-detection/action.yml
vendored
@@ -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 }}"
|
||||
|
||||
4
.github/actions/setup-node/action.yml
vendored
4
.github/actions/setup-node/action.yml
vendored
@@ -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'
|
||||
66
.github/workflows/frontend-lint.yml
vendored
66
.github/workflows/frontend-lint.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,7 +24,6 @@ metaV0Alpha1: {
|
||||
translations?: [string]: string
|
||||
// +listType=atomic
|
||||
children?: [...string]
|
||||
aliasIds?: [...string]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
2
apps/plugins/pkg/apis/plugins_manifest.go
generated
2
apps/plugins/pkg/apis/plugins_manifest.go
generated
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -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
|
||||
|
||||
|
3
pkg/services/featuremgmt/toggles_gen.json
generated
3
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
83
pkg/storage/unified/sql/remote_search.go
Normal file
83
pkg/storage/unified/sql/remote_search.go
Normal 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)
|
||||
}
|
||||
353
pkg/storage/unified/sql/search_service.go
Normal file
353
pkg/storage/unified/sql/search_service.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user