Compare commits

..
Author SHA1 Message Date
Alejandro Malavet 3ef7d6d2a7 Merge branch 'main' into ale/apiextensions 2026-01-12 13:27:30 -06:00
Alejandro Malavet 8d20008b84 Merge branch 'feat/mt-apiextensions' into ale/apiextensions 2026-01-12 13:27:23 -06:00
Alejandro Malavet 14d2507f3b ignore private key 2026-01-12 13:27:13 -06:00
Nick Richmond 53aa5e8f7f MetricsDrilldown: Remove exploreMetricsRelatedLogs feature toggle (#116090)
chore: remove unused exploreMetricsRelatedLogs feature toggle
2026-01-12 12:52:40 -05:00
Ida Štambuk 69bf3068b3 Dashboards: Never show scopes variables (#116132) 2026-01-12 18:52:23 +01:00
Will Assis 1263a3d364 unified-storage: HappyPath and notifier tests + couple of bugfixes (#116087)
* unified-storage: couple of bugfixes and enable HappyPath and notifier sqlkv tests
2026-01-12 12:17:41 -05:00
Igor Suleymanov 39c34f85f4 Merge branch 'main' into feat/mt-apiextensions
Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
2026-01-12 14:19:55 +02:00
Igor Suleymanov 9e4e93bf5e Merge branch 'ale/apiextensions' into feat/mt-apiextensions
Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
2025-12-19 16:08:09 +02:00
Igor Suleymanov 8df2debd34 Merge remote-tracking branch 'origin/main' into feat/mt-apiextensions
Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
2025-12-19 16:03:06 +02:00
Alejandro Malavet d146d2c539 Merge branch 'main' into ale/apiextensions 2025-12-19 00:39:44 -05:00
Igor Suleymanov 1bd36486e9 Merge remote-tracking branch 'origin/main' into feat/mt-apiextensions 2025-12-16 12:24:07 +02:00
Igor Suleymanov b99639fe7a Merge remote-tracking branch 'origin/main' into feat/mt-apiextensions 2025-12-12 18:04:20 +02:00
Igor SuleymanovandClaude 719d779171 Restructure API Extensions to Enterprise-only + MT-only
**OSS Changes (Removal):**
- Deleted `pkg/registry/apis/apiextensions/` directory (8,122 LOC)
- Removed apiextensions from OSS wireset and apis.go ServiceSink
- Removed apiExtensionsEnabled block and createAPIExtensionsServer from service.go
- Cleaned up wireexts_oss.go imports

**Enterprise Changes (Added separately):**
- Created `pkg/extensions/apiserver/registry/apiextensions/` with:
  - `register.go`: RegisterAPIService (prod) + RegisterAPIServiceForTesting (test)
  - `storage.go`: EnterpriseCRDStorageProvider with Unified Storage integration
  - `README.md`: Comprehensive testing and development documentation
- Updated `pkg/extensions/apiserver/registry/wireset.go` with new registrations
- Updated `pkg/extensions/apiserver/factory.go` to use new builder

**Configuration:**
- Updated feature flag description to indicate "Enterprise + MT-only"

Following the Secrets API pattern, API Extensions are now:
1. **Enterprise-only**: No code in OSS builds
2. **MT-only in production**: Double gating (StackID + feature flag)
3. **Testable without StackID**: Via RegisterAPIServiceForTesting
4. **Wire-based registration**: Standard pattern, no manual factory registration

-  OSS build succeeds without apiextensions code
-  Enterprise build succeeds with apiextensions code
-  Wire generation works for both OSS and Enterprise
-  `make gen-go` and `make build-go` pass

- Epic: https://github.com/grafana/grafana-org/issues/545
- OSS PR: https://github.com/grafana/grafana/pull/114466
- Enterprise PR: https://github.com/grafana/grafana-enterprise/pull/10324

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
2025-12-12 17:58:51 +02:00
konsalex 09d98402c4 Add support for apiextensions to Grafana 2025-12-12 17:35:55 +02:00
33 changed files with 596 additions and 1197 deletions
+1
View File
@@ -1325,6 +1325,7 @@ embed.go @grafana/grafana-as-code
/conf/provisioning/datasources/ @grafana/plugins-platform-backend
/conf/provisioning/plugins/ @grafana/plugins-platform-backend
/conf/provisioning/sample/ @grafana/grafana-git-ui-sync-team
/conf/apiextensions.ini @grafana/grafana-app-platform-squad
# Security
/relyance.yaml @grafana/security-team
+5
View File
@@ -135,6 +135,10 @@ profile.cov
/pkg/operators/enterprise_*
/pkg/operators/**/enterprise_*
# Enterprise apiextensions server
pkg/registry/apis/apiextensions/*
!pkg/registry/apis/apiextensions/register.go
debug.test
/examples/*/dist
/packaging/**/*.rpm
@@ -262,3 +266,4 @@ public/mockServiceWorker.js
# Ignore grafana/hippocampus local cache folder
.hippo
devenv/blocks/auth/signer/keys/ec_private_key.pem
+53
View File
@@ -0,0 +1,53 @@
; Run locally unified storage with SQLite to test
; new API registration changes
app_mode = development
target = all
[log]
level = debug
[server]
; HTTPS is required for kubectl (but HTTP works for testing with curl)
protocol = https
http_port = 1111
[feature_toggles]
; Enable the apiextensions feature
apiExtensions = true
; Enable unified storage globally
unifiedStorage = true
; Enable search indexing for unified storage
unifiedStorageSearch = true
; Enable the grafana-apiserver explicitly
grafanaAPIServer = true
; Enable K8s aggregator for API discovery aggregation
; NOTE: This is an enterprise-only feature that requires TLS certificates
; This will surface the new registered group APIs to the `/apis` endpoint.
kubernetesAggregator = true
[grafana-apiserver]
; Use unified storage backed by SQL (uses your Grafana database)
storage_type = unified
; Certificates for the Kubernetes aggregator (generated by hack/make-aggregator-pki.sh)
proxy_client_cert_file = data/grafana-aggregator/client.crt
proxy_client_key_file = data/grafana-aggregator/client.key
; Configure dashboards to use unified storage
[unified_storage.dashboards.dashboard.grafana.app]
dualWriterMode = 5
; Configure folders to use unified storage (required for dashboards)
[unified_storage.folders.folder.grafana.app]
dualWriterMode = 5
[database]
; SQLite database for testing
type = sqlite3
path = grafana.db
high_availability = false
; Will only be used for the MT grafana
; apiextensions service
; [auth.extended_jwt]
; enabled = true
; jwks_url = "http://localhost:6481/jwks"
@@ -322,7 +322,7 @@ If you continue to experience issues after following this troubleshooting guide:
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](https://grafana.com/contact/) if you're an Enterprise or Cloud customer.
1. Contact Grafana Support if you're an Enterprise or Cloud customer.
When reporting issues, include:
+20 -113
View File
@@ -17,145 +17,52 @@ menuTitle: MySQL
title: MySQL data source
weight: 1000
refs:
annotate-visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
configure-mysql-data-source:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configure/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configuration/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configure/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configuration/
mysql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
troubleshoot-mysql:
alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/troubleshooting/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/troubleshooting/
mysql-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
mysql-alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
mysql-annotations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
transformations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/transform-data/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/transform-data/
visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/
variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
query-caching:
- pattern: /docs/grafana/
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
postgres:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/postgres/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/postgres/
mssql:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mssql/
---
# MySQL data source
Grafana ships with built-in support for MySQL.
You can query and visualize data from MySQL-compatible databases like [MariaDB](https://mariadb.org/) or [Percona Server](https://www.percona.com/).
Grafana ships with a built-in MySQL data source plugin that allows you to query and visualize data from a MySQL-compatible database like [MariaDB](https://mariadb.org/) or [Percona Server](https://www.percona.com/). You don't need to install a plugin in order to add the MySQL data source to your Grafana instance.
Use this data source to create dashboards, explore SQL data, and monitor MySQL-based workloads in real time.
Grafana offers several configuration options for this data source as well as a visual and code-based query editor.
{{< docs/play title="MySQL Overview" url="https://play.grafana.org/d/edyh1ib7db6rkb/mysql-overview" >}}
## Get started with the MySQL data source
## Supported databases
This data source supports the following MySQL-compatible databases:
- MySQL 5.7 and newer
- MySQL 8.0 and newer
- MariaDB 10.2 and newer
- Percona Server 5.7 and newer
- Amazon Aurora MySQL
- Azure Database for MySQL
- Google Cloud SQL for MySQL
Grafana recommends using the latest available version for your database for optimal compatibility.
## Key capabilities
The MySQL data source supports:
- **Time series queries:** Visualize metrics over time using 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 MySQL on your dashboard graphs.
- **Alerting:** Create alerts based on MySQL query results.
- **Macros:** Simplify queries with built-in macros for time filtering and grouping.
## Get started
The following documentation helps you get started with the MySQL data source:
The following documents will help you get started with the MySQL data source in Grafana:
- [Configure the MySQL data source](ref:configure-mysql-data-source)
- [MySQL query editor](ref:mysql-query-editor)
- [MySQL template variables](ref:mysql-template-variables)
- [MySQL annotations](ref:mysql-annotations)
- [MySQL alerting](ref:mysql-alerting)
- [Troubleshoot MySQL data source issues](ref:troubleshoot-mysql)
## Additional resources
Once you have configured the data source you can:
After configuring the MySQL data source, you can also:
- Add [annotations](ref:annotate-visualizations)
- Set up [alerting](ref:alerting)
- Add [transformations](ref:transformations)
- Create a wide variety of [visualizations](ref:visualizations).
- Configure and use [templates and variables](ref:variables).
- Add [transformations](ref:transformations).
- Optimize performance with [query caching](ref:query-caching).
View a MySQL overview on Grafana Play:
## Pre-configured dashboards
If you want to monitor your MySQL server's performance metrics (connections, queries, replication, and more), Grafana provides pre-configured dashboards through the MySQL integration:
- **MySQL Overview** - Key performance metrics for your MySQL server.
- **MySQL Logs** - Log analysis for troubleshooting.
The MySQL integration uses the Prometheus MySQL Exporter to collect server metrics and includes 15 pre-configured alert rules.
To use these dashboards:
1. In Grafana Cloud, navigate to **Connections** > **Add new connection**.
1. Search for **MySQL** and select the MySQL integration.
1. Follow the setup instructions to install the MySQL Exporter.
1. Import the pre-configured dashboards from the integration page.
For more MySQL dashboards, browse the [Grafana dashboard catalog](https://grafana.com/grafana/dashboards/?search=mysql).
{{< admonition type="note" >}}
The MySQL integration monitors your MySQL _server_ using Prometheus metrics. The MySQL _data source_ documented here queries data stored _in_ MySQL tables. These are complementary features for different use cases.
{{< /admonition >}}
## Related data sources
- [PostgreSQL](ref:postgres) - For PostgreSQL databases.
- [Microsoft SQL Server](ref:mssql) - For Microsoft SQL Server and Azure SQL databases.
{{< docs/play title="MySQL Overview" url="https://play.grafana.org/d/edyh1ib7db6rkb/mysql-overview" >}}
@@ -1,188 +0,0 @@
---
description: Using Grafana Alerting with the MySQL data source
keywords:
- grafana
- mysql
- alerting
- alerts
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Alerting
title: MySQL alerting
weight: 350
refs:
alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
create-alert-rule:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-grafana-managed-rule/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule/
mysql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
---
# MySQL alerting
You can use Grafana Alerting with MySQL to create alerts based on your MySQL data. This allows you to monitor metrics, detect anomalies, and receive notifications when specific conditions are met.
For general information about Grafana Alerting, refer to [Grafana Alerting](ref:alerting).
## Before you begin
Before creating alerts with MySQL, ensure you have:
- A MySQL data source configured in Grafana.
- Appropriate permissions to create alert rules.
- Understanding of the metrics you want to monitor.
## Supported query types
MySQL alerting works with **time series queries** that return numeric data over time. Table formatted queries are not supported in alert rule conditions.
To create a valid alert query:
- Include a `time` column that returns a SQL datetime or UNIX epoch timestamp
- Return numeric values for the metrics you want to alert on
- Sort results by the time column
For more information on writing time series queries, refer to [MySQL query editor](ref:mysql-query-editor).
### Query format requirements
| Query format | Alerting support | Notes |
| ------------ | ---------------- | ---------------------------------------- |
| Time series | Yes | Required for alerting |
| Table | No | Convert to time series format for alerts |
## Create an alert rule
To create an alert rule using MySQL:
1. Navigate to **Alerting** > **Alert rules**.
1. Click **New alert rule**.
1. Enter a name for the alert rule.
1. Select your **MySQL** data source.
1. Build your query using the query editor:
- Set the **Format** to **Time series**
- Include a time column using the `$__time()` or `$__timeGroup()` macro
- Add numeric columns for the values to monitor
- Use `$__timeFilter()` to filter data by the dashboard time range
1. Configure the alert condition (for example, when the average is above a threshold).
1. Set the evaluation interval and pending period.
1. Configure notifications and labels.
1. Click **Save rule**.
For detailed instructions, refer to [Create a Grafana-managed alert rule](ref:create-alert-rule).
## Example alert queries
The following examples show common alerting scenarios with MySQL.
### Alert on high error count
Monitor the number of errors over time:
```sql
SELECT
$__timeGroup(created_at, '1m') AS time,
COUNT(*) AS error_count
FROM error_logs
WHERE $__timeFilter(created_at)
AND level = 'error'
GROUP BY time
ORDER BY time
```
**Condition:** When error_count is above 100.
### Alert on average response time
Monitor API response times:
```sql
SELECT
$__timeGroup(request_time, '5m') AS time,
AVG(response_time_ms) AS avg_response_time
FROM api_requests
WHERE $__timeFilter(request_time)
GROUP BY time
ORDER BY time
```
**Condition:** When avg_response_time is above 500 (milliseconds).
### Alert on low order volume
Detect drops in order activity:
```sql
SELECT
$__timeGroup(order_date, '1h') AS time,
COUNT(*) AS order_count
FROM orders
WHERE $__timeFilter(order_date)
GROUP BY time
ORDER BY time
```
**Condition:** When order_count is below 10.
### Alert on disk usage percentage
Monitor database storage metrics:
```sql
SELECT
$__timeGroup(recorded_at, '5m') AS time,
AVG(disk_used_percent) AS disk_usage
FROM system_metrics
WHERE $__timeFilter(recorded_at)
AND metric_type = 'disk'
GROUP BY time
ORDER BY time
```
**Condition:** When disk_usage is above 85.
## Limitations
When using MySQL with Grafana Alerting, be aware of the following limitations:
### Template variables not supported
Alert queries cannot contain template variables. Grafana evaluates alert rules on the backend without dashboard context, so variables like `$hostname` or `$environment` won't be resolved.
If your dashboard query uses template variables, create a separate query for alerting with hard coded values.
### Table format not supported
Queries using the **Table** format cannot be used for alerting. Set the query format to **Time series** and ensure your query returns a time column.
### Query timeout
Complex queries with large datasets may timeout during alert evaluation. Optimize queries for alerting by:
- Adding appropriate `WHERE` clauses to limit data
- Using indexes on time and filter columns
- Reducing the time range evaluated
## Best practices
Follow these best practices when creating MySQL alerts:
- **Use time series format:** Always set the query format to Time series for alert queries.
- **Include time filters:** Use the `$__timeFilter()` macro to limit data to the evaluation window.
- **Optimize queries:** Add indexes on columns used in `WHERE` clauses and `GROUP BY`.
- **Test queries first:** Verify your query returns expected results in Explore before creating an alert.
- **Set realistic thresholds:** Base alert thresholds on historical data patterns.
- **Use meaningful names:** Give alert rules descriptive names that indicate what they monitor.
@@ -1,163 +0,0 @@
---
description: Using annotations with MySQL in Grafana
keywords:
- grafana
- mysql
- annotations
- events
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Annotations
title: MySQL annotations
weight: 340
refs:
annotate-visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
---
# MySQL annotations
Annotations overlay event data on your dashboard graphs, helping you correlate events with metrics.
You can use MySQL as a data source for annotations to display events such as deployments, alerts, or other significant occurrences on your visualizations.
For general information about annotations, refer to [Annotate visualizations](ref:annotate-visualizations).
## Before you begin
Before creating MySQL annotations, ensure you have:
- A MySQL data source configured in Grafana.
- Tables containing event data with timestamp fields.
- Read access to the tables containing your events.
## Create an annotation query
To add a MySQL annotation to your dashboard:
1. Navigate to your dashboard and click **Dashboard settings** (gear icon).
1. Select **Annotations** in the left menu.
1. Click **Add annotation query**.
1. Enter a **Name** for the annotation.
1. Select your **MySQL** data source from the **Data source** drop-down.
1. Write a SQL query that returns the required columns.
1. Click **Save dashboard**.
## Query columns
Your annotation query must return a `time` column and can optionally include `timeend`, `text`, and `tags` columns.
| Column | Required | Description |
| --------- | -------- | --------------------------------------------------------------------------------------------- |
| `time` | Yes | The timestamp for the annotation. Can be a SQL datetime or UNIX epoch value. |
| `timeend` | No | The end timestamp for range annotations. Creates a shaded region instead of a vertical line. |
| `text` | No | The annotation description displayed when you hover over the annotation. |
| `tags` | No | Tags for the annotation as a comma-separated string. Helps categorize and filter annotations. |
## Example queries
The following examples show common annotation query patterns.
### Basic annotation with epoch time
Display events using UNIX epoch timestamps:
```sql
SELECT
epoch_time as time,
description as text,
CONCAT(tag1, ',', tag2) as tags
FROM events
WHERE $__unixEpochFilter(epoch_time)
```
### Annotation with a single tag
Display events with a single tag value:
```sql
SELECT
epoch_time as time,
message as text,
category as tags
FROM event_log
WHERE $__unixEpochFilter(epoch_time)
```
### Range annotation with start and end time
Display events with duration as shaded regions:
```sql
SELECT
start_time as time,
end_time as timeend,
description as text,
CONCAT(type, ',', severity) as tags
FROM incidents
WHERE $__unixEpochFilter(start_time)
```
### Annotation with native SQL datetime
Display events using native MySQL datetime columns:
```sql
SELECT
event_date as time,
message as text,
CONCAT(category, ',', priority) as tags
FROM system_events
WHERE $__timeFilter(event_date)
```
### Deployment annotations
Display deployment events:
```sql
SELECT
deployed_at as time,
CONCAT('Deployed ', version, ' to ', environment) as text,
environment as tags
FROM deployments
WHERE $__timeFilter(deployed_at)
```
### Maintenance window annotations
Display maintenance windows as range annotations:
```sql
SELECT
start_time as time,
end_time as timeend,
CONCAT('Maintenance: ', description) as text,
'maintenance' as tags
FROM maintenance_windows
WHERE $__timeFilter(start_time)
```
## Macros
Use these macros in your annotation queries to filter by the dashboard time range:
| Macro | Description |
| ---------------------------- | ---------------------------------------------------------------- |
| `$__timeFilter(column)` | Filters by time range using a native SQL datetime column. |
| `$__unixEpochFilter(column)` | Filters by time range using a column with UNIX epoch timestamps. |
## Best practices
Follow these best practices when creating MySQL annotations:
- **Use time filters:** Always include `$__timeFilter()` or `$__unixEpochFilter()` to limit results to the dashboard time range.
- **Keep queries efficient:** Add indexes on time columns and filter columns to improve query performance.
- **Use meaningful text:** Include descriptive information in the `text` column to make annotations useful.
- **Organize with tags:** Use consistent tag values to categorize annotations and enable filtering.
- **Test queries first:** Verify your query returns expected results in Explore before adding it as an annotation.
@@ -1,6 +1,4 @@
---
aliases:
- ../configuration/
description: This document provides instructions for configuring the MySQL data source and explains available configuration options.
keywords:
- grafana
@@ -12,7 +10,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Configure
menuTitle: Configure the MySQL data source
title: Configure the MySQL data source
weight: 10
refs:
@@ -36,41 +34,6 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/
mysql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
annotate-visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
mysql-alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
mysql-annotations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
mysql-troubleshoot:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/troubleshooting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/troubleshooting/
mysql-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
---
# Configure the MySQL data source
@@ -79,35 +42,24 @@ This document provides instructions for configuring the MySQL data source and ex
## Before you begin
Before configuring the MySQL data source, ensure you have the following:
- **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.
- **A running MySQL instance:** MySQL 5.7 or newer, MariaDB 10.2 or newer, or a compatible MySQL-based database such as Percona Server.
- **Network access:** Grafana must be able to reach your MySQL server. The default port is `3306`.
- **Authentication credentials:** A MySQL user with at least `SELECT` permissions on the databases and tables you want to query.
- **Security certificates:** If using encrypted connections, gather any necessary TLS/SSL certificates.
You must have the `Organization administrator` role in order to configure the MySQL data source.
Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system.
{{< admonition type="note" >}}
Grafana ships with a built-in MySQL data source plugin. No additional installation is required.
Grafana ships with the MySQL data source by default, so no additional installation is required.
{{< /admonition >}}
{{< admonition type="tip" >}}
**Grafana Cloud users:** If your MySQL server is in a private network, you can configure [Private data source connect](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) to establish connectivity.
{{< admonition type="caution" >}}
When adding a data source, ensure the database user you specify has only `SELECT` permissions on the relevant database and tables. Grafana does not validate the safety of queries, which means they can include potentially harmful SQL statements, such as `USE otherdb;` or `DROP TABLE user;`, which could get executed.
To minimize this risk, Grafana strongly recommends creating a dedicated MySQL user with restricted permissions.
{{< /admonition >}}
### Database user permissions
When adding a data source, ensure the database user you specify has only `SELECT` permissions on the relevant database and tables. Grafana doesn't validate the safety of queries, which means they can include potentially harmful SQL statements, such as `USE otherdb;` or `DROP TABLE user;`, which could get executed.
To minimize this risk, Grafana strongly recommends creating a dedicated MySQL user with restricted permissions:
Example:
```sql
CREATE USER 'grafanaReader' IDENTIFIED BY 'password';
GRANT SELECT ON mydatabase.mytable TO 'grafanaReader';
CREATE USER 'grafanaReader' IDENTIFIED BY 'password';
GRANT SELECT ON mydatabase.mytable TO 'grafanaReader';
```
Use wildcards (`*`) in place of a database or table if you want to grant access to more databases and tables.
@@ -261,46 +213,3 @@ datasources:
tlsClientCert: ${GRAFANA_TLS_CLIENT_CERT}
tlsCACert: ${GRAFANA_TLS_CA_CERT}
```
## Configure with Terraform
You can configure the MySQL 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 MySQL data source:
```hcl
resource "grafana_data_source" "mysql" {
name = "MySQL"
type = "mysql"
url = "localhost:3306"
user = "grafana"
json_data_encoded = jsonencode({
database = "grafana"
maxOpenConns = 100
maxIdleConns = 100
maxIdleConnsAuto = true
connMaxLifetime = 14400
})
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 MySQL data source, you can:
- [Write queries](ref:mysql-query-editor) using the query editor to explore and visualize your data.
- [Use template variables](ref:mysql-template-variables) to create dynamic, reusable dashboards.
- [Add annotations](ref:mysql-annotations) to overlay MySQL events on your graphs.
- [Set up alerting](ref:mysql-alerting) to create alert rules based on your MySQL data.
- [Troubleshoot issues](ref:mysql-troubleshoot) if you encounter problems with your data source.
@@ -9,7 +9,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Query editor
menuTitle: MySQL query editor
title: MySQL query editor
weight: 30
refs:
@@ -61,21 +61,6 @@ refs:
configure-standard-options:
- pattern: /docs/grafana/
- destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/
mysql-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/template-variables/
mysql-alerting:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/alerting/
mysql-annotations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/annotations/
---
# MySQL query editor
@@ -320,20 +305,171 @@ Table panel result:
The query returns multiple columns representing minimum and maximum values within the defined range.
## Template variables
## Templating
Instead of hard-coding values like server, application, or sensor names in your metric queries, you can use variables. Variables appear as drop-down select boxes at the top of the dashboard, making it easy to change the data displayed in your dashboard.
Instead of hardcoding values like server, application, or sensor names in your metric queries, you can use variables. Variables appear as drop-down select boxes at the top of the dashboard. These drop-downs make it easy to change the data being displayed in your dashboard.
For detailed information on using template variables with MySQL, refer to [MySQL template variables](ref:mysql-template-variables).
Refer to [Templates](ref:variables) for an introduction to creating template variables as well as the different types.
### Query variable
If you add a `Query` template variable you can write a MySQL query to retrieve items such as measurement names, key names, or key values, which will be displayed in the drop-down menu.
For example, you can use a variable to retrieve all the values from the `hostname` column in a table by creating the following query in the templating variable _Query_ setting.
```sql
SELECT hostname FROM my_host
```
A query can return multiple columns, and Grafana will automatically generate a list based on the query results. For example, the following query returns a list with values from `hostname` and `hostname2`.
```sql
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
```
To use time range dependent macros like `$__timeFilter(column)` in your query,you must set the template variable's refresh mode to _On Time Range Change_.
```sql
SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
```
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column must contain unique values (if not, only the first value is used). This allows the drop-down options to display a text-friendly name as the text while using an ID as the value. For example, a query could use `hostname` as the text and `id` as the value:
```sql
SELECT hostname AS __text, id AS __value FROM my_host
```
You can also create nested variables. For example, if you have a variable named `region`, you can configure the `hosts` variable to display only the hosts within the currently selected region as shown in the following example. If `region` is a multi-value variable, use the `IN` operator instead of `=` to match multiple values.
```sql
SELECT hostname FROM my_host WHERE region IN($region)
```
#### Use `__searchFilter` to filter results in a query variable
Using `__searchFilter` in the query field allows the query results to be filtered based on the users input in the drop-down selection box. If you do not enter anything, the default value for `__searchFilter` is %
Note that you must enclose the `__searchFilter` expression in quotes as Grafana does not add them automatically.
The following example demonstrates how to use `__searchFilter` in the query field to enable real-time searching for `hostname` as the user type in the drop-down selection box.
```sql
SELECT hostname FROM my_host WHERE hostname LIKE '$__searchFilter'
```
### Using variables in queries
Template variable values are only quoted when the template variable is a `multi-value`.
If the variable is a multi-value variable, use the `IN` comparison operator instead of `=` to match against multiple values.
You can use two different syntaxes:
`$<varname>` Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in($hostname)
ORDER BY atimestamp ASC
```
`[[varname]]` Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in([[hostname]])
ORDER BY atimestamp ASC
```
#### Disabling quoting for multi-value variables
Grafana automatically creates a quoted, comma-separated string for multi-value variables. For example: if `server01` and `server02` are selected then it will be formatted as: `'server01', 'server02'`. To disable quoting, use the csv formatting option for variables:
Grafana automatically formats multi-value variables as a quoted, comma-separated string. For example, if `server01` and `server02` are selected, they are formatted as `'server01'`, `'server02'`. To remove the quotes, enable the CSV formatting option for the variables.
`${servers:csv}`
Read more about variable formatting options in the [Variables](ref:variable-syntax-advanced-variable-format-options) documentation.
## Annotations
Annotations allow you to overlay event information on your graphs, helping you correlate events with metrics. You can write SQL queries that return event data to display as annotations on your dashboards.
[Annotations](ref:annotate-visualizations) allow you to overlay rich event information on top of graphs. You add annotation queries via the **Dashboard settings > Annotations view**.
For detailed information on creating annotations with MySQL, refer to [MySQL annotations](ref:mysql-annotations).
**Example query using a`time` column with epoch values:**
```sql
SELECT
epoch_time as time,
metric1 as text,
CONCAT(tag1, ',', tag2) as tags
FROM
public.test_data
WHERE
$__unixEpochFilter(epoch_time)
```
You may use one or more tags to show them as annotations in a common-separate string.
**Example query using a `time` column with epoch values for a single tag:**
```sql
SELECT
epoch_time as time,
metric1 as text,
tag1 as tag
FROM
my_data
WHERE
$__unixEpochFilter(epoch_time)
```
**Example region query using `time` and `timeend` columns with epoch values:**
```sql
SELECT
epoch_time as time,
epoch_timeend as timeend,
metric1 as text,
CONCAT(tag1, ',', tag2) as tags
FROM
public.test_data
WHERE
$__unixEpochFilter(epoch_time)
```
**Example query using a `time` column with a native SQL date/time data type:**
```sql
SELECT
native_date_time as time,
metric1 as text,
CONCAT(tag1, ',', tag2) as tags
FROM
public.test_data
WHERE
$__timeFilter(native_date_time)
```
| Name | Description |
| --------- | --------------------------------------------------------------------------------------------------------------------- |
| `time` | The name of the date/time field, which can be a column with a native SQL date/time data type or epoch value. |
| `timeend` | Optional name of the end date/time field, which can be a column with a native SQL date/time data type or epoch value. |
| `text` | Event description field. |
| `tags` | Optional field name to use for event tags as a comma separated string. |
## Alerting
You can use time series queries to create Grafana-managed alert rules. Table formatted queries are not supported in alert rule conditions.
Use time series queries to create alerts. Table formatted queries aren't yet supported in alert rule conditions.
For detailed information on creating alerts with MySQL, refer to [MySQL alerting](ref:mysql-alerting).
For more information regarding alerting refer to the following:
- [Alert rules](ref:alert-rules)
- [Template annotations and labels](ref:template-annotations-and-labels)
@@ -1,146 +0,0 @@
---
description: Using template variables with MySQL in Grafana
keywords:
- grafana
- mysql
- templates
- variables
- queries
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Template variables
title: MySQL template variables
weight: 300
refs:
variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
variable-syntax-advanced-variable-format-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#advanced-variable-format-options
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#advanced-variable-format-options
add-template-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/
---
# MySQL template variables
Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables.
Grafana displays these variables in drop-down select boxes at the top of the dashboard to help you change the data displayed in your dashboard.
Grafana refers to such variables as **template variables**.
For an introduction to templating and template variables, refer to [Templating](ref:variables) and [Add and manage variables](ref:add-template-variables).
## Query variable
A query variable in Grafana dynamically retrieves values from your data source using a query. With a query variable, you can write a SQL query that returns values such as measurement names, key names, or key values that are shown in a drop-down select box.
For example, the following query returns all values from the `hostname` column:
```sql
SELECT hostname FROM my_host
```
A query can return multiple columns, and Grafana automatically generates a list using the values from those columns. For example, the following query returns values from both the `hostname` and `hostname2` columns, which are included in the variable's drop-down list.
```sql
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
```
To use time range dependent macros like `$__timeFilter(column)` in your query, you must set the template variable's refresh mode to **On Time Range Change**.
```sql
SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
```
### Key/value variables
You can create a key/value variable using a query that returns two columns named `__text` and `__value`.
- The `__text` column defines the label shown in the drop-down.
- The `__value` column defines the value passed to panel queries.
This is useful when you want to display a user-friendly label (like a hostname) but use a different underlying value (like an ID).
Note that the values in the `__text` column should be unique. If there are duplicates, Grafana uses only the first matching entry.
```sql
SELECT hostname AS __text, id AS __value FROM my_host
```
### Nested variables
You can create nested variables, where one variable depends on the value of another. For example, if you have a variable named `region`, you can configure a `hosts` variable to only show hosts from the selected region. If `region` is a multi-value variable, use the `IN` operator instead of `=` to match against multiple selected values.
```sql
SELECT hostname FROM my_host WHERE region IN($region)
```
### Filter results with `__searchFilter`
Using `__searchFilter` in the query field allows the query results to be filtered based on the user's input in the drop-down selection box. If you don't enter anything, the default value for `__searchFilter` is `%`.
Note that you must enclose the `__searchFilter` expression in quotes as Grafana doesn't add them automatically.
The following example demonstrates how to use `__searchFilter` in the query field to enable real-time searching for `hostname` as the user types in the drop-down selection box.
```sql
SELECT hostname FROM my_host WHERE hostname LIKE '$__searchFilter'
```
## Use variables in queries
Grafana automatically quotes template variable values only when the template variable is a `multi-value`.
When using a multi-value variable, use the `IN` comparison operator instead of `=` to match against multiple values.
Grafana supports two syntaxes for using variables in queries:
- **`$<varname>` syntax**
Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in($hostname)
ORDER BY atimestamp ASC
```
- **`[[varname]]` syntax**
Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in([[hostname]])
ORDER BY atimestamp ASC
```
### Disable quoting for multi-value variables
By default, Grafana formats multi-value variables as a quoted, comma-separated string. For example, if `server01` and `server02` are selected, the result will be `'server01'`, `'server02'`. To disable quoting, use the `csv` formatting option for variables:
```text
${servers:csv}
```
This outputs the values as an unquoted comma-separated list.
Refer to [Advanced variable format options](ref:variable-syntax-advanced-variable-format-options) for additional information.
@@ -0,0 +1,80 @@
---
description: Learn how to troubleshoot common problems with the Grafana MySQL data source plugin
keywords:
- grafana
- mysql
- query
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Troubleshoot
title: Troubleshoot common problems with the Grafana MySQL data source plugin
weight: 40
refs:
variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
variable-syntax-advanced-variable-format-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#advanced-variable-format-options
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/variable-syntax/#advanced-variable-format-options
annotate-visualizations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
explore:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
query-transform-data:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/
panel-inspector:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/panel-inspector/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-inspector/
query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/#query-editors
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/#query-editors
alert-rules:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/
template-annotations-and-labels:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/templates/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templates/
configure-standard-options:
- pattern: /docs/grafana/
- destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-standard-options/
---
# Troubleshoot common problems with the Grafana MySQL data source plugin
This page lists common issues you might experience when setting up the Grafana MySQL data source plugin.
### My data source connection fails when using the Grafana MySQL data source plugin
- Check if the MySQL server is up and running.
- Make sure that your firewall is open for MySQL server (default port is `3306`).
- Ensure that you have the correct permissions to access the MySQL server and also have permission to access the database.
- If the error persists, create a new user for the Grafana MySQL data source plugin with correct permissions and try to connect with it.
### What should I do if I see "An unexpected error happened" or "Could not connect to MySQL" after trying all of the above?
- Check the Grafana logs for more details about the error.
- For Grafana Cloud customers, contact support.
@@ -1,370 +0,0 @@
---
aliases:
- ../troubleshoot/
description: Troubleshoot common problems with the MySQL data source in Grafana
keywords:
- grafana
- mysql
- troubleshooting
- errors
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Troubleshooting
title: Troubleshoot MySQL data source issues
weight: 400
refs:
configure-mysql-data-source:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configure/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/configure/
mysql-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/mysql/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 MySQL data source issues
This document provides solutions to common issues you may encounter when configuring or using the MySQL data source in Grafana.
## Connection errors
These errors occur when Grafana cannot establish or maintain a connection to the MySQL server.
### Unable to connect to the server
**Error message:** "dial tcp: connection refused" or "Could not connect to MySQL"
**Cause:** Grafana cannot establish a network connection to the MySQL server.
**Solution:**
1. Verify that the MySQL server is running and accessible.
1. Check that the host and port are correct in the data source configuration. The default MySQL port is `3306`.
1. Ensure there are no firewall rules blocking the connection between Grafana and MySQL.
1. Verify that MySQL is configured to allow remote connections by checking the `bind-address` setting in your MySQL configuration.
1. For Grafana Cloud, ensure you have configured [Private data source connect](ref:private-data-source-connect) if your MySQL instance isn't publicly accessible.
### Connection timeout
**Error message:** "Connection timed out" or "I/O timeout"
**Cause:** The connection to MySQL timed out before receiving a response.
**Solution:**
1. Check the network latency between Grafana and MySQL.
1. Verify that MySQL isn't overloaded or experiencing performance issues.
1. Check if any network devices (load balancers, proxies) are timing out the connection.
1. Increase the `wait_timeout` setting in MySQL if connections are timing out during idle periods.
### TLS/SSL connection failures
**Error message:** "TLS handshake failed" or "x509: certificate verify failed"
**Cause:** There is a mismatch between the TLS settings in Grafana and what the MySQL server supports or requires.
**Solution:**
1. Verify that the MySQL server has a valid SSL certificate if encryption is enabled.
1. Check that the certificate is trusted by the Grafana server.
1. If using a self-signed certificate, enable **With CA Cert** and provide the root certificate under **TLS/SSL Root Certificate**.
1. To bypass certificate validation (not recommended for production), enable **Skip TLS Verification** in the data source configuration.
1. Ensure the SSL certificate hasn't expired.
### Connection reset by peer
**Error message:** "Connection reset by peer" or "EOF"
**Cause:** The MySQL server closed the connection unexpectedly.
**Solution:**
1. Check the `max_connections` setting on the MySQL server to ensure it isn't being exceeded.
1. Verify the `wait_timeout` and `interactive_timeout` settings in MySQL aren't set too low.
1. Increase the **Max lifetime** setting in Grafana's data source configuration to be lower than MySQL's `wait_timeout`.
1. Check MySQL server logs for any errors or connection-related messages.
## Authentication errors
These errors occur when there are issues with authentication credentials or permissions.
### Access denied for user
**Error message:** "Access denied for user 'username'@'host'" or "Authentication failed"
**Cause:** The authentication credentials are invalid or the user doesn't have permission to connect from the Grafana server's host.
**Solution:**
1. Verify that the username and password are correct.
1. Check that the user exists in MySQL and is enabled.
1. Ensure the user has permission to connect from the Grafana server's IP address. MySQL restricts access based on the connecting host:
```sql
SELECT user, host FROM mysql.user WHERE user = 'your_user';
```
1. If necessary, create a user that can connect from the Grafana server:
```sql
CREATE USER 'grafana'@'grafana_server_ip' IDENTIFIED BY 'password';
```
1. If using the `mysql_native_password` authentication plugin, ensure it's enabled on the server.
### Cannot access database
**Error message:** "Access denied for user 'username'@'host' to database 'dbname'"
**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 has the required permissions on the database:
```sql
GRANT SELECT ON your_database.* TO 'grafana'@'grafana_server_ip';
FLUSH PRIVILEGES;
```
1. For production environments, grant permissions only on specific tables:
```sql
GRANT SELECT ON your_database.your_table TO 'grafana'@'grafana_server_ip';
```
### PAM authentication issues
**Error message:** "Authentication plugin 'auth_pam' cannot be loaded" or cleartext password errors
**Cause:** PAM (Pluggable Authentication Modules) authentication requires cleartext password transmission.
**Solution:**
1. Enable **Allow Cleartext Passwords** in the data source configuration if using PAM authentication.
1. Ensure TLS is enabled to protect password transmission when using cleartext passwords.
1. Verify that the PAM plugin is correctly installed and configured on the MySQL server.
## 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 convert your date column: `$__time(your_date_column)`.
1. Verify the time column is of a valid MySQL date/time type (`DATETIME`, `TIMESTAMP`, `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. 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.
1. Ensure backticks are used for reserved words or special characters in column names: `$__timeFilter(\`time-column\`)`.
### 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. Set the **Session Timezone** in the data source configuration to match your data's timezone, or use `+00:00` for UTC.
1. If your timestamps are stored in local time, convert them to UTC in your query:
```sql
SELECT
CONVERT_TZ(your_datetime_column, 'Your/Timezone', 'UTC') AS time,
value
FROM your_table
```
### 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 `LIMIT` clause to restrict results: `SELECT ... LIMIT 1000`.
1. Use the `$__timeGroup()` macro to aggregate data into time intervals.
### Syntax error in SQL statement
**Error message:** "You have an error in your SQL syntax" followed by specific error details
**Cause:** The SQL query contains invalid syntax.
**Solution:**
1. Check for missing or extra commas, parentheses, or quotes.
1. Ensure reserved words used as identifiers are enclosed in backticks: `` `table` ``, `` `select` ``.
1. Verify that template variable syntax is correct: `$variable` or `${variable}`.
1. Test the query directly in a MySQL client to isolate Grafana-specific issues.
### Unknown column in field list
**Error message:** "Unknown column 'column_name' in 'field list'"
**Cause:** The specified column doesn't exist in the table or is misspelled.
**Solution:**
1. Verify the column name is spelled correctly.
1. Check that the column exists in the specified table.
1. If the column name contains special characters or spaces, enclose it in backticks: `` `column-name` ``.
1. Ensure the correct database is selected if you're referencing columns without the full table path.
## 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:
```sql
CREATE INDEX idx_time ON your_table(time_column);
```
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 using `EXPLAIN` to identify bottlenecks:
```sql
EXPLAIN SELECT * FROM your_table WHERE time_column > NOW() - INTERVAL 1 HOUR;
```
### 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.
1. Increase the `max_connections` setting in MySQL if necessary:
```sql
SHOW VARIABLES LIKE 'max_connections';
SET GLOBAL max_connections = 200;
```
### Query timeout
**Error message:** "Query execution was interrupted" or "Lock wait timeout exceeded"
**Cause:** The query takes too long and exceeds the configured timeout.
**Solution:**
1. Optimize the query by adding appropriate indexes.
1. Reduce the amount of data being queried by narrowing the time range.
1. Use aggregations to reduce the result set size.
1. Check for table locks that might be blocking the query.
## Other common issues
The following issues don't produce specific error messages but are commonly encountered.
### 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.
### Special characters in database or table names
**Cause:** Queries fail when tables or databases contain reserved words or special characters.
**Solution:**
1. Enclose identifiers with special characters in backticks: `` `my-database`.`my-table` ``.
1. The query editor automatically handles this for selections, but manual queries require backticks.
1. Avoid using reserved words as identifiers when possible.
### An unexpected error happened
**Error message:** "An unexpected error happened"
**Cause:** A general error occurred that doesn't have a specific error message.
**Solution:**
1. Check the Grafana server logs for more details about the error.
1. Verify all data source configuration settings are correct.
1. Test the connection using the **Save & test** button.
1. Ensure the MySQL server is accessible and responding to queries.
1. For Grafana Cloud customers, contact support for assistance.
## 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 MySQL error logs for additional details.
1. Contact Grafana Support if you're an Enterprise or Cloud customer.
When reporting issues, include:
- Grafana version
- MySQL version
- Error messages (redact sensitive information)
- Steps to reproduce
- Relevant query examples (redact sensitive data)
+1 -1
View File
@@ -642,7 +642,7 @@ require (
gopkg.in/telebot.v3 v3.3.8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.34.3 // indirect
k8s.io/apiextensions-apiserver v0.34.3
k8s.io/kms v0.34.3 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
+17
View File
@@ -737,6 +737,7 @@ github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
@@ -990,6 +991,7 @@ github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed/go.mod h1:OIAAKNgG
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5 h1:MAQ2B0cu0V1S91ZjVa7NomNZFjaR2SmdtvdwhqBtyhU=
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco=
github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA=
github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 h1:6D2gGAwyQBElSrp3E+9lSr7k8gLuP3Aiy20rweLWeBw=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3/go.mod h1:YeND+6FDA7OuFgDzYODN8kfPhXLCehcpxe4T9mdnpCY=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 h1:4/BZkGObFWZf4cLbE2Vqg/1VTz67Q0AJ7LHspWLKJoQ=
@@ -1460,6 +1462,7 @@ github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiy
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs=
github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/segmentio/asm v1.1.4/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
@@ -1493,6 +1496,7 @@ github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -1642,10 +1646,13 @@ go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1Zt
go.etcd.io/etcd v3.3.25+incompatible h1:V1RzkZJj9LqsJRy+TUBgpWSbZXITLB819lstuTFoZOY=
go.etcd.io/etcd v3.3.25+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao=
go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4=
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI=
go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA=
go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o=
@@ -1969,6 +1976,7 @@ golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sU
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
@@ -2251,24 +2259,30 @@ k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU=
k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE=
k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I=
k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.26.2/go.mod h1:GHcozwXgXsPuOJ28EnQ/jXEM9QeG6HT22YxSNmpYNh8=
k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI=
k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU=
k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/code-generator v0.34.3 h1:6ipJKsJZZ9q21BO8I2jEj4OLN3y8/1n4aihKN0xKmQk=
k8s.io/code-generator v0.34.3/go.mod h1:oW73UPYpGLsbRN8Ozkhd6ZzkF8hzFCiYmvEuWZDroI4=
k8s.io/component-base v0.26.2/go.mod h1:DxbuIe9M3IZPRxPIzhch2m1eT7uFrSBJUBuVCQEBivs=
k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM=
k8s.io/cri-api v0.27.1/go.mod h1:+Ts/AVYbIo04S86XbTD73UPp/DkTiYxtsFeOFEu32L0=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk=
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q=
@@ -2282,7 +2296,9 @@ k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
k8s.io/kms v0.34.2/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
k8s.io/kube-aggregator v0.34.1/go.mod h1:RU8j+5ERfp0h+gIvWtxRPfsa5nK7rboDm8RST8BJfYQ=
k8s.io/kube-aggregator v0.34.2/go.mod h1:/tp4cc/1p2AvICsS4mjjSJakdrbhcGbRmj0mdHTdR2Q=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
@@ -2318,6 +2334,7 @@ rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
+4 -4
View File
@@ -474,6 +474,10 @@ export interface FeatureToggles {
*/
kubernetesAggregatorCapTokenAuth?: boolean;
/**
* Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration
*/
apiExtensions?: boolean;
/**
* Enable groupBy variable support in scenes dashboards
*/
groupByVariable?: boolean;
@@ -695,10 +699,6 @@ export interface FeatureToggles {
*/
passwordlessMagicLinkAuthentication?: boolean;
/**
* Display Related Logs in Grafana Metrics Drilldown
*/
exploreMetricsRelatedLogs?: boolean;
/**
* Adds support for quotes and special characters in label values for Prometheus queries
*/
prometheusSpecialCharsInLabelValues?: boolean;
+1
View File
@@ -163,6 +163,7 @@ var serviceIdentityTokenPermissions = []string{
"plugins.grafana.app:*",
"historian.alerting.grafana.app:*",
"advisor.grafana.app:*",
"apiextensions.grafana.app:*",
// Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions.
"secret.grafana.app/securevalues:decrypt",
+26 -14
View File
@@ -131,19 +131,31 @@ func NamespaceKeyFunc(gr schema.GroupResource) func(ctx context.Context, name st
}
}
// NoNamespaceKeyFunc is the default function for constructing storage paths
// to a resource relative to the given prefix without a namespace.
func NoNamespaceKeyFunc(ctx context.Context, prefix string, gr schema.GroupResource, name string) (string, error) {
if len(name) == 0 {
return "", apierrors.NewBadRequest("Name parameter required.")
// ClusterScopedKeyFunc constructs storage paths for cluster-scoped resources (no namespace).
func ClusterScopedKeyFunc(gr schema.GroupResource) func(ctx context.Context, name string) (string, error) {
return func(ctx context.Context, name string) (string, error) {
if len(name) == 0 {
return "", apierrors.NewBadRequest("Name parameter required.")
}
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
}
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Name: name,
}
return key.String(), nil
}
}
// ClusterScopedKeyRootFunc is used by the generic registry store for cluster-scoped resources.
func ClusterScopedKeyRootFunc(gr schema.GroupResource) func(ctx context.Context) string {
return func(ctx context.Context) string {
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
}
return key.String()
}
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
}
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Name: name,
}
return prefix + key.String(), nil
}
+16 -3
View File
@@ -1,6 +1,8 @@
package generic
import (
"context"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
@@ -28,10 +30,21 @@ func NewRegistryStoreWithSelectableFields(scheme *runtime.Scheme, resourceInfo u
gv := resourceInfo.GroupVersion()
gv.Version = runtime.APIVersionInternal
strategy := NewStrategy(scheme, gv)
gr := resourceInfo.GroupResource()
var keyRootFunc func(ctx context.Context) string
var keyFunc func(ctx context.Context, name string) (string, error)
if resourceInfo.IsClusterScoped() {
strategy = strategy.WithClusterScope()
keyRootFunc = ClusterScopedKeyRootFunc(gr)
keyFunc = ClusterScopedKeyFunc(gr)
} else {
keyRootFunc = KeyRootFunc(gr)
keyFunc = NamespaceKeyFunc(gr)
}
// Use custom GetAttrs if provided, otherwise use default
var attrFunc storage.AttrFunc
var predicateFunc func(label labels.Selector, field fields.Selector) storage.SelectionPredicate
@@ -47,10 +60,10 @@ func NewRegistryStoreWithSelectableFields(scheme *runtime.Scheme, resourceInfo u
store := &registry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: NamespaceKeyFunc(resourceInfo.GroupResource()),
KeyRootFunc: keyRootFunc,
KeyFunc: keyFunc,
PredicateFunc: predicateFunc,
DefaultQualifiedResource: resourceInfo.GroupResource(),
DefaultQualifiedResource: gr,
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: resourceInfo.TableConverter(),
CreateStrategy: strategy,
@@ -3,6 +3,7 @@ package aggregatorrunner
import (
"context"
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
genericapiserver "k8s.io/apiserver/pkg/server"
@@ -21,6 +22,10 @@ func (n NoopAggregatorConfigurator) Run(ctx context.Context, transport *options.
return nil, nil
}
func (n *NoopAggregatorConfigurator) SetCRDInformer(_ apiextensionsinformers.CustomResourceDefinitionInformer) {
// noop
}
func ProvideNoopAggregatorConfigurator() AggregatorRunner {
return &NoopAggregatorConfigurator{}
}
@@ -3,6 +3,7 @@ package aggregatorrunner
import (
"context"
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
genericapiserver "k8s.io/apiserver/pkg/server"
@@ -21,4 +22,8 @@ type AggregatorRunner interface {
// Run starts the complete apiserver chain, expects it executes any logic inside a goroutine and doesn't block. Returns the running server.
Run(ctx context.Context, transport *options.RoundTripperFunc, stoppedCh chan error) (*genericapiserver.GenericAPIServer, error)
// SetCRDInformer sets the CRD informer for auto-registering APIServices for CRDs.
// This should be called before Configure if CRD API is enabled.
SetCRDInformer(informer apiextensionsinformers.CustomResourceDefinitionInformer)
}
+10 -6
View File
@@ -346,6 +346,7 @@ func (s *service) start(ctx context.Context) error {
serverConfig.MaxRequestBodyBytes = MaxRequestBodyBytes
var optsregister apistore.StorageOptionsRegister
var restOptsGetter *apistore.RESTOptionsGetter
if o.StorageOptions.StorageType == grafanaapiserveroptions.StorageTypeEtcd {
if err := o.RecommendedOptions.Etcd.Validate(); len(err) > 0 {
@@ -355,9 +356,9 @@ func (s *service) start(ctx context.Context) error {
return err
}
} else {
getter := apistore.NewRESTOptionsGetterForClient(s.unified, s.secrets, o.RecommendedOptions.Etcd.StorageConfig, s.restConfigProvider)
optsregister = getter.RegisterOptions
serverConfig.RESTOptionsGetter = getter
restOptsGetter = apistore.NewRESTOptionsGetterForClient(s.unified, s.secrets, o.RecommendedOptions.Etcd.StorageConfig, s.restConfigProvider)
optsregister = restOptsGetter.RegisterOptions
serverConfig.RESTOptionsGetter = restOptsGetter
}
defGetters := []common.GetOpenAPIDefinitions{
@@ -398,8 +399,11 @@ func (s *service) start(ctx context.Context) error {
return fmt.Errorf("failed to register post start hooks for app installers: %w", err)
}
// Create the server
server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
// Determine the delegate for the main server
var delegationTarget = genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler)
// Create the main Grafana API server
server, err := serverConfig.Complete().New("grafana-apiserver", delegationTarget)
if err != nil {
return err
}
@@ -671,4 +675,4 @@ func useNamespaceFromPath(path string, user *user.SignedInUser) {
}
}
}
}
}
+7 -8
View File
@@ -777,6 +777,13 @@ var (
Owner: grafanaAppPlatformSquad,
RequiresRestart: true,
},
{
Name: "apiExtensions",
Description: "Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration (Enterprise + MT-only)",
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
RequiresRestart: true,
},
{
Name: "groupByVariable",
Description: "Enable groupBy variable support in scenes dashboards",
@@ -1148,14 +1155,6 @@ var (
Owner: identityAccessTeam,
HideFromDocs: true,
},
{
Name: "exploreMetricsRelatedLogs",
Description: "Display Related Logs in Grafana Metrics Drilldown",
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityMetricsSquad,
FrontendOnly: true,
HideFromDocs: false,
},
{
Name: "prometheusSpecialCharsInLabelValues",
Description: "Adds support for quotes and special characters in label values for Prometheus queries",
+1 -1
View File
@@ -107,6 +107,7 @@ sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,fa
sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true
kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
kubernetesAggregatorCapTokenAuth,experimental,@grafana/grafana-app-platform-squad,false,true,false
apiExtensions,experimental,@grafana/grafana-app-platform-squad,false,true,false
groupByVariable,experimental,@grafana/dashboards-squad,false,false,false
scopeFilters,experimental,@grafana/dashboards-squad,false,false,false
oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false
@@ -159,7 +160,6 @@ newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false
passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false
exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true
prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true
enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false
enableSCIM,preview,@grafana/identity-access-team,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
107 sqlExpressionsColumnAutoComplete experimental @grafana/datapro false false true
108 kubernetesAggregator experimental @grafana/grafana-app-platform-squad false true false
109 kubernetesAggregatorCapTokenAuth experimental @grafana/grafana-app-platform-squad false true false
110 apiExtensions experimental @grafana/grafana-app-platform-squad false true false
111 groupByVariable experimental @grafana/dashboards-squad false false false
112 scopeFilters experimental @grafana/dashboards-squad false false false
113 oauthRequireSubClaim experimental @grafana/identity-access-team false false false
160 azureMonitorDisableLogLimit GA @grafana/partner-datasources false false false
161 playlistsReconciler experimental @grafana/grafana-app-platform-squad false true false
162 passwordlessMagicLinkAuthentication experimental @grafana/identity-access-team false false false
exploreMetricsRelatedLogs experimental @grafana/observability-metrics false false true
163 prometheusSpecialCharsInLabelValues experimental @grafana/oss-big-tent false false true
164 enableExtensionsAdminPage experimental @grafana/plugins-platform-backend false true false
165 enableSCIM preview @grafana/identity-access-team false false false
+4
View File
@@ -319,6 +319,10 @@ const (
// Enable CAP token based authentication in grafana&#39;s embedded kube-aggregator
FlagKubernetesAggregatorCapTokenAuth = "kubernetesAggregatorCapTokenAuth"
// FlagApiExtensions
// Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration
FlagApiExtensions = "apiExtensions"
// FlagGroupByVariable
// Enable groupBy variable support in scenes dashboards
FlagGroupByVariable = "groupByVariable"
+15 -1
View File
@@ -632,6 +632,19 @@
"expression": "true"
}
},
{
"metadata": {
"name": "apiExtensions",
"resourceVersion": "1764159104213",
"creationTimestamp": "2025-11-26T12:11:44Z"
},
"spec": {
"description": "Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad",
"requiresRestart": true
}
},
{
"metadata": {
"name": "appPlatformGrpcClientAuth",
@@ -1408,7 +1421,8 @@
"metadata": {
"name": "exploreMetricsRelatedLogs",
"resourceVersion": "1764664939750",
"creationTimestamp": "2024-11-05T16:28:43Z"
"creationTimestamp": "2024-11-05T16:28:43Z",
"deletionTimestamp": "2026-01-09T22:14:53Z"
},
"spec": {
"description": "Display Related Logs in Grafana Metrics Drilldown",
+1
View File
@@ -177,6 +177,7 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req
return true
}, claims.NoopZookie{}, nil
}
if !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) {
span.SetAttributes(attribute.Bool("allowed", false))
span.SetStatus(codes.Error, "Namespace mismatch")
+4 -4
View File
@@ -78,13 +78,13 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event {
cache := gocache.New(cacheTTL, cacheCleanupInterval)
events := make(chan Event, opts.BufferSize)
initialRV, err := n.lastEventResourceVersion(ctx)
lastRV, err := n.lastEventResourceVersion(ctx)
if errors.Is(err, ErrNotFound) {
initialRV = snowflakeFromTime(time.Now()) // No events yet, start from the beginning
lastRV = 0 // No events yet, start from the beginning
} else if err != nil {
n.log.Error("Failed to get last event resource version", "error", err)
}
lastRV := initialRV + 1 // We want to start watching from the next event
lastRV = lastRV + 1 // We want to start watching from the next event
go func() {
defer close(events)
@@ -110,7 +110,7 @@ func (n *notifier) Watch(ctx context.Context, opts watchOptions) <-chan Event {
}
// Skip old events lower than the requested resource version
if evt.ResourceVersion <= initialRV {
if evt.ResourceVersion < lastRV {
continue
}
+7 -15
View File
@@ -25,7 +25,6 @@ func setupTestNotifier(t *testing.T) (*notifier, *eventStore) {
return notifier, eventStore
}
// nolint:unused
func setupTestNotifierSqlKv(t *testing.T) (*notifier, *eventStore) {
dbstore := db.InitTestDB(t)
eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil)
@@ -60,8 +59,7 @@ func runNotifierTestWith(t *testing.T, storeName string, newStoreFn func(*testin
func TestNotifier_lastEventResourceVersion(t *testing.T) {
runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierLastEventResourceVersion)
// enable this when sqlkv is ready
// runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierLastEventResourceVersion)
runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierLastEventResourceVersion)
}
func testNotifierLastEventResourceVersion(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) {
@@ -112,8 +110,7 @@ func testNotifierLastEventResourceVersion(t *testing.T, ctx context.Context, not
func TestNotifier_cachekey(t *testing.T) {
runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierCachekey)
// enable this when sqlkv is ready
// runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierCachekey)
runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierCachekey)
}
func testNotifierCachekey(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) {
@@ -167,8 +164,7 @@ func testNotifierCachekey(t *testing.T, ctx context.Context, notifier *notifier,
func TestNotifier_Watch_NoEvents(t *testing.T) {
runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchNoEvents)
// enable this when sqlkv is ready
// runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchNoEvents)
runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchNoEvents)
}
func testNotifierWatchNoEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) {
@@ -209,8 +205,7 @@ func testNotifierWatchNoEvents(t *testing.T, ctx context.Context, notifier *noti
func TestNotifier_Watch_WithExistingEvents(t *testing.T) {
runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchWithExistingEvents)
// enable this when sqlkv is ready
// runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchWithExistingEvents)
runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchWithExistingEvents)
}
func testNotifierWatchWithExistingEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) {
@@ -284,8 +279,7 @@ func testNotifierWatchWithExistingEvents(t *testing.T, ctx context.Context, noti
func TestNotifier_Watch_EventDeduplication(t *testing.T) {
runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchEventDeduplication)
// enable this when sqlkv is ready
// runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchEventDeduplication)
runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchEventDeduplication)
}
func testNotifierWatchEventDeduplication(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) {
@@ -351,8 +345,7 @@ func testNotifierWatchEventDeduplication(t *testing.T, ctx context.Context, noti
func TestNotifier_Watch_ContextCancellation(t *testing.T) {
runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchContextCancellation)
// enable this when sqlkv is ready
// runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchContextCancellation)
runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchContextCancellation)
}
func testNotifierWatchContextCancellation(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) {
@@ -398,8 +391,7 @@ func testNotifierWatchContextCancellation(t *testing.T, ctx context.Context, not
func TestNotifier_Watch_MultipleEvents(t *testing.T) {
runNotifierTestWith(t, "badger", setupTestNotifier, testNotifierWatchMultipleEvents)
// enable this when sqlkv is ready
// runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchMultipleEvents)
runNotifierTestWith(t, "sqlkv", setupTestNotifierSqlKv, testNotifierWatchMultipleEvents)
}
func testNotifierWatchMultipleEvents(t *testing.T, ctx context.Context, notifier *notifier, eventStore *eventStore) {
@@ -346,7 +346,8 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in
return 0, fmt.Errorf("failed to write data: %w", err)
}
dataKey.ResourceVersion = rvmanager.SnowflakeFromRv(rv)
rv = rvmanager.SnowflakeFromRv(rv)
dataKey.ResourceVersion = rv
} else {
err := k.dataStore.Save(ctx, dataKey, bytes.NewReader(event.Value))
if err != nil {
@@ -9,7 +9,6 @@ import (
"testing"
"time"
"github.com/bwmarrin/snowflake"
"github.com/stretchr/testify/require"
claims "github.com/grafana/authlib/types"
@@ -187,13 +186,30 @@ func runKeyPathTest(t *testing.T, backend resource.StorageBackend, nsPrefix stri
// verifyKeyPath is a helper function to verify key_path generation
func verifyKeyPath(t *testing.T, db sqldb.DB, ctx context.Context, key *resourcepb.ResourceKey, action string, resourceVersion int64, expectedFolder string) {
// For SQL backend (namespace contains "-sql"), resourceVersion is in microsecond format
// but key_path stores snowflake RV, so convert to snowflake
// For KV backend (namespace contains "-kv"), resourceVersion is already in snowflake format
isSqlBackend := strings.Contains(key.Namespace, "-sql")
var keyPathRV int64
if isSqlBackend {
// Convert microsecond RV to snowflake for key_path construction
keyPathRV = rvmanager.SnowflakeFromRv(resourceVersion)
} else {
// KV backend already provides snowflake RV
keyPathRV = resourceVersion
}
// Build the expected key_path using DataKey format: unified/data/group/resource/namespace/name/resourceVersion~action~folder
expectedKeyPath := fmt.Sprintf("unified/data/%s/%s/%s/%s/%d~%s~%s", key.Group, key.Resource, key.Namespace, key.Name, keyPathRV, action, expectedFolder)
var query string
if db.DriverName() == "postgres" {
query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = $1 AND name = $2 AND resource_version = $3"
query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE key_path = $1"
} else {
query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = ? AND name = ? AND resource_version = ?"
query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE key_path = ?"
}
rows, err := db.QueryContext(ctx, query, key.Namespace, key.Name, resourceVersion)
rows, err := db.QueryContext(ctx, query, expectedKeyPath)
require.NoError(t, err)
require.True(t, rows.Next(), "Resource not found in resource_history table - both SQL and KV backends should write to this table")
@@ -220,10 +236,6 @@ func verifyKeyPath(t *testing.T, db sqldb.DB, ctx context.Context, key *resource
// Verify action suffix
require.Contains(t, keyPath, fmt.Sprintf("~%s~", action))
// Verify snowflake calculation
expectedSnowflake := (((resourceVersion / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (resourceVersion % 1000)
require.Contains(t, keyPath, fmt.Sprintf("/%d~", expectedSnowflake), "actual RV: %d", actualRV)
// Verify folder if specified
if expectedFolder != "" {
require.Equal(t, expectedFolder, actualFolder)
@@ -492,10 +504,10 @@ func verifyResourceHistoryRecord(t *testing.T, record ResourceHistoryRecord, exp
}
// Validate previous_resource_version
// For KV backend operations, resource versions are stored as snowflake format
// but expectedPrevRV is in microsecond format, so we need to use IsRvEqual for comparison
// For KV backend operations, expectedPrevRV is now in snowflake format (returned by KV backend)
// but resource_history table stores microsecond RV, so we need to use IsRvEqual for comparison
if strings.Contains(record.Namespace, "-kv") {
require.True(t, rvmanager.IsRvEqual(record.PreviousResourceVersion, expectedPrevRV),
require.True(t, rvmanager.IsRvEqual(expectedPrevRV, record.PreviousResourceVersion),
"Previous resource version should match (KV backend snowflake format)")
} else {
require.Equal(t, expectedPrevRV, record.PreviousResourceVersion)
@@ -505,9 +517,10 @@ func verifyResourceHistoryRecord(t *testing.T, record ResourceHistoryRecord, exp
require.Equal(t, expectedGeneration, record.Generation)
// Validate resource_version
// For KV backend operations, resource versions are stored as snowflake format
// For KV backend operations, expectedRV is now in snowflake format (returned by KV backend)
// but resource_history table stores microsecond RV, so we need to use IsRvEqual for comparison
if strings.Contains(record.Namespace, "-kv") {
require.True(t, rvmanager.IsRvEqual(record.ResourceVersion, expectedRV),
require.True(t, rvmanager.IsRvEqual(expectedRV, record.ResourceVersion),
"Resource version should match (KV backend snowflake format)")
} else {
require.Equal(t, expectedRV, record.ResourceVersion)
@@ -574,7 +587,7 @@ func verifyResourceTable(t *testing.T, db sqldb.DB, namespace string, resources
// Resource version should match the expected version for test-resource-3 (updated version)
expectedRV := resourceVersions[2][1] // test-resource-3's update version
if strings.Contains(namespace, "-kv") {
require.True(t, rvmanager.IsRvEqual(record.ResourceVersion, expectedRV),
require.True(t, rvmanager.IsRvEqual(expectedRV, record.ResourceVersion),
"Resource version should match (KV backend snowflake format)")
} else {
require.Equal(t, expectedRV, record.ResourceVersion)
@@ -625,9 +638,16 @@ func verifyResourceVersionTable(t *testing.T, db sqldb.DB, namespace string, res
// The resource_version table should contain the latest RV for the group+resource
// It might be slightly higher due to RV manager operations, so check it's at least our max
require.GreaterOrEqual(t, record.ResourceVersion, maxRV, "resource_version should be at least the latest RV we tracked")
// But it shouldn't be too much higher (within a reasonable range)
require.LessOrEqual(t, record.ResourceVersion, maxRV+100, "resource_version shouldn't be much higher than expected")
// For KV backend, maxRV is in snowflake format but record.ResourceVersion is in microsecond format
// Use IsRvEqual for proper comparison between different RV formats
isKvBackend := strings.Contains(namespace, "-kv")
recordResourceVersion := record.ResourceVersion
if isKvBackend {
recordResourceVersion = rvmanager.SnowflakeFromRv(record.ResourceVersion)
}
require.Less(t, recordResourceVersion, int64(9223372036854775807), "resource_version should be reasonable")
require.Greater(t, recordResourceVersion, maxRV, "resource_version should be at least the latest RV we tracked")
}
// runTestCrossBackendConsistency tests basic consistency between SQL and KV backends (lightweight)
@@ -38,7 +38,6 @@ func TestBadgerKVStorageBackend(t *testing.T) {
func TestSQLKVStorageBackend(t *testing.T) {
skipTests := map[string]bool{
TestHappyPath: true,
TestWatchWriteEvents: true,
TestList: true,
TestBlobSupport: true,
@@ -51,21 +50,24 @@ func TestSQLKVStorageBackend(t *testing.T) {
TestGetResourceLastImportTime: true,
TestOptimisticLocking: true,
}
// without RvManager
RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend {
backend, _ := NewTestSqlKvBackend(t, ctx, false)
return backend
}, &TestOptions{
NSPrefix: "sqlkvstorage-test",
SkipTests: skipTests,
t.Run("Without RvManager", func(t *testing.T) {
RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend {
backend, _ := NewTestSqlKvBackend(t, ctx, false)
return backend
}, &TestOptions{
NSPrefix: "sqlkvstorage-test",
SkipTests: skipTests,
})
})
// with RvManager
RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend {
backend, _ := NewTestSqlKvBackend(t, ctx, true)
return backend
}, &TestOptions{
NSPrefix: "sqlkvstorage-withrvmanager-test",
SkipTests: skipTests,
t.Run("With RvManager", func(t *testing.T) {
RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend {
backend, _ := NewTestSqlKvBackend(t, ctx, true)
return backend
}, &TestOptions{
NSPrefix: "sqlkvstorage-withrvmanager-test",
SkipTests: skipTests,
})
})
}
@@ -0,0 +1,84 @@
import { render, screen } from '@testing-library/react';
import { VariableHide } from '@grafana/data';
import { SceneGridLayout, SceneVariable, SceneVariableSet, ScopesVariable, TextBoxVariable } from '@grafana/scenes';
import { DashboardScene } from './DashboardScene';
import { VariableControls } from './VariableControls';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
jest.mock('@grafana/runtime', () => {
const runtime = jest.requireActual('@grafana/runtime');
return {
...runtime,
config: {
...runtime.config,
featureToggles: {
dashboardNewLayouts: true,
},
},
};
});
describe('VariableControls', () => {
it('should not render scopes variable', () => {
const variables = [new ScopesVariable({})];
const dashboard = buildScene(variables);
dashboard.activate();
render(<VariableControls dashboard={dashboard} />);
expect(screen.queryByText('__scopes')).not.toBeInTheDocument();
});
it('should not render regular hidden variables', () => {
const hiddenVariable = new TextBoxVariable({
name: 'HiddenVar',
hide: VariableHide.hideVariable,
});
const variables = [hiddenVariable];
const dashboard = buildScene(variables);
dashboard.activate();
render(<VariableControls dashboard={dashboard} />);
expect(screen.queryByText('HiddenVar')).not.toBeInTheDocument();
});
it('should render regular hidden variables in edit mode', async () => {
const hiddenVariable = new TextBoxVariable({
name: 'HiddenVar',
hide: VariableHide.hideVariable,
});
const variables = [hiddenVariable];
const dashboard = buildScene(variables);
dashboard.activate();
dashboard.setState({ isEditing: true });
render(<VariableControls dashboard={dashboard} />);
expect(await screen.findByText('HiddenVar')).toBeInTheDocument();
});
it('should not render variables hidden in controls menu in edit mode', async () => {
const dashboard = buildScene([new TextBoxVariable({ name: 'TextVarControls', hide: VariableHide.inControlsMenu })]);
dashboard.activate();
dashboard.setState({ isEditing: true });
render(<VariableControls dashboard={dashboard} />);
expect(screen.queryByText('TextVarControls')).not.toBeInTheDocument();
});
});
function buildScene(variables: SceneVariable[] = []) {
const dashboard = new DashboardScene({
$variables: new SceneVariableSet({ variables }),
body: new DefaultGridLayoutManager({
grid: new SceneGridLayout({
children: [],
}),
}),
});
return dashboard;
}
@@ -39,8 +39,9 @@ export function VariableControls({ dashboard }: { dashboard: DashboardScene }) {
? restVariables.filter((v) => v.state.hide !== VariableHide.inControlsMenu)
: variables.filter(
(v) =>
// used for scopes variables, should always be hidden
// if we're editing in dynamic dashboards, still shows hidden variable but greyed out
(isEditingNewLayouts && v.state.hide === VariableHide.hideVariable) ||
(!v.UNSAFE_renderAsHidden && isEditingNewLayouts && v.state.hide === VariableHide.hideVariable) ||
v.state.hide !== VariableHide.inControlsMenu
);