| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | package api | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 	"encoding/json" | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	"path/filepath" | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	"strconv" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 	amconfig "github.com/prometheus/alertmanager/config" | 
					
						
							|  |  |  | 	"github.com/prometheus/alertmanager/pkg/labels" | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	prommodel "github.com/prometheus/common/model" | 
					
						
							|  |  |  | 	"gopkg.in/yaml.v3" | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/api/response" | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/apimachinery/errutil" | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/infra/log" | 
					
						
							|  |  |  | 	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/dashboards" | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/datasources" | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/featuremgmt" | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/folder" | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | 	apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/ngalert/api/validation" | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/ngalert/models" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/services/ngalert/prom" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/services/ngalert/provisioning" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/setting" | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/util" | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ( | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	// datasourceUIDHeader is the name of the header that specifies the UID of the datasource to be used for the rules.
 | 
					
						
							|  |  |  | 	datasourceUIDHeader = "X-Grafana-Alerting-Datasource-UID" | 
					
						
							| 
									
										
										
										
											2025-03-18 16:53:50 +08:00
										 |  |  | 	// targetDatasourceUIDHeader is the name of the header that specifies the UID of the target datasource to be used for recording rules.
 | 
					
						
							|  |  |  | 	targetDatasourceUIDHeader = "X-Grafana-Alerting-Target-Datasource-UID" | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// If the folderUIDHeader is present, namespaces and rule groups will be created in the specified folder.
 | 
					
						
							|  |  |  | 	// If not, the root folder will be used as the default.
 | 
					
						
							|  |  |  | 	folderUIDHeader = "X-Grafana-Alerting-Folder-UID" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// These headers control the paused state of newly created rules. By default, rules are not paused.
 | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	recordingRulesPausedHeader = "X-Grafana-Alerting-Recording-Rules-Paused" | 
					
						
							|  |  |  | 	alertRulesPausedHeader     = "X-Grafana-Alerting-Alert-Rules-Paused" | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// notificationSettingsHeader is the header that specifies the notification settings to be used for the rules.
 | 
					
						
							|  |  |  | 	// The value should be a JSON-encoded AlertRuleNotificationSettings object.
 | 
					
						
							|  |  |  | 	notificationSettingsHeader = "X-Grafana-Alerting-Notification-Settings" | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// mergeMatchersHeader is the header that specifies the merge matchers for imported Alertmanager config.
 | 
					
						
							|  |  |  | 	// The value should be comma-separated key=value pairs, e.g., "environment=production,team=alerting".
 | 
					
						
							|  |  |  | 	mergeMatchersHeader = "X-Grafana-Alerting-Merge-Matchers" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// configIdentifierHeader is the header that specifies the identifier for imported Alertmanager config.
 | 
					
						
							|  |  |  | 	configIdentifierHeader = "X-Grafana-Alerting-Config-Identifier" | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | var ( | 
					
						
							|  |  |  | 	errDatasourceUIDHeaderMissing = errutil.ValidationFailed( | 
					
						
							|  |  |  | 		"alerting.datasourceUIDHeaderMissing", | 
					
						
							|  |  |  | 		errutil.WithPublicMessage(fmt.Sprintf("Missing datasource UID header: %s", datasourceUIDHeader)), | 
					
						
							|  |  |  | 	).Errorf("missing datasource UID header") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 	errInvalidHeaderValueMsg  = "Invalid value for header {{.Public.Header}}: {{.Public.Error}}" | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | 	errInvalidHeaderValueBase = errutil.ValidationFailed("alerting.invalidHeaderValue").MustTemplate(errInvalidHeaderValueMsg, errutil.WithPublic(errInvalidHeaderValueMsg)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	errRecordingRulesNotEnabled = errutil.ValidationFailed( | 
					
						
							|  |  |  | 		"alerting.recordingRulesNotEnabled", | 
					
						
							|  |  |  | 		errutil.WithPublicMessage("Cannot import recording rules: Feature not enabled."), | 
					
						
							|  |  |  | 	).Errorf("recording rules not enabled") | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | func errInvalidHeaderValue(header string, err error) error { | 
					
						
							|  |  |  | 	return errInvalidHeaderValueBase.Build(errutil.TemplateData{Public: map[string]any{"Header": header, "Error": err}}) | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | // ConvertPrometheusSrv converts Prometheus rules to Grafana rules
 | 
					
						
							|  |  |  | // and retrieves them in a Prometheus-compatible format.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // It is designed to support mimirtool integration, so that rules that work with Mimir
 | 
					
						
							|  |  |  | // can be imported into Grafana. It works similarly to the provisioning API,
 | 
					
						
							|  |  |  | // where once a rule group is created, it is marked as "provisioned" (via provenance mechanism)
 | 
					
						
							|  |  |  | // and is not editable in the UI.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This service returns only rule groups that were initially imported from Prometheus-compatible sources.
 | 
					
						
							|  |  |  | // Rule groups not imported from Prometheus are excluded because their original rule definitions are unavailable.
 | 
					
						
							|  |  |  | // When a rule group is converted from Prometheus to Grafana, the original definition is preserved alongside
 | 
					
						
							|  |  |  | // the Grafana rule and used for reading requests here.
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | //
 | 
					
						
							|  |  |  | // Folder Structure Handling:
 | 
					
						
							|  |  |  | // mimirtool does not support nested folder structures, while Grafana allows folder nesting.
 | 
					
						
							|  |  |  | // To keep compatibility, this service only returns direct child folders of the working folder
 | 
					
						
							|  |  |  | // as namespaces, and rule groups and rules that are directly in these child folders.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // For example, given this folder structure in Grafana:
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | //	grafana/
 | 
					
						
							|  |  |  | //	├── production/
 | 
					
						
							|  |  |  | //	│   ├── service1/
 | 
					
						
							|  |  |  | //	│   │   └── alerts/
 | 
					
						
							|  |  |  | //	│   └── service2/
 | 
					
						
							|  |  |  | //	└── testing/
 | 
					
						
							|  |  |  | //	    └── service3/
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // If the working folder is "grafana":
 | 
					
						
							|  |  |  | //   - Only namespaces "production" and "testing" are returned
 | 
					
						
							|  |  |  | //   - Only rule groups directly within these folders are included
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // If the working folder is "production":
 | 
					
						
							|  |  |  | //   - Only namespaces "service1" and "service2" are returned
 | 
					
						
							|  |  |  | //   - Only rule groups directly within these folders are included
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // The "working folder" is specified by the X-Grafana-Alerting-Folder-UID header, which can be set to any folder UID,
 | 
					
						
							|  |  |  | // and defaults to the root folder if not provided.
 | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | type ConvertPrometheusSrv struct { | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	cfg              *setting.UnifiedAlertingSettings | 
					
						
							|  |  |  | 	logger           log.Logger | 
					
						
							|  |  |  | 	ruleStore        RuleStore | 
					
						
							|  |  |  | 	datasourceCache  datasources.CacheService | 
					
						
							|  |  |  | 	alertRuleService *provisioning.AlertRuleService | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | 	featureToggles   featuremgmt.FeatureToggles | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 	am               Alertmanager | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type Alertmanager interface { | 
					
						
							| 
									
										
										
										
											2025-06-18 18:37:56 +08:00
										 |  |  | 	DeleteExtraConfiguration(ctx context.Context, org int64, identifier string) error | 
					
						
							| 
									
										
										
										
											2025-06-18 21:05:06 +08:00
										 |  |  | 	SaveAndApplyExtraConfiguration(ctx context.Context, org int64, extraConfig apimodels.ExtraConfiguration) error | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 	GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool) (apimodels.GettableUserConfig, error) | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | func NewConvertPrometheusSrv( | 
					
						
							|  |  |  | 	cfg *setting.UnifiedAlertingSettings, | 
					
						
							|  |  |  | 	logger log.Logger, | 
					
						
							|  |  |  | 	ruleStore RuleStore, | 
					
						
							|  |  |  | 	datasourceCache datasources.CacheService, | 
					
						
							|  |  |  | 	alertRuleService *provisioning.AlertRuleService, | 
					
						
							|  |  |  | 	featureToggles featuremgmt.FeatureToggles, | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 	am Alertmanager, | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | ) *ConvertPrometheusSrv { | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	return &ConvertPrometheusSrv{ | 
					
						
							|  |  |  | 		cfg:              cfg, | 
					
						
							|  |  |  | 		logger:           logger, | 
					
						
							|  |  |  | 		ruleStore:        ruleStore, | 
					
						
							|  |  |  | 		datasourceCache:  datasourceCache, | 
					
						
							|  |  |  | 		alertRuleService: alertRuleService, | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | 		featureToggles:   featureToggles, | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 		am:               am, | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | // RouteConvertPrometheusGetRules returns all Grafana-managed alert rules in all namespaces (folders)
 | 
					
						
							|  |  |  | // that were imported from a Prometheus-compatible source.
 | 
					
						
							|  |  |  | // It responds with a YAML containing a mapping of folders to arrays of Prometheus rule groups.
 | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response { | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	workingFolderUID := getWorkingFolderUID(c) | 
					
						
							|  |  |  | 	logger = logger.New("working_folder_uid", workingFolderUID) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-10 20:42:23 +08:00
										 |  |  | 	folders, err := srv.ruleStore.GetNamespaceChildren(c.Req.Context(), workingFolderUID, c.GetOrgID(), c.SignedInUser) | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	if len(folders) == 0 || errors.Is(err, dashboards.ErrFolderNotFound) { | 
					
						
							|  |  |  | 		// If there is no such folder or no children, return empty response
 | 
					
						
							|  |  |  | 		// because mimirtool expects 200 OK response in this case.
 | 
					
						
							|  |  |  | 		return response.YAML(http.StatusOK, map[string][]apimodels.PrometheusRuleGroup{}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to get folders", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	folderUIDs := make([]string, 0, len(folders)) | 
					
						
							|  |  |  | 	for _, f := range folders { | 
					
						
							|  |  |  | 		folderUIDs = append(folderUIDs, f.UID) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	filterOpts := &provisioning.FilterOptions{ | 
					
						
							| 
									
										
										
										
											2025-06-05 18:08:44 +08:00
										 |  |  | 		HasPrometheusRuleDefinition: util.Pointer(true), | 
					
						
							|  |  |  | 		NamespaceUIDs:               folderUIDs, | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to get alert groups", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	namespaces, err := grafanaNamespacesToPrometheus(groups) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to convert Grafana rules to Prometheus format", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return response.YAML(http.StatusOK, namespaces) | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | // RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source
 | 
					
						
							|  |  |  | // within a specified namespace.
 | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response { | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	workingFolderUID := getWorkingFolderUID(c) | 
					
						
							|  |  |  | 	logger = logger.New("working_folder_uid", workingFolderUID) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger.Debug("Looking up folder by title", "folder_title", namespaceTitle) | 
					
						
							| 
									
										
										
										
											2025-04-10 20:42:23 +08:00
										 |  |  | 	namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.GetOrgID(), c.SignedInUser, workingFolderUID) | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return namespaceErrorResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	logger.Info("Deleting all Prometheus-imported rule groups", "folder_uid", namespace.UID, "folder_title", namespaceTitle) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 	provenance := getProvenance(c) | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	filterOpts := &provisioning.FilterOptions{ | 
					
						
							| 
									
										
										
										
											2025-06-05 18:08:44 +08:00
										 |  |  | 		NamespaceUIDs:               []string{namespace.UID}, | 
					
						
							|  |  |  | 		HasPrometheusRuleDefinition: util.Pointer(true), | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 	err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, provenance, filterOpts) | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	if errors.Is(err, models.ErrAlertRuleGroupNotFound) { | 
					
						
							|  |  |  | 		return response.Empty(http.StatusNotFound) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to delete rule groups", "folder_uid", namespace.UID, "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return successfulResponse() | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | // RouteConvertPrometheusDeleteRuleGroup deletes a specific rule group if it was imported from a Prometheus-compatible source.
 | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	workingFolderUID := getWorkingFolderUID(c) | 
					
						
							|  |  |  | 	logger = logger.New("working_folder_uid", workingFolderUID) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger.Debug("Looking up folder by title", "folder_title", namespaceTitle) | 
					
						
							| 
									
										
										
										
											2025-04-10 20:42:23 +08:00
										 |  |  | 	folder, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.GetOrgID(), c.SignedInUser, workingFolderUID) | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return namespaceErrorResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	logger.Info("Deleting Prometheus-imported rule group", "folder_uid", folder.UID, "folder_title", namespaceTitle, "group", group) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 	provenance := getProvenance(c) | 
					
						
							|  |  |  | 	err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, provenance) | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	if errors.Is(err, models.ErrAlertRuleGroupNotFound) { | 
					
						
							|  |  |  | 		return response.Empty(http.StatusNotFound) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to delete rule group", "folder_uid", folder.UID, "group", group, "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return successfulResponse() | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | // RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder).
 | 
					
						
							|  |  |  | // It responds with a YAML containing a mapping of a single folder to an array of Prometheus rule groups.
 | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response { | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	workingFolderUID := getWorkingFolderUID(c) | 
					
						
							|  |  |  | 	logger = logger.New("working_folder_uid", workingFolderUID) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger.Debug("Looking up folder by title", "folder_title", namespaceTitle) | 
					
						
							| 
									
										
										
										
											2025-04-10 20:42:23 +08:00
										 |  |  | 	namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.GetOrgID(), c.SignedInUser, workingFolderUID) | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to get folder", "error", err) | 
					
						
							|  |  |  | 		return namespaceErrorResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	filterOpts := &provisioning.FilterOptions{ | 
					
						
							| 
									
										
										
										
											2025-06-05 18:08:44 +08:00
										 |  |  | 		HasPrometheusRuleDefinition: util.Pointer(true), | 
					
						
							|  |  |  | 		NamespaceUIDs:               []string{namespace.UID}, | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to get alert groups", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	ns, err := grafanaNamespacesToPrometheus(groups) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to convert Grafana rules to Prometheus format", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return response.YAML(http.StatusOK, ns) | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | // RouteConvertPrometheusGetRuleGroup retrieves a single rule group for a given namespace (folder)
 | 
					
						
							|  |  |  | // in Prometheus-compatible YAML format if it was imported from a Prometheus-compatible source.
 | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	workingFolderUID := getWorkingFolderUID(c) | 
					
						
							|  |  |  | 	logger = logger.New("working_folder_uid", workingFolderUID) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger.Debug("Looking up folder by title", "folder_title", namespaceTitle) | 
					
						
							| 
									
										
										
										
											2025-04-10 20:42:23 +08:00
										 |  |  | 	namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.GetOrgID(), c.SignedInUser, workingFolderUID) | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to get folder", "error", err) | 
					
						
							|  |  |  | 		return namespaceErrorResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	if namespace == nil { | 
					
						
							|  |  |  | 		return response.Error(http.StatusNotFound, "Folder not found", nil) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	filterOpts := &provisioning.FilterOptions{ | 
					
						
							| 
									
										
										
										
											2025-06-05 18:08:44 +08:00
										 |  |  | 		HasPrometheusRuleDefinition: util.Pointer(true), | 
					
						
							|  |  |  | 		NamespaceUIDs:               []string{namespace.UID}, | 
					
						
							|  |  |  | 		RuleGroups:                  []string{group}, | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	groupsWithFolders, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to get alert group", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if len(groupsWithFolders) == 0 { | 
					
						
							|  |  |  | 		return response.Error(http.StatusNotFound, "Rule group not found", nil) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if len(groupsWithFolders) > 1 { | 
					
						
							|  |  |  | 		logger.Error("Multiple rule groups found when only one was expected", "folder_title", namespaceTitle, "group", group) | 
					
						
							|  |  |  | 		// It shouldn't happen, but if we get more than 1 group, we return an error.
 | 
					
						
							|  |  |  | 		return response.Error(http.StatusInternalServerError, "Multiple rule groups found", nil) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	promGroup, err := grafanaRuleGroupToPrometheus(groupsWithFolders[0].Title, groupsWithFolders[0].Rules) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to convert Grafana rule to Prometheus format", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return response.YAML(http.StatusOK, promGroup) | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | // RouteConvertPrometheusPostRuleGroup converts a Prometheus rule group into a Grafana rule group
 | 
					
						
							|  |  |  | // and creates or updates it within the specified namespace (folder).
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // If the group already exists and was not imported from a Prometheus-compatible source initially,
 | 
					
						
							|  |  |  | // it will not be replaced and an error will be returned.
 | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, promGroup apimodels.PrometheusRuleGroup) response.Response { | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 	return srv.RouteConvertPrometheusPostRuleGroups(c, map[string][]apimodels.PrometheusRuleGroup{namespaceTitle: {promGroup}}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *contextmodel.ReqContext, promNamespaces map[string][]apimodels.PrometheusRuleGroup) response.Response { | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 	// 1. Parse the appropriate headers
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	workingFolderUID := getWorkingFolderUID(c) | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 	logger = logger.New("working_folder_uid", workingFolderUID) | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 	pauseRecordingRules, err := parseBooleanHeader(c.Req.Header.Get(recordingRulesPausedHeader), recordingRulesPausedHeader) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 	pauseAlertRules, err := parseBooleanHeader(c.Req.Header.Get(alertRulesPausedHeader), alertRulesPausedHeader) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	datasourceUID := strings.TrimSpace(c.Req.Header.Get(datasourceUIDHeader)) | 
					
						
							|  |  |  | 	if datasourceUID == "" { | 
					
						
							|  |  |  | 		return response.Err(errDatasourceUIDHeaderMissing) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	ds, err := srv.datasourceCache.GetDatasourceByUID(c.Req.Context(), datasourceUID, c.SignedInUser, c.SkipDSCache) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to get datasource", "datasource_uid", datasourceUID, "error", err) | 
					
						
							| 
									
										
										
										
											2025-03-18 16:53:50 +08:00
										 |  |  | 		return errorToResponse(fmt.Errorf("failed to get datasource: %w", err)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// By default the target datasource is the same as the query datasource,
 | 
					
						
							|  |  |  | 	// but if the header "X-Grafana-Alerting-Target-Datasource-UID" is present, we use that instead.
 | 
					
						
							|  |  |  | 	tds := ds | 
					
						
							|  |  |  | 	if uid := strings.TrimSpace(c.Req.Header.Get(targetDatasourceUIDHeader)); uid != "" { | 
					
						
							|  |  |  | 		tds, err = srv.datasourceCache.GetDatasourceByUID(c.Req.Context(), uid, c.SignedInUser, c.SkipDSCache) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			logger.Error("Failed to get target datasource for recording rules", "datasource_uid", uid, "error", err) | 
					
						
							|  |  |  | 			return errorToResponse(fmt.Errorf("failed to get recording rules target datasource: %w", err)) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 	provenance := getProvenance(c) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// If the provenance is not ConvertedPrometheus, we don't keep the original rule definition.
 | 
					
						
							|  |  |  | 	// This is because the rules can be modified through the UI, which may break compatibility
 | 
					
						
							|  |  |  | 	// with the Prometheus format. We only preserve the original rule definition
 | 
					
						
							|  |  |  | 	// to ensure we can return them in this API in Prometheus format.
 | 
					
						
							|  |  |  | 	keepOriginalRuleDefinition := provenance == models.ProvenanceConvertedPrometheus | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 	notificationSettings, err := parseNotificationSettingsHeader(c) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to parse notification settings header", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 	// 2. Convert Prometheus Rules to GMA
 | 
					
						
							|  |  |  | 	grafanaGroups := make([]*models.AlertRuleGroup, 0, len(promNamespaces)) | 
					
						
							|  |  |  | 	for ns, rgs := range promNamespaces { | 
					
						
							|  |  |  | 		logger.Debug("Creating a new namespace", "title", ns) | 
					
						
							|  |  |  | 		namespace, errResp := srv.getOrCreateNamespace(c, ns, logger, workingFolderUID) | 
					
						
							|  |  |  | 		if errResp != nil { | 
					
						
							|  |  |  | 			logger.Error("Failed to create a new namespace", "folder_uid", workingFolderUID) | 
					
						
							|  |  |  | 			return errResp | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for _, rg := range rgs { | 
					
						
							|  |  |  | 			// If we're importing recording rules, we can only import them if the feature is enabled,
 | 
					
						
							|  |  |  | 			// and the feature flag that enables configuring target datasources per-rule is also enabled.
 | 
					
						
							|  |  |  | 			if promGroupHasRecordingRules(rg) { | 
					
						
							|  |  |  | 				if !srv.cfg.RecordingRules.Enabled { | 
					
						
							|  |  |  | 					logger.Error("Cannot import recording rules", "error", errRecordingRulesNotEnabled) | 
					
						
							|  |  |  | 					return errorToResponse(errRecordingRulesNotEnabled) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 			grafanaGroup, err := srv.convertToGrafanaRuleGroup( | 
					
						
							|  |  |  | 				c, | 
					
						
							|  |  |  | 				ds, | 
					
						
							|  |  |  | 				tds, | 
					
						
							|  |  |  | 				namespace.UID, | 
					
						
							|  |  |  | 				rg, | 
					
						
							|  |  |  | 				pauseRecordingRules, | 
					
						
							|  |  |  | 				pauseAlertRules, | 
					
						
							|  |  |  | 				keepOriginalRuleDefinition, | 
					
						
							|  |  |  | 				notificationSettings, | 
					
						
							|  |  |  | 				logger, | 
					
						
							|  |  |  | 			) | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err) | 
					
						
							|  |  |  | 				return errorToResponse(err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			grafanaGroups = append(grafanaGroups, grafanaGroup) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 	// 3. Update the GMA Rules in the DB
 | 
					
						
							|  |  |  | 	err = srv.alertRuleService.ReplaceRuleGroups(c.Req.Context(), c.SignedInUser, grafanaGroups, provenance) | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 		logger.Error("Failed to replace rule groups", "error", err) | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-27 20:04:47 +08:00
										 |  |  | 	return successfulResponse() | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 12:30:17 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger, workingFolderUID string) (*folder.FolderReference, response.Response) { | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	logger.Debug("Getting or creating a new folder") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	ns, err := srv.ruleStore.GetOrCreateNamespaceByTitle( | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 		c.Req.Context(), | 
					
						
							|  |  |  | 		title, | 
					
						
							| 
									
										
										
										
											2025-04-10 20:42:23 +08:00
										 |  |  | 		c.GetOrgID(), | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 		c.SignedInUser, | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 		workingFolderUID, | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to get or create a new folder", "error", err) | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 		return nil, namespaceErrorResponse(err) | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger.Debug("Using folder for the converted rules", "folder_uid", ns.UID) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return ns, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup( | 
					
						
							|  |  |  | 	c *contextmodel.ReqContext, | 
					
						
							|  |  |  | 	ds *datasources.DataSource, | 
					
						
							| 
									
										
										
										
											2025-03-18 16:53:50 +08:00
										 |  |  | 	tds *datasources.DataSource, | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 	namespaceUID string, | 
					
						
							|  |  |  | 	promGroup apimodels.PrometheusRuleGroup, | 
					
						
							| 
									
										
										
										
											2025-03-27 23:30:11 +08:00
										 |  |  | 	pauseRecordingRules bool, | 
					
						
							|  |  |  | 	pauseAlertRules bool, | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 	keepOriginalRuleDefinition bool, | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 	notificationSettings []models.NotificationSettings, | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 	logger log.Logger, | 
					
						
							|  |  |  | ) (*models.AlertRuleGroup, error) { | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	logger.Info("Converting Prometheus rules to Grafana rules", "rules", len(promGroup.Rules), "folder_uid", namespaceUID, "datasource_uid", ds.UID, "datasource_type", ds.Type) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	rules := make([]prom.PrometheusRule, len(promGroup.Rules)) | 
					
						
							|  |  |  | 	for i, r := range promGroup.Rules { | 
					
						
							|  |  |  | 		rules[i] = prom.PrometheusRule{ | 
					
						
							|  |  |  | 			Alert:         r.Alert, | 
					
						
							|  |  |  | 			Expr:          r.Expr, | 
					
						
							|  |  |  | 			For:           r.For, | 
					
						
							|  |  |  | 			KeepFiringFor: r.KeepFiringFor, | 
					
						
							|  |  |  | 			Labels:        r.Labels, | 
					
						
							|  |  |  | 			Annotations:   r.Annotations, | 
					
						
							|  |  |  | 			Record:        r.Record, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	group := prom.PrometheusRuleGroup{ | 
					
						
							| 
									
										
										
										
											2025-06-06 17:21:39 +08:00
										 |  |  | 		Name:        promGroup.Name, | 
					
						
							|  |  |  | 		Interval:    promGroup.Interval, | 
					
						
							|  |  |  | 		Rules:       rules, | 
					
						
							|  |  |  | 		QueryOffset: promGroup.QueryOffset, | 
					
						
							|  |  |  | 		Limit:       promGroup.Limit, | 
					
						
							|  |  |  | 		Labels:      promGroup.Labels, | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	converter, err := prom.NewConverter( | 
					
						
							|  |  |  | 		prom.Config{ | 
					
						
							| 
									
										
										
										
											2025-03-18 16:53:50 +08:00
										 |  |  | 			DatasourceUID:        ds.UID, | 
					
						
							|  |  |  | 			DatasourceType:       ds.Type, | 
					
						
							|  |  |  | 			TargetDatasourceUID:  tds.UID, | 
					
						
							|  |  |  | 			TargetDatasourceType: tds.Type, | 
					
						
							|  |  |  | 			DefaultInterval:      srv.cfg.DefaultRuleEvaluationInterval, | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 			RecordingRules: prom.RulesConfig{ | 
					
						
							|  |  |  | 				IsPaused: pauseRecordingRules, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			AlertRules: prom.RulesConfig{ | 
					
						
							|  |  |  | 				IsPaused: pauseAlertRules, | 
					
						
							|  |  |  | 			}, | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 			KeepOriginalRuleDefinition: util.Pointer(keepOriginalRuleDefinition), | 
					
						
							| 
									
										
										
										
											2025-03-20 22:31:21 +08:00
										 |  |  | 			EvaluationOffset:           &srv.cfg.PrometheusConversion.RuleQueryOffset, | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 			NotificationSettings:       notificationSettings, | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to create Prometheus converter", "datasource_uid", ds.UID, "datasource_type", ds.Type, "error", err) | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-10 20:42:23 +08:00
										 |  |  | 	grafanaGroup, err := converter.PrometheusRulesToGrafana(c.GetOrgID(), namespaceUID, group) | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err) | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return grafanaGroup, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-11 19:45:02 +08:00
										 |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, amCfg apimodels.AlertmanagerUserConfig) response.Response { | 
					
						
							| 
									
										
										
										
											2025-06-18 21:05:06 +08:00
										 |  |  | 	if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) { | 
					
						
							|  |  |  | 		return response.Error(http.StatusNotImplemented, "Not Implemented", nil) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	identifier, err := parseConfigIdentifierHeader(c) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to parse config identifier header", "error", err, "identifier", identifier) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	mergeMatchers, err := parseMergeMatchersHeader(c) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to parse merge matchers header", "error", err, "identifier", identifier) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	ec := apimodels.ExtraConfiguration{ | 
					
						
							|  |  |  | 		Identifier:         identifier, | 
					
						
							|  |  |  | 		MergeMatchers:      mergeMatchers, | 
					
						
							|  |  |  | 		TemplateFiles:      amCfg.TemplateFiles, | 
					
						
							|  |  |  | 		AlertmanagerConfig: amCfg.AlertmanagerConfig, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	err = ec.Validate() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Invalid alertmanager configuration", "error", err, "identifier", identifier) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	err = srv.am.SaveAndApplyExtraConfiguration(c.Req.Context(), c.GetOrgID(), ec) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to save alertmanager configuration", "error", err, "identifier", identifier) | 
					
						
							|  |  |  | 		return errorToResponse(fmt.Errorf("failed to save alertmanager configuration: %w", err)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger.Info("Successfully updated alertmanager configuration with imported Prometheus config", "identifier", identifier) | 
					
						
							|  |  |  | 	return successfulResponse() | 
					
						
							| 
									
										
										
										
											2025-06-11 19:45:02 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *contextmodel.ReqContext) response.Response { | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 	if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) { | 
					
						
							|  |  |  | 		return response.Error(http.StatusNotImplemented, "Not Implemented", nil) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							|  |  |  | 	ctx := c.Req.Context() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	identifier, err := parseConfigIdentifierHeader(c) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("failed to parse config identifier header", "err", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	cfg, err := srv.am.GetAlertmanagerConfiguration(ctx, c.GetOrgID(), false) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("failed to get alertmanager configuration", "err", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var extraCfg *apimodels.ExtraConfiguration | 
					
						
							|  |  |  | 	for i := range cfg.ExtraConfigs { | 
					
						
							|  |  |  | 		if cfg.ExtraConfigs[i].Identifier == identifier { | 
					
						
							|  |  |  | 			extraCfg = &cfg.ExtraConfigs[i] | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if extraCfg == nil { | 
					
						
							|  |  |  | 		return response.Error(http.StatusNotFound, "Alertmanager configuration not found", nil) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-18 23:01:19 +08:00
										 |  |  | 	// Parse the configuration into our Gettable struct which will automatically
 | 
					
						
							|  |  |  | 	// sanitize secrets and exclude global settings when marshaled back to YAML.
 | 
					
						
							|  |  |  | 	var prometheusConfig amconfig.Config | 
					
						
							|  |  |  | 	if err := yaml.Unmarshal([]byte(extraCfg.AlertmanagerConfig), &prometheusConfig); err != nil { | 
					
						
							|  |  |  | 		return response.Error(http.StatusBadRequest, "Invalid Alertmanager configuration format", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	respBody := apimodels.GettableAlertmanagerUserConfig{ | 
					
						
							|  |  |  | 		AlertmanagerConfig: apimodels.GettableAlertmanagerConfig{ | 
					
						
							|  |  |  | 			Config: prometheusConfig, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		TemplateFiles: extraCfg.TemplateFiles, | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	resp := response.YAML(http.StatusOK, respBody) | 
					
						
							|  |  |  | 	resp.SetHeader(configIdentifierHeader, extraCfg.Identifier) | 
					
						
							|  |  |  | 	resp.SetHeader(mergeMatchersHeader, formatMergeMatchers(extraCfg.MergeMatchers)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return resp | 
					
						
							| 
									
										
										
										
											2025-06-11 19:45:02 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteAlertmanagerConfig(c *contextmodel.ReqContext) response.Response { | 
					
						
							| 
									
										
										
										
											2025-06-18 18:37:56 +08:00
										 |  |  | 	if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) { | 
					
						
							|  |  |  | 		return response.Error(http.StatusNotImplemented, "Not Implemented", nil) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger := srv.logger.FromContext(c.Req.Context()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	identifier, err := parseConfigIdentifierHeader(c) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to parse config identifier header", "error", err) | 
					
						
							|  |  |  | 		return errorToResponse(err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	err = srv.am.DeleteExtraConfiguration(c.Req.Context(), c.GetOrgID(), identifier) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Error("Failed to delete alertmanager configuration", "error", err, "identifier", identifier) | 
					
						
							|  |  |  | 		return errorToResponse(fmt.Errorf("failed to delete alertmanager configuration: %w", err)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	logger.Info("Successfully deleted extra alertmanager configuration", "identifier", identifier) | 
					
						
							|  |  |  | 	return successfulResponse() | 
					
						
							| 
									
										
										
										
											2025-06-10 17:35:57 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | // parseBooleanHeader parses a boolean header value, returning an error if the header
 | 
					
						
							|  |  |  | // is present but invalid. If the header is not present, returns (false, nil).
 | 
					
						
							|  |  |  | func parseBooleanHeader(header string, headerName string) (bool, error) { | 
					
						
							|  |  |  | 	if header == "" { | 
					
						
							|  |  |  | 		return false, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	val, err := strconv.ParseBool(header) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 		return false, errInvalidHeaderValue(headerName, errors.New("must be 'true' or 'false'")) | 
					
						
							| 
									
										
										
										
											2025-02-25 18:26:36 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return val, nil | 
					
						
							| 
									
										
										
										
											2025-02-12 15:13:21 +08:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func grafanaNamespacesToPrometheus(groups []models.AlertRuleGroupWithFolderFullpath) (map[string][]apimodels.PrometheusRuleGroup, error) { | 
					
						
							|  |  |  | 	result := map[string][]apimodels.PrometheusRuleGroup{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, group := range groups { | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 		// Since the folder can be nested but mimirtool does not support nested paths,
 | 
					
						
							|  |  |  | 		// we need to use only the last folder in the full path.
 | 
					
						
							|  |  |  | 		// For example, if the current working folder is "general" and the full path is "grafana/some folder/general/production",
 | 
					
						
							|  |  |  | 		// we should use the "production" folder.
 | 
					
						
							|  |  |  | 		folder := filepath.Base(group.FolderFullpath) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 		promGroup, err := grafanaRuleGroupToPrometheus(group.Title, group.Rules) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 		result[folder] = append(result[folder], promGroup) | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return result, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func grafanaRuleGroupToPrometheus(group string, rules []models.AlertRule) (apimodels.PrometheusRuleGroup, error) { | 
					
						
							|  |  |  | 	if len(rules) == 0 { | 
					
						
							|  |  |  | 		return apimodels.PrometheusRuleGroup{}, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	interval := time.Duration(rules[0].IntervalSeconds) * time.Second | 
					
						
							|  |  |  | 	promGroup := apimodels.PrometheusRuleGroup{ | 
					
						
							|  |  |  | 		Name:     group, | 
					
						
							|  |  |  | 		Interval: prommodel.Duration(interval), | 
					
						
							|  |  |  | 		Rules:    make([]apimodels.PrometheusRule, len(rules)), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i, rule := range rules { | 
					
						
							| 
									
										
										
										
											2025-03-05 21:02:28 +08:00
										 |  |  | 		promDefinition, err := rule.PrometheusRuleDefinition() | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return apimodels.PrometheusRuleGroup{}, fmt.Errorf("failed to get the Prometheus definition of the rule with UID %s: %w", rule.UID, err) | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		var r apimodels.PrometheusRule | 
					
						
							|  |  |  | 		if err := yaml.Unmarshal([]byte(promDefinition), &r); err != nil { | 
					
						
							|  |  |  | 			return apimodels.PrometheusRuleGroup{}, fmt.Errorf("failed to unmarshal Prometheus rule definition of the rule with UID %s: %w", rule.UID, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		promGroup.Rules[i] = r | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return promGroup, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | func successfulResponse() response.Response { | 
					
						
							|  |  |  | 	return response.JSON(http.StatusAccepted, apimodels.ConvertPrometheusResponse{ | 
					
						
							|  |  |  | 		Status: "success", | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // getWorkingFolderUID returns the value of the folderUIDHeader
 | 
					
						
							|  |  |  | // if present. Otherwise, it returns the UID of the root folder.
 | 
					
						
							|  |  |  | func getWorkingFolderUID(c *contextmodel.ReqContext) string { | 
					
						
							|  |  |  | 	folderUID := strings.TrimSpace(c.Req.Header.Get(folderUIDHeader)) | 
					
						
							|  |  |  | 	if folderUID != "" { | 
					
						
							|  |  |  | 		return folderUID | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return folder.RootFolderUID | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | func namespaceErrorResponse(err error) response.Response { | 
					
						
							| 
									
										
										
										
											2025-03-04 00:59:01 +08:00
										 |  |  | 	if errors.Is(err, dashboards.ErrFolderNotFound) { | 
					
						
							| 
									
										
										
										
											2025-02-25 22:49:08 +08:00
										 |  |  | 		return response.Empty(http.StatusNotFound) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return toNamespaceErrorResponse(err) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-03-07 23:56:24 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func promGroupHasRecordingRules(promGroup apimodels.PrometheusRuleGroup) bool { | 
					
						
							|  |  |  | 	for _, rule := range promGroup.Rules { | 
					
						
							|  |  |  | 		if rule.Record != "" { | 
					
						
							|  |  |  | 			return true | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return false | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-03-12 02:53:28 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // getProvenance determines the provenance value to use for rules created via the Prometheus conversion API.
 | 
					
						
							|  |  |  | // If the X-Disable-Provenance header is present in the request, returns ProvenanceNone,
 | 
					
						
							|  |  |  | // otherwise returns ProvenanceConvertedPrometheus.
 | 
					
						
							|  |  |  | func getProvenance(ctx *contextmodel.ReqContext) models.Provenance { | 
					
						
							|  |  |  | 	if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled { | 
					
						
							|  |  |  | 		return models.ProvenanceNone | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return models.ProvenanceConvertedPrometheus | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-05-13 04:07:02 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func parseNotificationSettingsHeader(ctx *contextmodel.ReqContext) ([]models.NotificationSettings, error) { | 
					
						
							|  |  |  | 	var notificationSettings []models.NotificationSettings | 
					
						
							|  |  |  | 	notificationSettingsJSON := ctx.Req.Header.Get(notificationSettingsHeader) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if notificationSettingsJSON != "" { | 
					
						
							|  |  |  | 		var settings apimodels.AlertRuleNotificationSettings | 
					
						
							|  |  |  | 		var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if err := json.Unmarshal([]byte(notificationSettingsJSON), &settings); err != nil { | 
					
						
							|  |  |  | 			return nil, errInvalidHeaderValue(notificationSettingsHeader, errors.New("invalid JSON")) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		notificationSettings, err = validation.ValidateNotificationSettings(&settings) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, errInvalidHeaderValue(notificationSettingsHeader, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return notificationSettings, nil | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-06-18 16:49:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // parseMergeMatchersHeader parses the merge matchers header value.
 | 
					
						
							|  |  |  | // Expected format: "key1=value1,key2=value2"
 | 
					
						
							|  |  |  | func parseMergeMatchersHeader(c *contextmodel.ReqContext) (amconfig.Matchers, error) { | 
					
						
							|  |  |  | 	matchersStr := strings.TrimSpace(c.Req.Header.Get(mergeMatchersHeader)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if matchersStr == "" { | 
					
						
							|  |  |  | 		return amconfig.Matchers{}, errInvalidHeaderValue(mergeMatchersHeader, errors.New("value cannot be empty")) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	matchers := amconfig.Matchers{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for pair := range strings.SplitSeq(matchersStr, ",") { | 
					
						
							|  |  |  | 		parts := strings.SplitN(strings.TrimSpace(pair), "=", 2) | 
					
						
							|  |  |  | 		if len(parts) != 2 { | 
					
						
							|  |  |  | 			return nil, errInvalidHeaderValue(mergeMatchersHeader, errors.New("format should be 'key=value,key2=value2'")) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		key := strings.TrimSpace(parts[0]) | 
					
						
							|  |  |  | 		value := strings.TrimSpace(parts[1]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if key == "" || value == "" { | 
					
						
							|  |  |  | 			return nil, errInvalidHeaderValue(mergeMatchersHeader, errors.New("keys and values cannot be empty")) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		matchers = append(matchers, &labels.Matcher{ | 
					
						
							|  |  |  | 			Type:  labels.MatchEqual, | 
					
						
							|  |  |  | 			Name:  key, | 
					
						
							|  |  |  | 			Value: value, | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return matchers, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func formatMergeMatchers(matchers amconfig.Matchers) string { | 
					
						
							|  |  |  | 	var pairs []string | 
					
						
							|  |  |  | 	for _, matcher := range matchers { | 
					
						
							|  |  |  | 		if matcher.Type == labels.MatchEqual { | 
					
						
							|  |  |  | 			pairs = append(pairs, fmt.Sprintf("%s=%s", matcher.Name, matcher.Value)) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return strings.Join(pairs, ",") | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func parseConfigIdentifierHeader(c *contextmodel.ReqContext) (string, error) { | 
					
						
							|  |  |  | 	identifier := strings.TrimSpace(c.Req.Header.Get(configIdentifierHeader)) | 
					
						
							|  |  |  | 	if identifier == "" { | 
					
						
							|  |  |  | 		return "", errInvalidHeaderValue(configIdentifierHeader, errors.New("identifier cannot be empty")) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return identifier, nil | 
					
						
							|  |  |  | } |