Alerting: Add file provisioning for alert rules (#51635)

This commit is contained in:
Jean-Philippe Quéméner
2022-07-14 23:53:13 +02:00
committed by GitHub
parent e5e8747ee9
commit 41790083d2
22 changed files with 1427 additions and 10 deletions

View File

@ -31,6 +31,7 @@ var (
ScopeProvisionersPlugins = ac.Scope("provisioners", "plugins") ScopeProvisionersPlugins = ac.Scope("provisioners", "plugins")
ScopeProvisionersDatasources = ac.Scope("provisioners", "datasources") ScopeProvisionersDatasources = ac.Scope("provisioners", "datasources")
ScopeProvisionersNotifications = ac.Scope("provisioners", "notifications") ScopeProvisionersNotifications = ac.Scope("provisioners", "notifications")
ScopeProvisionersAlertRules = ac.Scope("provisioners", "alerting")
) )
// declareFixedRoles declares to the AccessControl service fixed roles and their // declareFixedRoles declares to the AccessControl service fixed roles and their

View File

@ -39,3 +39,11 @@ func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext)
} }
return response.Success("Notifications config reloaded") return response.Success("Notifications config reloaded")
} }
func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *models.ReqContext) response.Response {
err := hs.ProvisioningService.ProvisionAlertRules(c.Req.Context())
if err != nil {
return response.Error(500, "", err)
}
return response.Success("Alerting config reloaded")
}

View File

@ -135,6 +135,33 @@ func TestAPI_AdminProvisioningReload_AccessControl(t *testing.T) {
url: "/api/admin/provisioning/plugins/reload", url: "/api/admin/provisioning/plugins/reload",
exit: true, exit: true,
}, },
{
desc: "should fail for alerting with no permission",
expectedCode: http.StatusForbidden,
url: "/api/admin/provisioning/alerting/reload",
exit: true,
},
{
desc: "should work for alert rules with specific scope",
expectedCode: http.StatusOK,
expectedBody: `{"message":"Alerting config reloaded"}`,
permissions: []accesscontrol.Permission{
{
Action: ActionProvisioningReload,
Scope: ScopeProvisionersAlertRules,
},
},
url: "/api/admin/provisioning/alerting/reload",
checkCall: func(mock provisioning.ProvisioningServiceMock) {
assert.Len(t, mock.Calls.ProvisionAlertRules, 1)
},
},
{
desc: "should fail for alerting with no permission",
expectedCode: http.StatusForbidden,
url: "/api/admin/provisioning/alerting/reload",
exit: true,
},
} }
cfg := setting.NewCfg() cfg := setting.NewCfg()

View File

@ -576,6 +576,7 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins)) adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDatasources)), routing.Wrap(hs.AdminProvisioningReloadDatasources)) adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDatasources)), routing.Wrap(hs.AdminProvisioningReloadDatasources))
adminRoute.Post("/provisioning/notifications/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersNotifications)), routing.Wrap(hs.AdminProvisioningReloadNotifications)) adminRoute.Post("/provisioning/notifications/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersNotifications)), routing.Wrap(hs.AdminProvisioningReloadNotifications))
adminRoute.Post("/provisioning/alerting/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersAlertRules)), routing.Wrap(hs.AdminProvisioningReloadAlerting))
adminRoute.Post("/ldap/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPConfigReload)), routing.Wrap(hs.ReloadLDAPCfg)) adminRoute.Post("/ldap/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPConfigReload)), routing.Wrap(hs.ReloadLDAPCfg))
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersSync)), routing.Wrap(hs.PostSyncUserWithLDAP)) adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersSync)), routing.Wrap(hs.PostSyncUserWithLDAP))

View File

