diff --git a/conf/defaults.ini b/conf/defaults.ini
index 28cd0420eb2..363ca39d0c4 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -1653,6 +1653,11 @@ loki_basic_auth_password =
# Accepts duration formats like: 30s, 1m, 1h.
rule_query_offset = 1m
+# Default data source UID to use for query execution when importing Prometheus rules.
+# This default is used when the X-Grafana-Alerting-Datasource-UID header is not provided.
+# If not set, the header becomes required.
+default_datasource_uid =
+
[recording_rules]
# Enable recording rules.
enabled = true
diff --git a/conf/sample.ini b/conf/sample.ini
index 0bb5b82fdc9..530b14c87ac 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -1615,6 +1615,11 @@ max_annotations_to_keep =
# Accepts duration formats like: 30s, 1m, 1h.
rule_query_offset = 1m
+# Default data source UID to use for query execution when importing Prometheus rules.
+# This default is used when the X-Grafana-Alerting-Datasource-UID header is not provided.
+# If not set, the header becomes required.
+default_datasource_uid =
+
#################################### Recording Rules #####################
[recording_rules]
# Enable recording rules.
diff --git a/docs/sources/alerting/alerting-rules/alerting-migration.md b/docs/sources/alerting/alerting-rules/alerting-migration.md
index 3afa3aec453..ab2a5e995cd 100644
--- a/docs/sources/alerting/alerting-rules/alerting-migration.md
+++ b/docs/sources/alerting/alerting-rules/alerting-migration.md
@@ -242,6 +242,8 @@ Set to `true` to import recording rules in paused state.
The UID of the data source to use for alert rule queries.
+If not specified in the header, Grafana uses the configured default from `unified_alerting.prometheus_conversion.default_datasource_uid`. If neither the header nor the configuration option is provided, the request fails.
+
#### `X-Grafana-Alerting-Target-Datasource-UID`
The UID of the target data source for recording rules. If not specified, the value from `X-Grafana-Alerting-Datasource-UID` is used.
diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md
index 05d9cb66228..67c361b2bdc 100644
--- a/docs/sources/setup-grafana/configure-grafana/_index.md
+++ b/docs/sources/setup-grafana/configure-grafana/_index.md
@@ -2052,6 +2052,10 @@ This section applies only to rules imported as Grafana-managed rules. For more i
Set the query offset to imported Grafana-managed rules when `query_offset` is not defined in the original rule group configuration. The default value is `1m`.
+#### `default_datasource_uid`
+
+Set the default data source UID to use for query execution when importing Prometheus rules. Grafana uses this default when the `X-Grafana-Alerting-Datasource-UID` header isn't provided during import. If this option isn't set, the header becomes required. The default value is empty.
+
### `[annotations]`
diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go
index 79198ababf1..b6849f55dfc 100644
--- a/pkg/services/ngalert/api/api_convert_prometheus.go
+++ b/pkg/services/ngalert/api/api_convert_prometheus.go
@@ -375,6 +375,9 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
}
datasourceUID := strings.TrimSpace(c.Req.Header.Get(datasourceUIDHeader))
+ if datasourceUID == "" {
+ datasourceUID = srv.cfg.PrometheusConversion.DefaultDatasourceUID
+ }
if datasourceUID == "" {
return response.Err(errDatasourceUIDHeaderMissing)
}
diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go
index b4377431249..74e851ed195 100644
--- a/pkg/services/ngalert/api/api_convert_prometheus_test.go
+++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go
@@ -75,6 +75,46 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
require.Contains(t, string(response.Body()), "Missing datasource UID header")
})
+ t.Run("without datasource UID header but with config default should succeed", func(t *testing.T) {
+ srv, _, ruleStore := createConvertPrometheusSrv(t)
+ // Set the config default
+ srv.cfg.PrometheusConversion.DefaultDatasourceUID = existingDSUID
+
+ rc := createRequestCtx()
+ rc.Req.Header.Set(datasourceUIDHeader, "")
+
+ response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
+
+ require.Equal(t, http.StatusAccepted, response.Status())
+
+ // Verify that the config default datasource was used
+ assertRulesUseDatasource(t, ruleStore, existingDSUID, 2)
+ })
+
+ t.Run("header should take precedence over config default", func(t *testing.T) {
+ srv, dsCache, ruleStore := createConvertPrometheusSrv(t)
+ // Add another datasource
+ anotherDS := &datasources.DataSource{
+ UID: "another-ds",
+ Type: datasources.DS_PROMETHEUS,
+ }
+ dsCache.DataSources = append(dsCache.DataSources, anotherDS)
+
+ // Set the config default to one DS
+ srv.cfg.PrometheusConversion.DefaultDatasourceUID = "another-ds"
+
+ // But use the header to specify a different one
+ rc := createRequestCtx()
+ rc.Req.Header.Set(datasourceUIDHeader, existingDSUID)
+
+ response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
+
+ require.Equal(t, http.StatusAccepted, response.Status())
+
+ // Verify that the header datasource was used, not the config default
+ assertRulesUseDatasource(t, ruleStore, existingDSUID, 2)
+ })
+
t.Run("with invalid datasource should return error", func(t *testing.T) {
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
@@ -1761,6 +1801,26 @@ func createRequestCtx() *contextmodel.ReqContext {
}
}
+// assertRulesUseDatasource retrieves all alert rules from the store and verifies they use the expected datasource
+func assertRulesUseDatasource(t *testing.T, ruleStore *fakes.RuleStore, expectedDatasourceUID string, expectedRuleCount int) {
+ t.Helper()
+
+ rules, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
+ OrgID: 1,
+ })
+ require.NoError(t, err)
+ require.Len(t, rules, expectedRuleCount)
+
+ for _, rule := range rules {
+ if rule.Record == nil {
+ require.NotEmpty(t, rule.Data)
+ require.Equal(t, expectedDatasourceUID, rule.Data[0].DatasourceUID, rule.Title, expectedDatasourceUID)
+ } else {
+ require.Equal(t, expectedDatasourceUID, rule.Record.TargetDatasourceUID)
+ }
+ }
+}
+
// Test parseBooleanHeader function which handles boolean header values
func TestParseBooleanHeader(t *testing.T) {
headerName := "X-Test-Header"
diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go
index 0733e8241e6..743f386ff52 100644
--- a/pkg/setting/setting_unified_alerting.go
+++ b/pkg/setting/setting_unified_alerting.go
@@ -190,6 +190,8 @@ type UnifiedAlertingReservedLabelSettings struct {
type UnifiedAlertingPrometheusConversionSettings struct {
// RuleQueryOffset defines a time offset to apply to rule queries during conversion from Prometheus to Grafana format
RuleQueryOffset time.Duration
+ // DefaultDatasourceUID is the default datasource UID to use when converting Prometheus rules if not specified via header
+ DefaultDatasourceUID string
}
type UnifiedAlertingLokiSettings struct {
@@ -536,7 +538,8 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error {
prometheusConversion := iniFile.Section("unified_alerting.prometheus_conversion")
uaCfg.PrometheusConversion = UnifiedAlertingPrometheusConversionSettings{
- RuleQueryOffset: prometheusConversion.Key("rule_query_offset").MustDuration(time.Minute),
+ RuleQueryOffset: prometheusConversion.Key("rule_query_offset").MustDuration(time.Minute),
+ DefaultDatasourceUID: prometheusConversion.Key("default_datasource_uid").MustString(""),
}
rr := iniFile.Section("recording_rules")