Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56e1be8a60 |
@@ -89,7 +89,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20260114220314-c56cf1580dcf // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
|
||||
@@ -1627,6 +1627,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/alerting v0.0.0-20260114220314-c56cf1580dcf h1:3q+bASuUgOPd3AVcmfaZCvqZjf7lhB6Oydt8OdShcy8=
|
||||
github.com/grafana/alerting v0.0.0-20260114220314-c56cf1580dcf/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package definitions
|
||||
|
||||
import (
|
||||
"github.com/grafana/alerting/definition"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
)
|
||||
|
||||
@@ -117,8 +118,9 @@ type MuteTimingHeaders struct {
|
||||
type MuteTimeInterval struct {
|
||||
UID string `json:"-" yaml:"-"`
|
||||
config.MuteTimeInterval `json:",inline" yaml:",inline"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Provenance Provenance `json:"provenance,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Provenance Provenance `json:"provenance,omitempty"`
|
||||
Kind definition.TimeIntervalKind `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func (mt *MuteTimeInterval) ResourceType() string {
|
||||
|
||||
@@ -434,8 +434,8 @@ func (ng *AlertNG) init() error {
|
||||
// Provisioning
|
||||
policyService := provisioning.NewNotificationPolicyService(configStore, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log)
|
||||
contactPointService := provisioning.NewContactPointService(configStore, ng.SecretsService, ng.store, ng.store, provisioningReceiverService, ng.Log, ng.store, ng.ResourcePermissions)
|
||||
templateService := provisioning.NewTemplateService(configStore, ng.store, ng.store, ng.Log)
|
||||
muteTimingService := provisioning.NewMuteTimingService(configStore, ng.store, ng.store, ng.Log, ng.store)
|
||||
templateService := provisioning.NewTemplateService(configStore, ng.store, ng.store, ng.Log).WithIncludeImported()
|
||||
muteTimingService := provisioning.NewMuteTimingService(configStore, ng.store, ng.store, ng.Log, ng.store).WithIncludeImported()
|
||||
alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.folderService, ng.QuotaService, ng.store,
|
||||
int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
|
||||
int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/grafana/alerting/definition"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/timeinterval"
|
||||
"golang.org/x/exp/maps"
|
||||
@@ -27,6 +28,7 @@ type MuteTimingService struct {
|
||||
log log.Logger
|
||||
validator validation.ProvenanceStatusTransitionValidator
|
||||
ruleNotificationsStore AlertRuleNotificationSettingsStore
|
||||
includeImported bool
|
||||
}
|
||||
|
||||
func NewMuteTimingService(config alertmanagerConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger, ns AlertRuleNotificationSettingsStore) *MuteTimingService {
|
||||
@@ -37,6 +39,19 @@ func NewMuteTimingService(config alertmanagerConfigStore, prov ProvisioningStore
|
||||
log: log,
|
||||
validator: validation.ValidateProvenanceRelaxed,
|
||||
ruleNotificationsStore: ns,
|
||||
includeImported: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *MuteTimingService) WithIncludeImported() *MuteTimingService {
|
||||
return &MuteTimingService{
|
||||
configStore: svc.configStore,
|
||||
provenanceStore: svc.provenanceStore,
|
||||
xact: svc.xact,
|
||||
log: svc.log,
|
||||
validator: svc.validator,
|
||||
ruleNotificationsStore: svc.ruleNotificationsStore,
|
||||
includeImported: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +62,15 @@ func (svc *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
intervals := getTimeIntervals(rev)
|
||||
// Get Grafana intervals
|
||||
grafanaIntervals := make([]config.MuteTimeInterval, 0, len(rev.Config.AlertmanagerConfig.TimeIntervals)+len(rev.Config.AlertmanagerConfig.MuteTimeIntervals))
|
||||
for _, interval := range rev.Config.AlertmanagerConfig.TimeIntervals {
|
||||
grafanaIntervals = append(grafanaIntervals, config.MuteTimeInterval(interval))
|
||||
}
|
||||
grafanaIntervals = append(grafanaIntervals, rev.Config.AlertmanagerConfig.MuteTimeIntervals...)
|
||||
|
||||
if len(intervals) == 0 {
|
||||
// Early return if no intervals (including imported)
|
||||
if len(grafanaIntervals) == 0 && (!svc.includeImported || len(rev.Config.ExtraConfigs) == 0) {
|
||||
return []definitions.MuteTimeInterval{}, nil
|
||||
}
|
||||
|
||||
@@ -58,22 +79,51 @@ func (svc *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slices.SortFunc(intervals, func(a, b config.MuteTimeInterval) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
result := make([]definitions.MuteTimeInterval, 0, len(intervals))
|
||||
for _, interval := range intervals {
|
||||
result := make([]definitions.MuteTimeInterval, 0, len(grafanaIntervals))
|
||||
for _, interval := range grafanaIntervals {
|
||||
version := calculateMuteTimeIntervalFingerprint(interval)
|
||||
def := definitions.MuteTimeInterval{
|
||||
UID: legacy_storage.NameToUid(interval.Name),
|
||||
UID: timeIntervalUID(string(definition.GrafanaTimeIntervalKind), interval.Name),
|
||||
MuteTimeInterval: interval,
|
||||
Version: version,
|
||||
Kind: definition.GrafanaTimeIntervalKind,
|
||||
}
|
||||
if prov, ok := provenances[def.ResourceID()]; ok {
|
||||
def.Provenance = definitions.Provenance(prov)
|
||||
}
|
||||
result = append(result, def)
|
||||
}
|
||||
|
||||
// Add imported Mimir intervals if requested
|
||||
if svc.includeImported && len(rev.Config.ExtraConfigs) > 0 {
|
||||
mimirCfg := rev.Config.ExtraConfigs[0]
|
||||
cfg, err := mimirCfg.GetAlertmanagerConfig()
|
||||
if err == nil {
|
||||
mimirIntervals := make([]config.MuteTimeInterval, 0, len(cfg.GetMuteTimeIntervals())+len(cfg.GetTimeIntervals()))
|
||||
mimirIntervals = append(mimirIntervals, cfg.GetMuteTimeIntervals()...)
|
||||
for _, ti := range cfg.GetTimeIntervals() {
|
||||
mimirIntervals = append(mimirIntervals, config.MuteTimeInterval(ti))
|
||||
}
|
||||
|
||||
for _, interval := range mimirIntervals {
|
||||
version := calculateMuteTimeIntervalFingerprint(interval)
|
||||
def := definitions.MuteTimeInterval{
|
||||
UID: timeIntervalUID(string(definition.MimirTimeIntervalKind), interval.Name),
|
||||
MuteTimeInterval: interval,
|
||||
Version: version,
|
||||
Kind: definition.MimirTimeIntervalKind,
|
||||
Provenance: definitions.Provenance(models.ProvenanceConvertedPrometheus),
|
||||
}
|
||||
result = append(result, def)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
slices.SortFunc(result, func(a, b definitions.MuteTimeInterval) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -84,28 +134,71 @@ func (svc *MuteTimingService) GetMuteTiming(ctx context.Context, nameOrUID strin
|
||||
return definitions.MuteTimeInterval{}, err
|
||||
}
|
||||
|
||||
// Try to find in Grafana intervals first
|
||||
mt, found := getMuteTimingByName(rev, nameOrUID)
|
||||
kind := definition.GrafanaTimeIntervalKind
|
||||
provenance := models.ProvenanceNone
|
||||
|
||||
// If not found by name, try UID lookup in Grafana intervals
|
||||
if !found {
|
||||
name, err := legacy_storage.UidToName(nameOrUID)
|
||||
if err == nil {
|
||||
mt, found = getMuteTimingByName(rev, name)
|
||||
grafanaIntervals := getTimeIntervals(rev, false)
|
||||
for _, interval := range grafanaIntervals {
|
||||
if timeIntervalUID(string(definition.GrafanaTimeIntervalKind), interval.Name) == nameOrUID {
|
||||
mt = interval
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in Grafana and includeImported, try Mimir (by name or UID)
|
||||
if !found && svc.includeImported && len(rev.Config.ExtraConfigs) > 0 {
|
||||
mimirCfg := rev.Config.ExtraConfigs[0]
|
||||
cfg, err := mimirCfg.GetAlertmanagerConfig()
|
||||
if err == nil {
|
||||
// Collect all Mimir intervals
|
||||
mimirIntervals := make([]config.MuteTimeInterval, 0, len(cfg.GetMuteTimeIntervals())+len(cfg.GetTimeIntervals()))
|
||||
mimirIntervals = append(mimirIntervals, cfg.GetMuteTimeIntervals()...)
|
||||
for _, ti := range cfg.GetTimeIntervals() {
|
||||
mimirIntervals = append(mimirIntervals, config.MuteTimeInterval(ti))
|
||||
}
|
||||
|
||||
for _, interval := range mimirIntervals {
|
||||
// Check by name or UID
|
||||
if interval.Name == nameOrUID ||
|
||||
timeIntervalUID(string(definition.MimirTimeIntervalKind), interval.Name) == nameOrUID {
|
||||
mt = interval
|
||||
found = true
|
||||
kind = definition.MimirTimeIntervalKind
|
||||
provenance = models.ProvenanceConvertedPrometheus
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return definitions.MuteTimeInterval{}, ErrTimeIntervalNotFound.Errorf("")
|
||||
}
|
||||
|
||||
result := definitions.MuteTimeInterval{
|
||||
UID: legacy_storage.NameToUid(mt.Name),
|
||||
UID: timeIntervalUID(string(kind), mt.Name),
|
||||
MuteTimeInterval: mt,
|
||||
Version: calculateMuteTimeIntervalFingerprint(mt),
|
||||
Kind: kind,
|
||||
}
|
||||
|
||||
prov, err := svc.provenanceStore.GetProvenance(ctx, &result, orgID)
|
||||
if err != nil {
|
||||
return definitions.MuteTimeInterval{}, err
|
||||
// Only check provenance store for Grafana intervals
|
||||
if kind == definition.GrafanaTimeIntervalKind {
|
||||
prov, err := svc.provenanceStore.GetProvenance(ctx, &result, orgID)
|
||||
if err != nil {
|
||||
return definitions.MuteTimeInterval{}, err
|
||||
}
|
||||
result.Provenance = definitions.Provenance(prov)
|
||||
} else {
|
||||
result.Provenance = definitions.Provenance(provenance)
|
||||
}
|
||||
result.Provenance = definitions.Provenance(prov)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -115,6 +208,11 @@ func (svc *MuteTimingService) CreateMuteTiming(ctx context.Context, mt definitio
|
||||
return definitions.MuteTimeInterval{}, MakeErrTimeIntervalInvalid(err)
|
||||
}
|
||||
|
||||
// Block creation of Mimir kind intervals
|
||||
if mt.Kind == definition.MimirTimeIntervalKind {
|
||||
return definitions.MuteTimeInterval{}, MakeErrTimeIntervalInvalid(fmt.Errorf("time intervals of kind 'mimir' cannot be created"))
|
||||
}
|
||||
|
||||
revision, err := svc.configStore.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return definitions.MuteTimeInterval{}, err
|
||||
@@ -168,6 +266,16 @@ func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitio
|
||||
return definitions.MuteTimeInterval{}, ErrTimeIntervalNotFound.Errorf("")
|
||||
}
|
||||
|
||||
// Block updates to imported intervals
|
||||
existingMT := definitions.MuteTimeInterval{MuteTimeInterval: old}
|
||||
prov, err := svc.provenanceStore.GetProvenance(ctx, &existingMT, orgID)
|
||||
if err != nil {
|
||||
return definitions.MuteTimeInterval{}, err
|
||||
}
|
||||
if prov == models.ProvenanceConvertedPrometheus {
|
||||
return definitions.MuteTimeInterval{}, MakeErrTimeIntervalInvalid(fmt.Errorf("cannot update imported time interval from mimir/prometheus"))
|
||||
}
|
||||
|
||||
// check optimistic concurrency
|
||||
err = svc.checkOptimisticConcurrency(old, models.Provenance(mt.Provenance), mt.Version, "update")
|
||||
if err != nil {
|
||||
@@ -243,6 +351,12 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, nameOrUID st
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Block deletes of imported intervals
|
||||
if storedProvenance == models.ProvenanceConvertedPrometheus {
|
||||
return MakeErrTimeIntervalInvalid(fmt.Errorf("cannot delete imported time interval from mimir/prometheus"))
|
||||
}
|
||||
|
||||
if err := svc.validator(storedProvenance, models.Provenance(provenance)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -296,7 +410,7 @@ func isTimeIntervalInUseInRoutes(name string, route *definitions.Route) bool {
|
||||
}
|
||||
|
||||
func getMuteTimingByName(rev *legacy_storage.ConfigRevision, name string) (config.MuteTimeInterval, bool) {
|
||||
intervals := getTimeIntervals(rev)
|
||||
intervals := getTimeIntervals(rev, false)
|
||||
idx := slices.IndexFunc(intervals, func(interval config.MuteTimeInterval) bool {
|
||||
return interval.Name == name
|
||||
})
|
||||
@@ -306,12 +420,26 @@ func getMuteTimingByName(rev *legacy_storage.ConfigRevision, name string) (confi
|
||||
return intervals[idx], true
|
||||
}
|
||||
|
||||
func getTimeIntervals(rev *legacy_storage.ConfigRevision) []config.MuteTimeInterval {
|
||||
func getTimeIntervals(rev *legacy_storage.ConfigRevision, includeImported bool) []config.MuteTimeInterval {
|
||||
result := make([]config.MuteTimeInterval, 0, len(rev.Config.AlertmanagerConfig.TimeIntervals)+len(rev.Config.AlertmanagerConfig.MuteTimeIntervals))
|
||||
for _, interval := range rev.Config.AlertmanagerConfig.TimeIntervals {
|
||||
result = append(result, config.MuteTimeInterval(interval))
|
||||
}
|
||||
return append(result, rev.Config.AlertmanagerConfig.MuteTimeIntervals...)
|
||||
result = append(result, rev.Config.AlertmanagerConfig.MuteTimeIntervals...)
|
||||
|
||||
// Add imported intervals from Mimir config
|
||||
if includeImported && len(rev.Config.ExtraConfigs) > 0 {
|
||||
mimirCfg := rev.Config.ExtraConfigs[0]
|
||||
cfg, err := mimirCfg.GetAlertmanagerConfig()
|
||||
if err == nil {
|
||||
result = append(result, cfg.GetMuteTimeIntervals()...)
|
||||
for _, ti := range cfg.GetTimeIntervals() {
|
||||
result = append(result, config.MuteTimeInterval(ti))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func updateTimeInterval(rev *legacy_storage.ConfigRevision, interval config.MuteTimeInterval) {
|
||||
@@ -338,6 +466,10 @@ func deleteTimeInterval(rev *legacy_storage.ConfigRevision, interval config.Mute
|
||||
})
|
||||
}
|
||||
|
||||
func timeIntervalUID(kind string, name string) string {
|
||||
return legacy_storage.NameToUid(fmt.Sprintf("%s|%s", kind, name))
|
||||
}
|
||||
|
||||
func calculateMuteTimeIntervalFingerprint(interval config.MuteTimeInterval) string {
|
||||
sum := fnv.New64()
|
||||
|
||||
|
||||
@@ -66,17 +66,18 @@ func TestGetMuteTimings(t *testing.T) {
|
||||
require.Equal(t, "Test1", result[0].Name)
|
||||
require.EqualValues(t, provenances["Test1"], result[0].Provenance)
|
||||
require.NotEmpty(t, result[0].Version)
|
||||
require.Equal(t, legacy_storage.NameToUid(result[0].Name), result[0].UID)
|
||||
// UID now includes kind prefix
|
||||
require.Equal(t, legacy_storage.NameToUid(fmt.Sprintf("grafana|%s", result[0].Name)), result[0].UID)
|
||||
|
||||
require.Equal(t, "Test2", result[1].Name)
|
||||
require.EqualValues(t, provenances["Test2"], result[1].Provenance)
|
||||
require.NotEmpty(t, result[1].Version)
|
||||
require.Equal(t, legacy_storage.NameToUid(result[1].Name), result[1].UID)
|
||||
require.Equal(t, legacy_storage.NameToUid(fmt.Sprintf("grafana|%s", result[1].Name)), result[1].UID)
|
||||
|
||||
require.Equal(t, "Test3", result[2].Name)
|
||||
require.EqualValues(t, "", result[2].Provenance)
|
||||
require.NotEmpty(t, result[2].Version)
|
||||
require.Equal(t, legacy_storage.NameToUid(result[2].Name), result[2].UID)
|
||||
require.Equal(t, legacy_storage.NameToUid(fmt.Sprintf("grafana|%s", result[2].Name)), result[2].UID)
|
||||
|
||||
require.Len(t, store.Calls, 1)
|
||||
require.Equal(t, "Get", store.Calls[0].Method)
|
||||
@@ -161,7 +162,8 @@ func TestGetMuteTiming(t *testing.T) {
|
||||
|
||||
require.Equal(t, "Test1", result.Name)
|
||||
require.EqualValues(t, models.ProvenanceAPI, result.Provenance)
|
||||
require.Equal(t, legacy_storage.NameToUid(result.Name), result.UID)
|
||||
// UID now includes kind prefix
|
||||
require.Equal(t, legacy_storage.NameToUid(fmt.Sprintf("grafana|%s", result.Name)), result.UID)
|
||||
require.NotEmpty(t, result.Version)
|
||||
|
||||
require.Len(t, store.Calls, 1)
|
||||
@@ -192,7 +194,8 @@ func TestGetMuteTiming(t *testing.T) {
|
||||
|
||||
require.Equal(t, "Test2", result.Name)
|
||||
require.EqualValues(t, models.ProvenanceFile, result.Provenance)
|
||||
require.Equal(t, legacy_storage.NameToUid(result.Name), result.UID)
|
||||
// UID now includes kind prefix
|
||||
require.Equal(t, legacy_storage.NameToUid(fmt.Sprintf("grafana|%s", result.Name)), result.UID)
|
||||
require.NotEmpty(t, result.Version)
|
||||
|
||||
require.Len(t, store.Calls, 1)
|
||||
|
||||
Reference in New Issue
Block a user