Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support sending non-state notifications without an incident #188

Draft
wants to merge 6 commits into
base: non-state-notification-types-v1
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions internal/config/evaluable_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ type EvalOptions[T, U any] struct {
// Evaluable manages an evaluable config types in a centralised and structured way.
// An evaluable config is a config type that allows to evaluate filter expressions in some way.
type Evaluable struct {
Rules map[int64]bool `db:"-"`
RuleEntries map[int64]*rule.Escalation `db:"-" json:"-"`
Rules map[int64]bool `db:"-"`
RuleEntries map[int64]*rule.Entry `db:"-" json:"-"`
}

// NewEvaluable returns a fully initialised and ready to use Evaluable type.
func NewEvaluable() *Evaluable {
return &Evaluable{
Rules: make(map[int64]bool),
RuleEntries: make(map[int64]*rule.Escalation),
RuleEntries: make(map[int64]*rule.Entry),
}
}

Expand Down Expand Up @@ -95,7 +95,7 @@ func (e *Evaluable) EvaluateRules(r *RuntimeConfig, filterable filter.Filterable
// possible special cases.
//
// Returns an error if any of the provided callbacks return an error, otherwise always nil.
func (e *Evaluable) EvaluateRuleEntries(r *RuntimeConfig, filterable filter.Filterable, options EvalOptions[*rule.Escalation, any]) error {
func (e *Evaluable) EvaluateRuleEntries(r *RuntimeConfig, filterable filter.Filterable, options EvalOptions[*rule.Entry, any]) error {
retryAfter := rule.RetryNever

for ruleID := range e.Rules {
Expand All @@ -105,7 +105,7 @@ func (e *Evaluable) EvaluateRuleEntries(r *RuntimeConfig, filterable filter.Filt
continue
}

for _, entry := range ru.Escalations {
for _, entry := range ru.Entries {
if options.OnPreEvaluate != nil && !options.OnPreEvaluate(entry) {
continue
}
Expand Down
26 changes: 13 additions & 13 deletions internal/config/evaluable_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestEvaluableConfig(t *testing.T) {
runtime.Rules = maps.Clone(runtimeConfigTest.Rules)

e := NewEvaluable()
options := EvalOptions[*rule.Escalation, any]{}
options := EvalOptions[*rule.Entry, any]{}

expectedLen := 0
filterContext := &rule.EscalationFilter{IncidentSeverity: 9} // Event severity "emergency"
Expand All @@ -114,7 +114,7 @@ func TestEvaluableConfig(t *testing.T) {
require.NoError(t, e.EvaluateRuleEntries(runtime, filterContext, options))
}
require.Len(t, e.RuleEntries, *expectedLen)
e.RuleEntries = make(map[int64]*rule.Escalation)
e.RuleEntries = make(map[int64]*rule.Entry)
}

assertEntries(&expectedLen, false)
Expand All @@ -125,27 +125,27 @@ func TestEvaluableConfig(t *testing.T) {
// Drop some random rules from the runtime config to simulate a runtime config deletion!
maps.DeleteFunc(runtime.Rules, func(ruleID int64, _ *rule.Rule) bool { return ruleID > 35 && ruleID%defaultDivisor == 0 })

options.OnPreEvaluate = func(re *rule.Escalation) bool {
options.OnPreEvaluate = func(re *rule.Entry) bool {
if re.RuleID > 35 && re.RuleID%defaultDivisor == 0 { // Those rules are deleted from our runtime config.
require.Failf(t, "OnPreEvaluate() shouldn't have been called", "rule %d was deleted from runtime config", re.RuleID)
}

require.Nilf(t, e.RuleEntries[re.ID], "EvaluateRuleEntries() shouldn't evaluate entry %d twice", re.ID)
return true
}
options.OnError = func(re *rule.Escalation, err error) bool {
options.OnError = func(re *rule.Entry, err error) bool {
require.EqualError(t, err, `unknown severity "evaluable"`)
require.Truef(t, re.RuleID%defaultDivisor == 0, "evaluating rule entry %d should not fail", re.ID)
return true
}
options.OnFilterMatch = func(re *rule.Escalation) error {
options.OnFilterMatch = func(re *rule.Entry) error {
require.Nilf(t, e.RuleEntries[re.ID], "OnPreEvaluate() shouldn't evaluate %d twice", re.ID)
return nil
}
assertEntries(&expectedLen, false)

lenBeforeError := new(int)
options.OnError = func(re *rule.Escalation, err error) bool {
options.OnError = func(re *rule.Entry, err error) bool {
if *lenBeforeError != 0 {
require.Fail(t, "OnError() shouldn't have been called again")
}
Expand All @@ -160,7 +160,7 @@ func TestEvaluableConfig(t *testing.T) {

*lenBeforeError = 0
options.OnError = nil
options.OnFilterMatch = func(re *rule.Escalation) error {
options.OnFilterMatch = func(re *rule.Entry) error {
if *lenBeforeError != 0 {
require.Fail(t, "OnFilterMatch() shouldn't have been called again")
}
Expand All @@ -175,7 +175,7 @@ func TestEvaluableConfig(t *testing.T) {
filterContext.IncidentAge = 5 * time.Minute

options.OnFilterMatch = nil
options.OnPreEvaluate = func(re *rule.Escalation) bool { return re.RuleID < 5 }
options.OnPreEvaluate = func(re *rule.Entry) bool { return re.RuleID < 5 }
options.OnAllConfigEvaluated = func(result any) {
retryAfter := result.(time.Duration)
// The filter string of the escalation condition is incident_age>=10m and the actual incident age is 5m.
Expand All @@ -189,20 +189,20 @@ func makeRule(t *testing.T, i int) *rule.Rule {
r := new(rule.Rule)
r.ID = int64(i)
r.Name = fmt.Sprintf("rule-%d", i)
r.Escalations = make(map[int64]*rule.Escalation)
r.Entries = make(map[int64]*rule.Entry)

invalidSeverity, err := filter.Parse("incident_severity=evaluable")
require.NoError(t, err, "parsing incident_severity=evaluable shouldn't fail")

redundant := new(rule.Escalation)
redundant := new(rule.Entry)
redundant.ID = r.ID * 150 // It must be large enough to avoid colliding with others!
redundant.RuleID = r.ID
redundant.Condition = invalidSeverity

nonexistent, err := filter.Parse("nonexistent=evaluable")
require.NoError(t, err, "parsing nonexistent=evaluable shouldn't fail")

r.Escalations[redundant.ID] = redundant
r.Entries[redundant.ID] = redundant
r.ObjectFilter = nonexistent
if i%defaultDivisor == 0 {
objCond, err := filter.Parse("host=evaluable")
Expand All @@ -211,13 +211,13 @@ func makeRule(t *testing.T, i int) *rule.Rule {
escalationCond, err := filter.Parse("incident_severity>warning||incident_age>=10m")
require.NoError(t, err, "parsing incident_severity>=ok shouldn't fail")

entry := new(rule.Escalation)
entry := new(rule.Entry)
entry.ID = r.ID * 2
entry.RuleID = r.ID
entry.Condition = escalationCond

r.ObjectFilter = objCond
r.Escalations[entry.ID] = entry
r.Entries[entry.ID] = entry
}

return r
Expand Down
30 changes: 15 additions & 15 deletions internal/config/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (r *RuntimeConfig) applyPendingRules() {
newElement.TimePeriod = tp
}

newElement.Escalations = make(map[int64]*rule.Escalation)
newElement.Entries = make(map[int64]*rule.Entry)
return nil
},
func(curElement, update *rule.Rule) error {
Expand Down Expand Up @@ -48,17 +48,17 @@ func (r *RuntimeConfig) applyPendingRules() {

incrementalApplyPending(
r,
&r.ruleEscalations, &r.configChange.ruleEscalations,
func(newElement *rule.Escalation) error {
&r.ruleEntries, &r.configChange.ruleEntries,
func(newElement *rule.Entry) error {
elementRule, ok := r.Rules[newElement.RuleID]
if !ok {
return fmt.Errorf("rule escalation refers unknown rule %d", newElement.RuleID)
}

elementRule.Escalations[newElement.ID] = newElement
elementRule.Entries[newElement.ID] = newElement
return nil
},
func(curElement, update *rule.Escalation) error {
func(curElement, update *rule.Entry) error {
if curElement.RuleID != update.RuleID {
return errRemoveAndAddInstead
}
Expand All @@ -72,42 +72,42 @@ func (r *RuntimeConfig) applyPendingRules() {

return nil
},
func(delElement *rule.Escalation) error {
func(delElement *rule.Entry) error {
elementRule, ok := r.Rules[delElement.RuleID]
if !ok {
return nil
}

delete(elementRule.Escalations, delElement.ID)
delete(elementRule.Entries, delElement.ID)
return nil
})

incrementalApplyPending(
r,
&r.ruleEscalationRecipients, &r.configChange.ruleEscalationRecipients,
func(newElement *rule.EscalationRecipient) error {
&r.ruleEntryRecipients, &r.configChange.ruleEntryRecipients,
func(newElement *rule.EntryRecipient) error {
newElement.Recipient = r.GetRecipient(newElement.Key)
if newElement.Recipient == nil {
return fmt.Errorf("rule escalation recipient is missing or unknown")
}

escalation := r.GetRuleEscalation(newElement.EscalationID)
escalation := r.GetRuleEntry(newElement.EntryID)
if escalation == nil {
return fmt.Errorf("rule escalation recipient refers to unknown escalation %d", newElement.EscalationID)
return fmt.Errorf("rule escalation recipient refers to unknown escalation %d", newElement.EntryID)
}
escalation.Recipients = append(escalation.Recipients, newElement)

return nil
},
nil,
func(delElement *rule.EscalationRecipient) error {
escalation := r.GetRuleEscalation(delElement.EscalationID)
func(delElement *rule.EntryRecipient) error {
escalation := r.GetRuleEntry(delElement.EntryID)
if escalation == nil {
return nil
}

escalation.Recipients = slices.DeleteFunc(escalation.Recipients, func(recipient *rule.EscalationRecipient) bool {
return recipient.EscalationID == delElement.EscalationID
escalation.Recipients = slices.DeleteFunc(escalation.Recipients, func(recipient *rule.EntryRecipient) bool {
return recipient.EntryID == delElement.EntryID
})
return nil
})
Expand Down
26 changes: 13 additions & 13 deletions internal/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ type ConfigSet struct {

// The following fields contain intermediate values, necessary for the incremental config synchronization.
// Furthermore, they allow accessing intermediate tables as everything is referred by pointers.
groupMembers map[recipient.GroupMemberKey]*recipient.GroupMember
timePeriodEntries map[int64]*timeperiod.Entry
scheduleRotations map[int64]*recipient.Rotation
scheduleRotationMembers map[int64]*recipient.RotationMember
ruleEscalations map[int64]*rule.Escalation
ruleEscalationRecipients map[int64]*rule.EscalationRecipient
groupMembers map[recipient.GroupMemberKey]*recipient.GroupMember
timePeriodEntries map[int64]*timeperiod.Entry
scheduleRotations map[int64]*recipient.Rotation
scheduleRotationMembers map[int64]*recipient.RotationMember
ruleEntries map[int64]*rule.Entry
ruleEntryRecipients map[int64]*rule.EntryRecipient
}

func (r *RuntimeConfig) UpdateFromDatabase(ctx context.Context) error {
Expand Down Expand Up @@ -156,13 +156,13 @@ func (r *RuntimeConfig) GetRecipient(k recipient.Key) recipient.Recipient {
return nil
}

// GetRuleEscalation returns a *rule.Escalation by the given id.
// Returns nil if there is no rule escalation with given id.
func (r *RuntimeConfig) GetRuleEscalation(escalationID int64) *rule.Escalation {
// GetRuleEntry returns a *rule.Entry by the given id.
// Returns nil if there is no rule entry with given id.
func (r *RuntimeConfig) GetRuleEntry(entryID int64) *rule.Entry {
for _, r := range r.Rules {
escalation, ok := r.Escalations[escalationID]
entry, ok := r.Entries[entryID]
if ok {
return escalation
return entry
}
}

Expand Down Expand Up @@ -248,8 +248,8 @@ func (r *RuntimeConfig) fetchFromDatabase(ctx context.Context) error {
func() error { return incrementalFetch(ctx, tx, r, &r.configChange.TimePeriods) },
func() error { return incrementalFetch(ctx, tx, r, &r.configChange.timePeriodEntries) },
func() error { return incrementalFetch(ctx, tx, r, &r.configChange.Rules) },
func() error { return incrementalFetch(ctx, tx, r, &r.configChange.ruleEscalations) },
func() error { return incrementalFetch(ctx, tx, r, &r.configChange.ruleEscalationRecipients) },
func() error { return incrementalFetch(ctx, tx, r, &r.configChange.ruleEntries) },
func() error { return incrementalFetch(ctx, tx, r, &r.configChange.ruleEntryRecipients) },
func() error { return incrementalFetch(ctx, tx, r, &r.configChange.Sources) },
}
for _, f := range fetchFns {
Expand Down
56 changes: 28 additions & 28 deletions internal/config/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,70 +252,70 @@ func (r *RuntimeConfig) debugVerifyRule(id int64, rule *rule.Rule) error {
return fmt.Errorf("rule has a ObjectFilterExpr but ObjectFilter is nil")
}

for escalationID, escalation := range rule.Escalations {
if escalation == nil {
return fmt.Errorf("Escalations[%d] is nil", escalationID)
for entryID, entry := range rule.Entries {
if entry == nil {
return fmt.Errorf("Entries[%d] is nil", entryID)
}

if escalation.ID != escalationID {
return fmt.Errorf("Escalations[%d]: ecalation has ID %d but is referenced as %d",
escalationID, escalation.ID, escalationID)
if entry.ID != entryID {
return fmt.Errorf("Entries[%d]: ecalation has ID %d but is referenced as %d",
entryID, entry.ID, entryID)
}

if escalation.RuleID != rule.ID {
return fmt.Errorf("Escalations[%d] (ID=%d) has RuleID = %d while being referenced from rule %d",
escalationID, escalation.ID, escalation.RuleID, rule.ID)
if entry.RuleID != rule.ID {
return fmt.Errorf("Entries[%d] (ID=%d) has RuleID = %d while being referenced from rule %d",
entryID, entry.ID, entry.RuleID, rule.ID)
}

if escalation.ConditionExpr.Valid && escalation.Condition == nil {
return fmt.Errorf("Escalations[%d] (ID=%d) has ConditionExpr but Condition is nil", escalationID, escalation.ID)
if entry.ConditionExpr.Valid && entry.Condition == nil {
return fmt.Errorf("Entries[%d] (ID=%d) has ConditionExpr but Condition is nil", entryID, entry.ID)
}

// TODO: verify fallback

for i, escalationRecpient := range escalation.Recipients {
if escalationRecpient == nil {
return fmt.Errorf("Escalations[%d].Recipients[%d] is nil", escalationID, i)
for i, entryRecipient := range entry.Recipients {
if entryRecipient == nil {
return fmt.Errorf("Entries[%d].Recipients[%d] is nil", entryID, i)
}

if escalationRecpient.EscalationID != escalation.ID {
return fmt.Errorf("Escalation[%d].Recipients[%d].EscalationID = %d does not match Escalations[%d].ID = %d",
escalationID, i, escalationRecpient.EscalationID, escalationID, escalation.ID)
if entryRecipient.EntryID != entry.ID {
return fmt.Errorf("Entry[%d].Recipients[%d].EntryID = %d does not match Entries[%d].ID = %d",
entryID, i, entryRecipient.EntryID, entryID, entry.ID)
}

switch rec := escalationRecpient.Recipient.(type) {
switch rec := entryRecipient.Recipient.(type) {
case *recipient.Contact:
if rec == nil {
return fmt.Errorf("Escalations[%d].Recipients[%d].Recipient (Contact) is nil", escalationID, i)
return fmt.Errorf("Entries[%d].Recipients[%d].Recipient (Contact) is nil", entryID, i)
}

err := r.debugVerifyContact(escalationRecpient.ContactID.Int64, rec)
err := r.debugVerifyContact(entryRecipient.ContactID.Int64, rec)
if err != nil {
return fmt.Errorf("Escalations[%d].Recipients[%d].Recipient (Contact): %w", escalationID, i, err)
return fmt.Errorf("Entries[%d].Recipients[%d].Recipient (Contact): %w", entryID, i, err)
}

case *recipient.Group:
if rec == nil {
return fmt.Errorf("Escalations[%d].Recipients[%d].Recipient (Group) is nil", escalationID, i)
return fmt.Errorf("Entries[%d].Recipients[%d].Recipient (Group) is nil", entryID, i)
}

err := r.debugVerifyGroup(escalationRecpient.GroupID.Int64, rec)
err := r.debugVerifyGroup(entryRecipient.GroupID.Int64, rec)
if err != nil {
return fmt.Errorf("Escalations[%d].Recipients[%d].Recipient (Group): %w", escalationID, i, err)
return fmt.Errorf("Entries[%d].Recipients[%d].Recipient (Group): %w", entryID, i, err)
}

case *recipient.Schedule:
if rec == nil {
return fmt.Errorf("Escalations[%d].Recipients[%d].Recipient (Schedule) is nil", escalationID, i)
return fmt.Errorf("Entries[%d].Recipients[%d].Recipient (Schedule) is nil", entryID, i)
}

err := r.debugVerifySchedule(escalationRecpient.ScheduleID.Int64, rec)
err := r.debugVerifySchedule(entryRecipient.ScheduleID.Int64, rec)
if err != nil {
return fmt.Errorf("Escalations[%d].Recipients[%d].Recipient (Schedule): %w", escalationID, i, err)
return fmt.Errorf("Entries[%d].Recipients[%d].Recipient (Schedule): %w", entryID, i, err)
}

default:
return fmt.Errorf("Escalations[%d].Recipients[%d].Recipient has invalid type %T", escalationID, i, rec)
return fmt.Errorf("Entries[%d].Recipients[%d].Recipient has invalid type %T", entryID, i, rec)
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions internal/daemon/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,20 @@ func ParseFlagsAndConfig() {
utils.PrintErrorThenExit(err, ExitFailure)
}
}

// InitTestConfig initialises the global daemon config instance and applies the defaults.
// This should be used for unit tests only.
func InitTestConfig() error {
daemonConfig = new(ConfigFile)
if err := defaults.Set(daemonConfig); err != nil {
return err
}
if err := defaults.Set(&daemonConfig.Database); err != nil {
return err
}
if err := defaults.Set(&daemonConfig.Logging); err != nil {
return err
}

return nil
}
Loading
Loading