mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Create endpoints for exporting in provisioning file format (#58623)
This adds provisioning endpoints for downloading alert rules and alert rule groups in a format that is compatible with file provisioning. Each endpoint supports both json and yaml response types via Accept header as well as a query parameter download=true/false that will set Content-Disposition to recommend initiating a download or inline display. This also makes some package changes to keep structs with potential to drift closer together. Eventually, other alerting file structs should also move into this new file package, but the rest require some refactoring that is out of scope for this PR.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package alerting
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -31,11 +31,11 @@ type AlertRuleGroupV1 struct {
|
||||
Rules []AlertRuleV1 `json:"rules" yaml:"rules"`
|
||||
}
|
||||
|
||||
func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) {
|
||||
ruleGroup := AlertRuleGroup{}
|
||||
ruleGroup.Name = ruleGroupV1.Name.Value()
|
||||
if strings.TrimSpace(ruleGroup.Name) == "" {
|
||||
return AlertRuleGroup{}, errors.New("rule group has no name set")
|
||||
func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroupWithFolderTitle, error) {
|
||||
ruleGroup := AlertRuleGroupWithFolderTitle{AlertRuleGroup: &models.AlertRuleGroup{}}
|
||||
ruleGroup.Title = ruleGroupV1.Name.Value()
|
||||
if strings.TrimSpace(ruleGroup.Title) == "" {
|
||||
return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no name set")
|
||||
}
|
||||
ruleGroup.OrgID = ruleGroupV1.OrgID.Value()
|
||||
if ruleGroup.OrgID < 1 {
|
||||
@@ -43,29 +43,27 @@ func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) {
|
||||
}
|
||||
interval, err := model.ParseDuration(ruleGroupV1.Interval.Value())
|
||||
if err != nil {
|
||||
return AlertRuleGroup{}, err
|
||||
return AlertRuleGroupWithFolderTitle{}, err
|
||||
}
|
||||
ruleGroup.Interval = time.Duration(interval)
|
||||
ruleGroup.Folder = ruleGroupV1.Folder.Value()
|
||||
if strings.TrimSpace(ruleGroup.Folder) == "" {
|
||||
return AlertRuleGroup{}, errors.New("rule group has no folder set")
|
||||
ruleGroup.Interval = int64(time.Duration(interval).Seconds())
|
||||
ruleGroup.FolderTitle = ruleGroupV1.Folder.Value()
|
||||
if strings.TrimSpace(ruleGroup.FolderTitle) == "" {
|
||||
return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no folder set")
|
||||
}
|
||||
for _, ruleV1 := range ruleGroupV1.Rules {
|
||||
rule, err := ruleV1.mapToModel(ruleGroup.OrgID)
|
||||
if err != nil {
|
||||
return AlertRuleGroup{}, err
|
||||
return AlertRuleGroupWithFolderTitle{}, err
|
||||
}
|
||||
ruleGroup.Rules = append(ruleGroup.Rules, rule)
|
||||
}
|
||||
return ruleGroup, nil
|
||||
}
|
||||
|
||||
type AlertRuleGroup struct {
|
||||
OrgID int64
|
||||
Name string
|
||||
Folder string
|
||||
Interval time.Duration
|
||||
Rules []models.AlertRule
|
||||
type AlertRuleGroupWithFolderTitle struct {
|
||||
*models.AlertRuleGroup
|
||||
OrgID int64
|
||||
FolderTitle string
|
||||
}
|
||||
|
||||
type AlertRuleV1 struct {
|
||||
@@ -175,3 +173,130 @@ func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) {
|
||||
Model: rawMessage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Response structs
|
||||
|
||||
// AlertingFileExport is the full provisioned file export.
|
||||
// swagger:model
|
||||
type AlertingFileExport struct {
|
||||
APIVersion int64 `json:"apiVersion" yaml:"apiVersion"`
|
||||
Groups []AlertRuleGroupExport `json:"groups" yaml:"groups"`
|
||||
}
|
||||
|
||||
// AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.
|
||||
type AlertRuleGroupExport struct {
|
||||
OrgID int64 `json:"orgId" yaml:"orgId"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Interval model.Duration `json:"interval" yaml:"interval"`
|
||||
Rules []AlertRuleExport `json:"rules" yaml:"rules"`
|
||||
}
|
||||
|
||||
// AlertRuleExport is the provisioned file export of models.AlertRule.
|
||||
type AlertRuleExport struct {
|
||||
UID string `json:"uid" yaml:"uid"`
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Condition string `json:"condition" yaml:"condition"`
|
||||
Data []AlertQueryExport `json:"data" yaml:"data"`
|
||||
DashboardUID string `json:"dasboardUid,omitempty" yaml:"dashboardUid,omitempty"`
|
||||
PanelID int64 `json:"panelId,omitempty" yaml:"panelId,omitempty"`
|
||||
NoDataState models.NoDataState `json:"noDataState" yaml:"noDataState"`
|
||||
ExecErrState models.ExecutionErrorState `json:"execErrState" yaml:"execErrState"`
|
||||
For model.Duration `json:"for" yaml:"for"`
|
||||
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// AlertQueryExport is the provisioned export of models.AlertQuery.
|
||||
type AlertQueryExport struct {
|
||||
RefID string `json:"refId" yaml:"refId"`
|
||||
QueryType string `json:"queryType,omitempty" yaml:"queryType,omitempty"`
|
||||
RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange,omitempty" yaml:"relativeTimeRange,omitempty"`
|
||||
DatasourceUID string `json:"datasourceUid" yaml:"datasourceUid"`
|
||||
Model map[string]interface{} `json:"model" yaml:"model"`
|
||||
}
|
||||
|
||||
// NewAlertingFileExport creates an AlertingFileExport DTO from []AlertRuleGroupWithFolderTitle.
|
||||
func NewAlertingFileExport(groups []AlertRuleGroupWithFolderTitle) (AlertingFileExport, error) {
|
||||
f := AlertingFileExport{APIVersion: 1}
|
||||
for _, group := range groups {
|
||||
export, err := newAlertRuleGroupExport(group)
|
||||
if err != nil {
|
||||
return AlertingFileExport{}, err
|
||||
}
|
||||
f.Groups = append(f.Groups, export)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// newAlertRuleGroupExport creates a AlertRuleGroupExport DTO from models.AlertRuleGroup.
|
||||
func newAlertRuleGroupExport(d AlertRuleGroupWithFolderTitle) (AlertRuleGroupExport, error) {
|
||||
rules := make([]AlertRuleExport, 0, len(d.Rules))
|
||||
for i := range d.Rules {
|
||||
alert, err := newAlertRuleExport(d.Rules[i])
|
||||
if err != nil {
|
||||
return AlertRuleGroupExport{}, err
|
||||
}
|
||||
rules = append(rules, alert)
|
||||
}
|
||||
return AlertRuleGroupExport{
|
||||
OrgID: d.OrgID,
|
||||
Name: d.Title,
|
||||
Folder: d.FolderTitle,
|
||||
Interval: model.Duration(time.Duration(d.Interval) * time.Second),
|
||||
Rules: rules,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newAlertRuleExport creates a AlertRuleExport DTO from models.AlertRule.
|
||||
func newAlertRuleExport(rule models.AlertRule) (AlertRuleExport, error) {
|
||||
data := make([]AlertQueryExport, 0, len(rule.Data))
|
||||
for i := range rule.Data {
|
||||
query, err := newAlertQueryExport(rule.Data[i])
|
||||
if err != nil {
|
||||
return AlertRuleExport{}, err
|
||||
}
|
||||
data = append(data, query)
|
||||
}
|
||||
|
||||
var dashboardUID string
|
||||
if rule.DashboardUID != nil {
|
||||
dashboardUID = *rule.DashboardUID
|
||||
}
|
||||
|
||||
var panelID int64
|
||||
if rule.PanelID != nil {
|
||||
panelID = *rule.PanelID
|
||||
}
|
||||
|
||||
return AlertRuleExport{
|
||||
UID: rule.UID,
|
||||
Title: rule.Title,
|
||||
For: model.Duration(rule.For),
|
||||
Condition: rule.Condition,
|
||||
Data: data,
|
||||
DashboardUID: dashboardUID,
|
||||
PanelID: panelID,
|
||||
NoDataState: rule.NoDataState,
|
||||
ExecErrState: rule.ExecErrState,
|
||||
Annotations: rule.Annotations,
|
||||
Labels: rule.Labels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newAlertQueryExport creates a AlertQueryExport DTO from models.AlertQuery.
|
||||
func newAlertQueryExport(query models.AlertQuery) (AlertQueryExport, error) {
|
||||
// We unmarshal the json.RawMessage model into a map in order to facilitate yaml marshalling.
|
||||
var mdl map[string]interface{}
|
||||
err := json.Unmarshal(query.Model, &mdl)
|
||||
if err != nil {
|
||||
return AlertQueryExport{}, err
|
||||
}
|
||||
return AlertQueryExport{
|
||||
RefID: query.RefID,
|
||||
QueryType: query.QueryType,
|
||||
RelativeTimeRange: query.RelativeTimeRange,
|
||||
DatasourceUID: query.DatasourceUID,
|
||||
Model: mdl,
|
||||
}, nil
|
||||
}
|
@@ -1,13 +1,14 @@
|
||||
package alerting
|
||||
package file
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/values"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/values"
|
||||
)
|
||||
|
||||
func TestRuleGroup(t *testing.T) {
|
||||
@@ -60,7 +61,7 @@ func TestRuleGroup(t *testing.T) {
|
||||
rg.Interval = interval
|
||||
rgMapped, err := rg.MapToModel()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 48*time.Hour, rgMapped.Interval)
|
||||
require.Equal(t, int64(48*time.Hour/time.Second), rgMapped.Interval)
|
||||
})
|
||||
t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) {
|
||||
rg := validRuleGroupV1(t)
|
@@ -41,24 +41,24 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
|
||||
files []*AlertingFile) error {
|
||||
for _, file := range files {
|
||||
for _, group := range file.Groups {
|
||||
folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID)
|
||||
folderUID, err := prov.getOrCreateFolderUID(ctx, group.FolderTitle, group.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prov.logger.Debug("provisioning alert rule group",
|
||||
"org", group.OrgID,
|
||||
"folder", group.Folder,
|
||||
"folder", group.FolderTitle,
|
||||
"folderUID", folderUID,
|
||||
"name", group.Name)
|
||||
"name", group.Title)
|
||||
for _, rule := range group.Rules {
|
||||
rule.NamespaceUID = folderUID
|
||||
rule.RuleGroup = group.Name
|
||||
err = prov.provisionRule(ctx, group.OrgID, rule, group.Folder, folderUID)
|
||||
rule.RuleGroup = group.Title
|
||||
err = prov.provisionRule(ctx, group.OrgID, rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Name, int64(group.Interval.Seconds()))
|
||||
err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Title, group.Interval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -77,9 +77,7 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
|
||||
func (prov *defaultAlertRuleProvisioner) provisionRule(
|
||||
ctx context.Context,
|
||||
orgID int64,
|
||||
rule alert_models.AlertRule,
|
||||
folder,
|
||||
folderUID string) error {
|
||||
rule alert_models.AlertRule) error {
|
||||
prov.logger.Debug("provisioning alert rule", "uid", rule.UID, "org", rule.OrgID)
|
||||
_, _, err := prov.ruleService.GetAlertRule(ctx, orgID, rule.UID)
|
||||
if err != nil && !errors.Is(err, alert_models.ErrAlertRuleNotFound) {
|
||||
|
@@ -3,6 +3,7 @@ package alerting
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/alerting/file"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/values"
|
||||
)
|
||||
|
||||
@@ -15,8 +16,8 @@ type OrgID int64
|
||||
type AlertingFile struct {
|
||||
configVersion
|
||||
Filename string
|
||||
Groups []AlertRuleGroup
|
||||
DeleteRules []RuleDelete
|
||||
Groups []file.AlertRuleGroupWithFolderTitle
|
||||
DeleteRules []file.RuleDelete
|
||||
ContactPoints []ContactPoint
|
||||
DeleteContactPoints []DeleteContactPoint
|
||||
Policies []NotificiationPolicy
|
||||
@@ -30,8 +31,8 @@ type AlertingFile struct {
|
||||
type AlertingFileV1 struct {
|
||||
configVersion
|
||||
Filename string
|
||||
Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"`
|
||||
DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
|
||||
Groups []file.AlertRuleGroupV1 `json:"groups" yaml:"groups"`
|
||||
DeleteRules []file.RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
|
||||
ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"`
|
||||
DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"`
|
||||
Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"`
|
||||
@@ -132,7 +133,7 @@ func (fileV1 *AlertingFileV1) mapRules(alertingFile *AlertingFile) error {
|
||||
if orgID < 1 {
|
||||
orgID = 1
|
||||
}
|
||||
ruleDelete := RuleDelete{
|
||||
ruleDelete := file.RuleDelete{
|
||||
UID: ruleDeleteV1.UID.Value(),
|
||||
OrgID: orgID,
|
||||
}
|
||||
|
@@ -269,6 +269,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
|
||||
ruleService := provisioning.NewAlertRuleService(
|
||||
st,
|
||||
st,
|
||||
ps.dashboardService,
|
||||
ps.quotaService,
|
||||
ps.SQLStore,
|
||||
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
|
||||
|
Reference in New Issue
Block a user