@ -133,8 +133,8 @@ func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64,
} }
// UpdateRuleGroup will update the interval for all rules in the group. // UpdateRuleGroup will update the interval for all rules in the group.
func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error { func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, intervalSeconds int64) error {
if err := models.ValidateRuleGroupInterval(interval, service.baseIntervalSeconds); err != nil { if err := models.ValidateRuleGroupInterval(intervalSeconds, service.baseIntervalSeconds); err != nil {
return err return err
} }
return service.xact.InTransaction(ctx, func(ctx context.Context) error { return service.xact.InTransaction(ctx, func(ctx context.Context) error {
@ -149,11 +149,11 @@ func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int6
} }
updateRules := make([]store.UpdateRule, 0, len(query.Result)) updateRules := make([]store.UpdateRule, 0, len(query.Result))
for _, rule := range query.Result { for _, rule := range query.Result {
if rule.IntervalSeconds == interval { if rule.IntervalSeconds == intervalSeconds {
continue continue
} }
newRule := *rule newRule := *rule
newRule.IntervalSeconds = interval newRule.IntervalSeconds = intervalSeconds
updateRules = append(updateRules, store.UpdateRule{ updateRules = append(updateRules, store.UpdateRule{
Existing: rule, Existing: rule,
New: newRule, New: newRule,
@ -180,7 +180,6 @@ func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule model
if err != nil { if err != nil {
return models.AlertRule{}, err return models.AlertRule{}, err
} }
service.log.Info("update rule", "ID", storedRule.ID, "labels", fmt.Sprintf("%+v", rule.Labels))
err = service.xact.InTransaction(ctx, func(ctx context.Context) error { err = service.xact.InTransaction(ctx, func(ctx context.Context) error {
err := service.ruleStore.UpdateAlertRules(ctx, []store.UpdateRule{ err := service.ruleStore.UpdateAlertRules(ctx, []store.UpdateRule{
{ {

View File

@ -0,0 +1,77 @@
package rules
import (
"context"
"fmt"
"io/fs"
"io/ioutil"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"gopkg.in/yaml.v2"
)
type rulesConfigReader struct {
log log.Logger
}
func newRulesConfigReader(logger log.Logger) rulesConfigReader {
return rulesConfigReader{
log: logger,
}
}
func (cr *rulesConfigReader) readConfig(ctx context.Context, path string) ([]*RuleFile, error) {
var alertRulesFiles []*RuleFile
cr.log.Debug("looking for alert rules provisioning files", "path", path)
files, err := ioutil.ReadDir(path)
if err != nil {
cr.log.Error("can't read alert rules provisioning files from directory", "path", path, "error", err)
return alertRulesFiles, nil
}
for _, file := range files {
cr.log.Debug("parsing alert rules provisioning file", "path", path, "file.Name", file.Name())
if !cr.isYAML(file.Name()) && !cr.isJSON(file.Name()) {
return nil, fmt.Errorf("file has invalid suffix '%s' (.yaml,.yml,.json accepted)", file.Name())
}
ruleFileV1, err := cr.parseConfig(path, file)
if err != nil {
return nil, err
}
if ruleFileV1 != nil {
ruleFile, err := ruleFileV1.MapToModel()
if err != nil {
return nil, err
}
alertRulesFiles = append(alertRulesFiles, &ruleFile)
}
}
return alertRulesFiles, nil
}
func (cr *rulesConfigReader) isYAML(file string) bool {
return strings.HasSuffix(file, ".yaml") || strings.HasSuffix(file, ".yml")
}
func (cr *rulesConfigReader) isJSON(file string) bool {
return strings.HasSuffix(file, ".json")
}
func (cr *rulesConfigReader) parseConfig(path string, file fs.FileInfo) (*RuleFileV1, error) {
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var cfg *RuleFileV1
err = yaml.Unmarshal(yamlFile, &cfg)
if err != nil {
return nil, err
}
return cfg, nil
}

View File

@ -0,0 +1,66 @@
package rules
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/stretchr/testify/require"
)
const (
testFileBrokenYAML = "./testdata/broken-yaml"
testFileCorrectProperties = "./testdata/correct-properties"
testFileCorrectPropertiesWithOrg = "./testdata/correct-properties-with-org"
testFileEmptyFile = "./testdata/empty-file"
testFileEmptyFolder = "./testdata/empty-folder"
testFileMultipleRules = "./testdata/multiple-rules"
testFileMultipleFiles = "./testdata/multiple-files"
testFileSupportedFiletypes = "./testdata/supported-filetypes"
)
func TestConfigReader(t *testing.T) {
configReader := newRulesConfigReader(log.NewNopLogger())
ctx := context.Background()
t.Run("a broken YAML file should error", func(t *testing.T) {
_, err := configReader.readConfig(ctx, testFileBrokenYAML)
require.Error(t, err)
})
t.Run("a rule file with correct properties should not error", func(t *testing.T) {
ruleFiles, err := configReader.readConfig(ctx, testFileCorrectProperties)
require.NoError(t, err)
t.Run("when no organization is present it should be set to 1", func(t *testing.T) {
require.Equal(t, int64(1), ruleFiles[0].Groups[0].Rules[0].OrgID)
})
})
t.Run("a rule file with correct properties and specific org should not error", func(t *testing.T) {
ruleFiles, err := configReader.readConfig(ctx, testFileCorrectPropertiesWithOrg)
require.NoError(t, err)
t.Run("when an organization is set it should not overwrite if with the default of 1", func(t *testing.T) {
require.Equal(t, int64(1337), ruleFiles[0].Groups[0].Rules[0].OrgID)
})
})
t.Run("an empty rule file should not make the config reader error", func(t *testing.T) {
_, err := configReader.readConfig(ctx, testFileEmptyFile)
require.NoError(t, err)
})
t.Run("an empty folder should not make the config reader error", func(t *testing.T) {
_, err := configReader.readConfig(ctx, testFileEmptyFolder)
require.NoError(t, err)
})
t.Run("the config reader should be able to read multiple files in the folder", func(t *testing.T) {
ruleFiles, err := configReader.readConfig(ctx, testFileMultipleFiles)
require.NoError(t, err)
require.Len(t, ruleFiles, 2)
})
t.Run("the config reader should be able to read multiple rule groups", func(t *testing.T) {
ruleFiles, err := configReader.readConfig(ctx, testFileMultipleRules)
require.NoError(t, err)
require.Len(t, ruleFiles[0].Groups, 2)
})
t.Run("the config reader should support .yaml,.yml and .json files", func(t *testing.T) {
ruleFiles, err := configReader.readConfig(ctx, testFileSupportedFiletypes)
require.NoError(t, err)
require.Len(t, ruleFiles, 3)
})
}

View File

@ -0,0 +1,165 @@
package rules
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
alert_models "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/util"
)
type AlertRuleProvisioner interface {
Provision(ctx context.Context, path string) error
}
func NewAlertRuleProvisioner(
logger log.Logger,
dashboardService dashboards.DashboardService,
dashboardProvService dashboards.DashboardProvisioningService,
ruleService provisioning.AlertRuleService) AlertRuleProvisioner {
return &defaultAlertRuleProvisioner{
logger: logger,
cfgReader: newRulesConfigReader(logger),
dashboardService: dashboardService,
dashboardProvService: dashboardProvService,
ruleService: ruleService,
}
}
type defaultAlertRuleProvisioner struct {
logger log.Logger
cfgReader rulesConfigReader
dashboardService dashboards.DashboardService
dashboardProvService dashboards.DashboardProvisioningService
ruleService provisioning.AlertRuleService
}
func Provision(
ctx context.Context,
path string,
dashboardService dashboards.DashboardService,
dashboardProvisioningService dashboards.DashboardProvisioningService,
ruleService provisioning.AlertRuleService,
) error {
ruleProvisioner := NewAlertRuleProvisioner(
log.New("provisioning.alerting"),
dashboardService,
dashboardProvisioningService,
ruleService,
)
return ruleProvisioner.Provision(ctx, path)
}
func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
path string) error {
prov.logger.Info("starting to provision the alert rules")
ruleFiles, err := prov.cfgReader.readConfig(ctx, path)
if err != nil {
return fmt.Errorf("failed to read alert rules files: %w", err)
}
prov.logger.Debug("read all alert rules files", "file_count", len(ruleFiles))
err = prov.provsionRuleFiles(ctx, ruleFiles)
if err != nil {
return fmt.Errorf("failed to provision alert rules: %w", err)
}
prov.logger.Info("finished to provision the alert rules")
return nil
}
func (prov *defaultAlertRuleProvisioner) provsionRuleFiles(ctx context.Context,
ruleFiles []*RuleFile) error {
for _, file := range ruleFiles {
for _, group := range file.Groups {
folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID)
if err != nil {
return err
}
prov.logger.Debug("provisioning alert rule group",
"org", group.OrgID,
"folder", group.Folder,
"folderUID", folderUID,
"name", group.Name)
for _, rule := range group.Rules {
rule.NamespaceUID = folderUID
rule.RuleGroup = group.Name
err = prov.provisionRule(ctx, group.OrgID, rule, group.Folder, folderUID)
if err != nil {
return err
}
}
err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Name, int64(group.Interval.Seconds()))
if err != nil {
return err
}
}
for _, deleteRule := range file.DeleteRules {
err := prov.ruleService.DeleteAlertRule(ctx, deleteRule.OrgID,
deleteRule.UID, alert_models.ProvenanceFile)
if err != nil {
return err
}
}
}
return nil
}
func (prov *defaultAlertRuleProvisioner) provisionRule(
ctx context.Context,
orgID int64,
rule alert_models.AlertRule,
folder,
folderUID string) 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) {
return err
} else if err != nil {
prov.logger.Debug("creating rule", "uid", rule.UID, "org", rule.OrgID)
// 0 is passed as userID as then the quota logic will only check for
// the organization quota, as we don't have any user scope here.
_, err = prov.ruleService.CreateAlertRule(ctx, rule, alert_models.ProvenanceFile, 0)
} else {
prov.logger.Debug("updating rule", "uid", rule.UID, "org", rule.OrgID)
_, err = prov.ruleService.UpdateAlertRule(ctx, rule, alert_models.ProvenanceFile)
}
return err
}
func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID(
ctx context.Context, folderName string, orgID int64) (string, error) {
cmd := &models.GetDashboardQuery{
Slug: models.SlugifyTitle(folderName),
OrgId: orgID,
}
err := prov.dashboardService.GetDashboard(ctx, cmd)
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {
return "", err
}
// dashboard folder not found. create one.
if errors.Is(err, dashboards.ErrDashboardNotFound) {
dash := &dashboards.SaveDashboardDTO{}
dash.Dashboard = models.NewDashboardFolder(folderName)
dash.Dashboard.IsFolder = true
dash.Overwrite = true
dash.OrgId = orgID
dash.Dashboard.SetUid(util.GenerateShortUID())
dbDash, err := prov.dashboardProvService.SaveFolderForProvisionedDashboards(ctx, dash)
if err != nil {
return "", err
}
return dbDash.Uid, nil
}
if !cmd.Result.IsFolder {
return "", fmt.Errorf("got invalid response. expected folder, found dashboard")
}
return cmd.Result.Uid, nil
}

View File

@ -0,0 +1,56 @@
apiVersion: 1
groups:
- name: my_group
folder: my_folder
interval: 10s
rules:
- title: my_first_rule
uid: my_first_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions

View File

@ -0,0 +1,57 @@
apiVersion: 1
groups:
- name: my_group
folder: my_folder
interval: 10s
orgId: 1337
rules:
- title: my_first_rule
uid: my_first_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions

View File

@ -0,0 +1,56 @@
apiVersion: 1
groups:
- name: my_group
folder: my_folder
interval: 10s
rules:
- title: my_first_rule
uid: my_first_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions

View File

@ -0,0 +1,56 @@
apiVersion: 1
groups:
- name: my_group
folder: my_folder
interval: 10s
rules:
- title: my_first_rule
uid: my_first_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions

View File

@ -0,0 +1,56 @@
apiVersion: 1
groups:
- name: my_other_group
folder: my_other_folder
interval: 10s
rules:
- title: my_other_rule
uid: my_other_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions

View File

@ -0,0 +1,110 @@
apiVersion: 1
groups:
- name: my_group
folder: my_folder
interval: 10s
rules:
- title: my_first_rule
uid: my_first_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions
- name: my_other_group
folder: my_other_folder
interval: 10s
rules:
- title: my_other_rule
uid: my_other_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions

View File

@ -0,0 +1,87 @@
{
"apiVersion": 1,
"groups": [
{
"name": "my_json_group",
"folder": "my_json_folder",
"interval": "10s",
"rules": [
{
"title": "my_json_rule",
"uid": "my_json_rule",
"condition": "A",
"for": "1m",
"annotations": {
"runbook": "http://google.com/"
},
"labels": {
"team": "infra",
"severity": "warning"
},
"data": [
{
"refId": "A",
"queryType": "",
"relativeTimeRange": {
"from": 600,
"to": 0
},
"datasourceUID": "PD8C576611E62080A",
"model": {
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "A"
}
},
{
"refId": "B",
"queryType": "",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUID": "-100",
"model": {
"conditions": [
{
"evaluator": {
"params": [
3
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A"
]
},
"reducer": {
"params": [
],
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "-100"
},
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "B",
"type": "classic_conditions"
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,56 @@
apiVersion: 1
groups:
- name: my_group
folder: my_folder
interval: 10s
rules:
- title: my_first_rule
uid: my_first_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions

View File

@ -0,0 +1,56 @@
apiVersion: 1
groups:
- name: my_other_group
folder: my_other_folder
interval: 10s
rules:
- title: my_other_rule
uid: my_other_rule
condition: A
for: 1m
annotations:
runbook: https://grafana.com
labels:
team: infra
severity: warning
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUID: PD8C576611E62080A
model:
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: A
- refId: B
queryType: ''
relativeTimeRange:
from: 0
to: 0
datasourceUID: "-100"
model:
conditions:
- evaluator:
params:
- 3
type: gt
operator:
type: and
query:
params:
- A
reducer:
params: []
type: last
type: query
datasource:
type: __expr__
uid: "-100"
hide: false
intervalMs: 1000
maxDataPoints: 43200
refId: B
type: classic_conditions

View File

@ -0,0 +1,210 @@
package rules
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
type configVersion struct {
APIVersion values.Int64Value `json:"apiVersion" yaml:"apiVersion"`
}
type RuleFile struct {
configVersion
Groups []AlertRuleGroup
DeleteRules []RuleDelete
}
type RuleFileV1 struct {
configVersion
Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"`
DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
}
func (ruleFileV1 *RuleFileV1) MapToModel() (RuleFile, error) {
ruleFile := RuleFile{}
ruleFile.configVersion = ruleFileV1.configVersion
for _, groupV1 := range ruleFileV1.Groups {
group, err := groupV1.mapToModel()
if err != nil {
return RuleFile{}, err
}
ruleFile.Groups = append(ruleFile.Groups, group)
}
for _, ruleDeleteV1 := range ruleFileV1.DeleteRules {
orgID := ruleDeleteV1.OrgID.Value()
if orgID < 1 {
orgID = 1
}
ruleDelete := RuleDelete{
UID: ruleDeleteV1.UID.Value(),
OrgID: orgID,
}
ruleFile.DeleteRules = append(ruleFile.DeleteRules, ruleDelete)
}
return ruleFile, nil
}
type RuleDelete struct {
UID string
OrgID int64
}
type RuleDeleteV1 struct {
UID values.StringValue `json:"uid" yaml:"uid"`
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
}
type AlertRuleGroupV1 struct {
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
Name values.StringValue `json:"name" yaml:"name"`
Folder values.StringValue `json:"folder" yaml:"folder"`
Interval values.StringValue `json:"interval" yaml:"interval"`
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")
}
ruleGroup.OrgID = ruleGroupV1.OrgID.Value()
if ruleGroup.OrgID < 1 {
ruleGroup.OrgID = 1
}
interval, err := time.ParseDuration(ruleGroupV1.Interval.Value())
if err != nil {
return AlertRuleGroup{}, err
}
ruleGroup.Interval = interval
ruleGroup.Folder = ruleGroupV1.Folder.Value()
if strings.TrimSpace(ruleGroup.Folder) == "" {
return AlertRuleGroup{}, 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
}
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 AlertRuleV1 struct {
UID values.StringValue `json:"uid" yaml:"uid"`
Title values.StringValue `json:"title" yaml:"title"`
Condition values.StringValue `json:"condition" yaml:"condition"`
Data []QueryV1 `json:"data" yaml:"data"`
DashboardUID values.StringValue `json:"dasboardUid" yaml:"dashboardUid"`
PanelID values.Int64Value `json:"panelId" yaml:"panelId"`
NoDataState values.StringValue `json:"noDataState" yaml:"noDataState"`
ExecErrState values.StringValue `json:"execErrState" yaml:"execErrState"`
For values.StringValue `json:"for" yaml:"for"`
Annotations values.StringMapValue `json:"annotations" yaml:"annotations"`
Labels values.StringMapValue `json:"labels" yaml:"labels"`
}
func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) {
alertRule := models.AlertRule{}
alertRule.Title = rule.Title.Value()
if alertRule.Title == "" {
return models.AlertRule{}, fmt.Errorf("rule has no title set")
}
alertRule.UID = rule.UID.Value()
if alertRule.UID == "" {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no UID set", alertRule.Title)
}
alertRule.OrgID = orgID
duration, err := time.ParseDuration(rule.For.Value())
if err != nil {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
}
alertRule.For = duration
dashboardUID := rule.DashboardUID.Value()
alertRule.DashboardUID = &dashboardUID
panelID := rule.PanelID.Value()
alertRule.PanelID = &panelID
execErrStateValue := strings.TrimSpace(rule.ExecErrState.Value())
execErrState, err := models.ErrStateFromString(execErrStateValue)
if err != nil && execErrStateValue != "" {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
}
if execErrStateValue == "" {
execErrState = models.AlertingErrState
}
alertRule.ExecErrState = execErrState
noDataStateValue := strings.TrimSpace(rule.NoDataState.Value())
noDataState, err := models.NoDataStateFromString(noDataStateValue)
if err != nil && noDataStateValue != "" {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
}
if noDataStateValue == "" {
noDataState = models.NoData
}
alertRule.NoDataState = noDataState
alertRule.Condition = rule.Condition.Value()
if alertRule.Condition == "" {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no condition set", alertRule.Title)
}
alertRule.Annotations = rule.Annotations.Value()
alertRule.Labels = rule.Labels.Value()
for _, queryV1 := range rule.Data {
query, err := queryV1.mapToModel()
if err != nil {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
}
alertRule.Data = append(alertRule.Data, query)
}
if len(alertRule.Data) == 0 {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no data set", alertRule.Title)
}
return alertRule, nil
}
type QueryV1 struct {
RefID values.StringValue `json:"refId" yaml:"refId"`
QueryType values.StringValue `json:"queryType" yaml:"queryType"`
RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange" yaml:"relativeTimeRange"`
DatasourceUID values.StringValue `json:"datasourceUid" yaml:"datasourceUid"`
Model values.JSONValue `json:"model" yaml:"model"`
}
func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) {
// In order to get the model into the format we need,
// we marshal it back to json and unmarshal it again
// in json.RawMessage. We do this as we cannot use
// json.RawMessage with a yaml files and have to use
// JSONValue that supports both, json and yaml.
encoded, err := json.Marshal(queryV1.Model.Value())
if err != nil {
return models.AlertQuery{}, err
}
var rawMessage json.RawMessage
err = json.Unmarshal(encoded, &rawMessage)
if err != nil {
return models.AlertQuery{}, err
}
return models.AlertQuery{
RefID: queryV1.RefID.Value(),
QueryType: queryV1.QueryType.Value(),
DatasourceUID: queryV1.DatasourceUID.Value(),
RelativeTimeRange: queryV1.RelativeTimeRange,
Model: rawMessage,
}, nil
}

View File

@ -0,0 +1,218 @@
package rules
import (
"testing"
"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"
)
func TestRuleGroup(t *testing.T) {
t.Run("a valid rule group should not error", func(t *testing.T) {
rg := validRuleGroupV1(t)
_, err := rg.mapToModel()
require.NoError(t, err)
})
t.Run("a rule group with out a name should error", func(t *testing.T) {
rg := validRuleGroupV1(t)
var name values.StringValue
err := yaml.Unmarshal([]byte(""), &name)
require.NoError(t, err)
rg.Name = name
_, err = rg.mapToModel()
require.Error(t, err)
})
t.Run("a rule group with out a folder should error", func(t *testing.T) {
rg := validRuleGroupV1(t)
var folder values.StringValue
err := yaml.Unmarshal([]byte(""), &folder)
require.NoError(t, err)
rg.Folder = folder
_, err = rg.mapToModel()
require.Error(t, err)
})
t.Run("a rule group with out an interval should error", func(t *testing.T) {
rg := validRuleGroupV1(t)
var interval values.StringValue
err := yaml.Unmarshal([]byte(""), &interval)
require.NoError(t, err)
rg.Interval = interval
_, err = rg.mapToModel()
require.Error(t, err)
})
t.Run("a rule group with an invalid interval should error", func(t *testing.T) {
rg := validRuleGroupV1(t)
var interval values.StringValue
err := yaml.Unmarshal([]byte("10x"), &interval)
require.NoError(t, err)
rg.Interval = interval
_, err = rg.mapToModel()
require.Error(t, err)
})
t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) {
rg := validRuleGroupV1(t)
rg.OrgID = values.Int64Value{}
rgMapped, err := rg.mapToModel()
require.NoError(t, err)
require.Equal(t, int64(1), rgMapped.OrgID)
})
t.Run("a rule group with a negative org id should default to 1", func(t *testing.T) {
rg := validRuleGroupV1(t)
orgID := values.Int64Value{}
err := yaml.Unmarshal([]byte("-1"), &orgID)
require.NoError(t, err)
rg.OrgID = orgID
rgMapped, err := rg.mapToModel()
require.NoError(t, err)
require.Equal(t, int64(1), rgMapped.OrgID)
})
}
func TestRules(t *testing.T) {
t.Run("a valid rule should not error", func(t *testing.T) {
rule := validRuleV1(t)
_, err := rule.mapToModel(1)
require.NoError(t, err)
})
t.Run("a rule with out a uid should error", func(t *testing.T) {
rule := validRuleV1(t)
rule.UID = values.StringValue{}
_, err := rule.mapToModel(1)
require.Error(t, err)
})
t.Run("a rule with out a title should error", func(t *testing.T) {
rule := validRuleV1(t)
rule.Title = values.StringValue{}
_, err := rule.mapToModel(1)
require.Error(t, err)
})
t.Run("a rule with out a for duration should error", func(t *testing.T) {
rule := validRuleV1(t)
rule.For = values.StringValue{}
_, err := rule.mapToModel(1)
require.Error(t, err)
})
t.Run("a rule with an invalid for duration should error", func(t *testing.T) {
rule := validRuleV1(t)
forDuration := values.StringValue{}
err := yaml.Unmarshal([]byte("10x"), &forDuration)
rule.For = forDuration
require.NoError(t, err)
_, err = rule.mapToModel(1)
require.Error(t, err)
})
t.Run("a rule with out a condition should error", func(t *testing.T) {
rule := validRuleV1(t)
rule.Condition = values.StringValue{}
_, err := rule.mapToModel(1)
require.Error(t, err)
})
t.Run("a rule with out data should error", func(t *testing.T) {
rule := validRuleV1(t)
rule.Data = []QueryV1{}
_, err := rule.mapToModel(1)
require.Error(t, err)
})
t.Run("a rule with out execErrState should have sane defaults", func(t *testing.T) {
rule := validRuleV1(t)
ruleMapped, err := rule.mapToModel(1)
require.NoError(t, err)
require.Equal(t, ruleMapped.ExecErrState, models.AlertingErrState)
})
t.Run("a rule with invalid execErrState should error", func(t *testing.T) {
rule := validRuleV1(t)
execErrState := values.StringValue{}
err := yaml.Unmarshal([]byte("abc"), &execErrState)
require.NoError(t, err)
rule.ExecErrState = execErrState
_, err = rule.mapToModel(1)
require.Error(t, err)
})
t.Run("a rule with a valid execErrState should map it correctly", func(t *testing.T) {
rule := validRuleV1(t)
execErrState := values.StringValue{}
err := yaml.Unmarshal([]byte(models.OkErrState), &execErrState)
require.NoError(t, err)
rule.ExecErrState = execErrState
ruleMapped, err := rule.mapToModel(1)
require.NoError(t, err)
require.Equal(t, ruleMapped.ExecErrState, models.OkErrState)
})
t.Run("a rule with out noDataState should have sane defaults", func(t *testing.T) {
rule := validRuleV1(t)
ruleMapped, err := rule.mapToModel(1)
require.NoError(t, err)
require.Equal(t, ruleMapped.NoDataState, models.NoData)
})
t.Run("a rule with an invalid noDataState should error", func(t *testing.T) {
rule := validRuleV1(t)
noDataState := values.StringValue{}
err := yaml.Unmarshal([]byte("abc"), &noDataState)
require.NoError(t, err)
rule.NoDataState = noDataState
_, err = rule.mapToModel(1)
require.Error(t, err)
})
t.Run("a rule with a valid noDataState should map it correctly", func(t *testing.T) {
rule := validRuleV1(t)
noDataState := values.StringValue{}
err := yaml.Unmarshal([]byte(models.NoData), &noDataState)
require.NoError(t, err)
rule.NoDataState = noDataState
ruleMapped, err := rule.mapToModel(1)
require.NoError(t, err)
require.Equal(t, ruleMapped.NoDataState, models.NoData)
})
}
func validRuleGroupV1(t *testing.T) AlertRuleGroupV1 {
t.Helper()
var (
orgID values.Int64Value
name values.StringValue
folder values.StringValue
interval values.StringValue
)
err := yaml.Unmarshal([]byte("1"), &orgID)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("Test"), &name)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("Test"), &folder)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("10s"), &interval)
require.NoError(t, err)
return AlertRuleGroupV1{
OrgID: orgID,
Name: name,
Folder: folder,
Interval: interval,
Rules: []AlertRuleV1{},
}
}
func validRuleV1(t *testing.T) AlertRuleV1 {
t.Helper()
var (
title values.StringValue
uid values.StringValue
forDuration values.StringValue
condition values.StringValue
)
err := yaml.Unmarshal([]byte("test"), &title)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("test_uid"), &uid)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("10s"), &forDuration)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("A"), &condition)
require.NoError(t, err)
return AlertRuleV1{
Title: title,
UID: uid,
For: forDuration,
Condition: condition,
Data: []QueryV1{{}},
}
}

View File

@ -9,37 +9,50 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
plugifaces "github.com/grafana/grafana/pkg/plugins" plugifaces "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources" datasourceservice "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsettings"
"github.com/grafana/grafana/pkg/services/provisioning/alerting/rules"
"github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/datasources"
"github.com/grafana/grafana/pkg/services/provisioning/notifiers" "github.com/grafana/grafana/pkg/services/provisioning/notifiers"
"github.com/grafana/grafana/pkg/services/provisioning/plugins" "github.com/grafana/grafana/pkg/services/provisioning/plugins"
"github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/services/provisioning/utils"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugifaces.Store, func ProvideService(
encryptionService encryption.Internal, notificatonService *notifications.NotificationService, ac accesscontrol.AccessControl,
cfg *setting.Cfg,
sqlStore *sqlstore.SQLStore,
pluginStore plugifaces.Store,
encryptionService encryption.Internal,
notificatonService *notifications.NotificationService,
dashboardProvisioningService dashboardservice.DashboardProvisioningService, dashboardProvisioningService dashboardservice.DashboardProvisioningService,
datasourceService datasourceservice.DataSourceService, datasourceService datasourceservice.DataSourceService,
dashboardService dashboardservice.DashboardService, dashboardService dashboardservice.DashboardService,
alertingService *alerting.AlertNotificationService, pluginSettings pluginsettings.Service, folderService dashboardservice.FolderService,
alertingService *alerting.AlertNotificationService,
pluginSettings pluginsettings.Service,
searchService searchV2.SearchService, searchService searchV2.SearchService,
quotaService *quota.QuotaService,
) (*ProvisioningServiceImpl, error) { ) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{ s := &ProvisioningServiceImpl{
Cfg: cfg, Cfg: cfg,
SQLStore: sqlStore, SQLStore: sqlStore,
ac: ac,
pluginStore: pluginStore, pluginStore: pluginStore,
EncryptionService: encryptionService, EncryptionService: encryptionService,
NotificationService: notificatonService, NotificationService: notificatonService,
log: log.New("provisioning"),
newDashboardProvisioner: dashboards.New, newDashboardProvisioner: dashboards.New,
provisionNotifiers: notifiers.Provision, provisionNotifiers: notifiers.Provision,
provisionDatasources: datasources.Provision, provisionDatasources: datasources.Provision,
@ -50,6 +63,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p
alertingService: alertingService, alertingService: alertingService,
pluginsSettings: pluginSettings, pluginsSettings: pluginSettings,
searchService: searchService, searchService: searchService,
quotaService: quotaService,
log: log.New("provisioning"),
} }
return s, nil return s, nil
} }
@ -61,18 +76,21 @@ type ProvisioningService interface {
ProvisionPlugins(ctx context.Context) error ProvisionPlugins(ctx context.Context) error
ProvisionNotifications(ctx context.Context) error ProvisionNotifications(ctx context.Context) error
ProvisionDashboards(ctx context.Context) error ProvisionDashboards(ctx context.Context) error
ProvisionAlertRules(ctx context.Context) error
GetDashboardProvisionerResolvedPath(name string) string GetDashboardProvisionerResolvedPath(name string) string
GetAllowUIUpdatesFromConfig(name string) bool GetAllowUIUpdatesFromConfig(name string) bool
} }
// Add a public constructor for overriding service to be able to instantiate OSS as fallback // Add a public constructor for overriding service to be able to instantiate OSS as fallback
func NewProvisioningServiceImpl() *ProvisioningServiceImpl { func NewProvisioningServiceImpl() *ProvisioningServiceImpl {
logger := log.New("provisioning")
return &ProvisioningServiceImpl{ return &ProvisioningServiceImpl{
log: log.New("provisioning"), log: logger,
newDashboardProvisioner: dashboards.New, newDashboardProvisioner: dashboards.New,
provisionNotifiers: notifiers.Provision, provisionNotifiers: notifiers.Provision,
provisionDatasources: datasources.Provision, provisionDatasources: datasources.Provision,
provisionPlugins: plugins.Provision, provisionPlugins: plugins.Provision,
provisionRules: rules.Provision,
} }
} }
@ -95,6 +113,7 @@ func newProvisioningServiceImpl(
type ProvisioningServiceImpl struct { type ProvisioningServiceImpl struct {
Cfg *setting.Cfg Cfg *setting.Cfg
SQLStore *sqlstore.SQLStore SQLStore *sqlstore.SQLStore
ac accesscontrol.AccessControl
pluginStore plugifaces.Store pluginStore plugifaces.Store
EncryptionService encryption.Internal EncryptionService encryption.Internal
NotificationService *notifications.NotificationService NotificationService *notifications.NotificationService
@ -105,6 +124,7 @@ type ProvisioningServiceImpl struct {
provisionNotifiers func(context.Context, string, notifiers.Manager, notifiers.SQLStore, encryption.Internal, *notifications.NotificationService) error provisionNotifiers func(context.Context, string, notifiers.Manager, notifiers.SQLStore, encryption.Internal, *notifications.NotificationService) error
provisionDatasources func(context.Context, string, datasources.Store, utils.OrgStore) error provisionDatasources func(context.Context, string, datasources.Store, utils.OrgStore) error
provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store, pluginsettings.Service) error provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store, pluginsettings.Service) error
provisionRules func(context.Context, string, dashboardservice.DashboardService, dashboardservice.DashboardProvisioningService, provisioning.AlertRuleService) error
mutex sync.Mutex mutex sync.Mutex
dashboardProvisioningService dashboardservice.DashboardProvisioningService dashboardProvisioningService dashboardservice.DashboardProvisioningService
dashboardService dashboardservice.DashboardService dashboardService dashboardservice.DashboardService
@ -112,6 +132,7 @@ type ProvisioningServiceImpl struct {
alertingService *alerting.AlertNotificationService alertingService *alerting.AlertNotificationService
pluginsSettings pluginsettings.Service pluginsSettings pluginsettings.Service
searchService searchV2.SearchService searchService searchV2.SearchService
quotaService quota.Service
} }
func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error { func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error {
@ -130,6 +151,11 @@ func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) erro
return err return err
} }
err = ps.ProvisionAlertRules(ctx)
if err != nil {
return err
}
return nil return nil
} }
@ -218,6 +244,29 @@ func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) erro
return nil return nil
} }
func (ps *ProvisioningServiceImpl) ProvisionAlertRules(ctx context.Context) error {
alertRulesPath := filepath.Join(ps.Cfg.ProvisioningPath, "alerting")
st := store.DBstore{
BaseInterval: ps.Cfg.UnifiedAlerting.BaseInterval,
DefaultInterval: ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval,
SQLStore: ps.SQLStore,
Logger: ps.log,
FolderService: nil, // we don't use it yet
AccessControl: ps.ac,
DashboardService: ps.dashboardService,
}
ruleService := provisioning.NewAlertRuleService(
st,
st,
ps.quotaService,
ps.SQLStore,
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
int64(ps.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
ps.log)
return rules.Provision(ctx, alertRulesPath, ps.dashboardService,
ps.dashboardProvisioningService, *ruleService)
}
func (ps *ProvisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string { func (ps *ProvisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string {
return ps.dashboardProvisioner.GetProvisionerResolvedPath(name) return ps.dashboardProvisioner.GetProvisionerResolvedPath(name)
} }

View File

@ -8,6 +8,7 @@ type Calls struct {
ProvisionPlugins []interface{} ProvisionPlugins []interface{}
ProvisionNotifications []interface{} ProvisionNotifications []interface{}
ProvisionDashboards []interface{} ProvisionDashboards []interface{}
ProvisionAlertRules []interface{}
GetDashboardProvisionerResolvedPath []interface{} GetDashboardProvisionerResolvedPath []interface{}
GetAllowUIUpdatesFromConfig []interface{} GetAllowUIUpdatesFromConfig []interface{}
Run []interface{} Run []interface{}
@ -71,6 +72,11 @@ func (mock *ProvisioningServiceMock) ProvisionDashboards(ctx context.Context) er
return nil return nil
} }
func (mock *ProvisioningServiceMock) ProvisionAlertRules(ctx context.Context) error {
mock.Calls.ProvisionAlertRules = append(mock.Calls.ProvisionAlertRules, nil)
return nil
}
func (mock *ProvisioningServiceMock) GetDashboardProvisionerResolvedPath(name string) string { func (mock *ProvisioningServiceMock) GetDashboardProvisionerResolvedPath(name string) string {
mock.Calls.GetDashboardProvisionerResolvedPath = append(mock.Calls.GetDashboardProvisionerResolvedPath, name) mock.Calls.GetDashboardProvisionerResolvedPath = append(mock.Calls.GetDashboardProvisionerResolvedPath, name)
if mock.GetDashboardProvisionerResolvedPathFunc != nil { if mock.GetDashboardProvisionerResolvedPathFunc != nil {