Skip to content

Commit 7d65e62

Browse files
committed
Support sending non-state notifications
1 parent 4f947bb commit 7d65e62

File tree

10 files changed

+463
-116
lines changed

10 files changed

+463
-116
lines changed

internal/daemon/config.go

+17
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,20 @@ func ParseFlagsAndConfig() {
101101
utils.PrintErrorThenExit(err, ExitFailure)
102102
}
103103
}
104+
105+
// InitTestConfig initialises the global daemon config instance and applies the defaults.
106+
// This should be used for unit tests only.
107+
func InitTestConfig() error {
108+
daemonConfig = new(ConfigFile)
109+
if err := defaults.Set(daemonConfig); err != nil {
110+
return err
111+
}
112+
if err := defaults.Set(&daemonConfig.Database); err != nil {
113+
return err
114+
}
115+
if err := defaults.Set(&daemonConfig.Logging); err != nil {
116+
return err
117+
}
118+
119+
return nil
120+
}

internal/events/events.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package events
2+
3+
import (
4+
"context"
5+
"github.com/icinga/icinga-go-library/database"
6+
"github.com/icinga/icinga-go-library/logging"
7+
"github.com/icinga/icinga-notifications/internal/config"
8+
"github.com/icinga/icinga-notifications/internal/event"
9+
"github.com/icinga/icinga-notifications/internal/notification"
10+
)
11+
12+
// Process processes the specified event.Event.
13+
//
14+
// Please note that this function is the only way to access the internal events.router type.
15+
//
16+
// The returned error might be wrapped around event.ErrSuperfluousStateChange.
17+
func Process(ctx context.Context, db *database.DB, logs *logging.Logging, rc *config.RuntimeConfig, ev *event.Event) error {
18+
r := &router{
19+
logs: logs,
20+
Evaluable: config.NewEvaluable(),
21+
Notifier: notification.Notifier{
22+
DB: db,
23+
RuntimeConfig: rc,
24+
Logger: logs.GetChildLogger("routing").SugaredLogger,
25+
},
26+
}
27+
28+
return r.route(ctx, ev)
29+
}

internal/events/events_test.go

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package events
2+
3+
import (
4+
"context"
5+
"github.com/icinga/icinga-go-library/logging"
6+
"github.com/icinga/icinga-go-library/types"
7+
"github.com/icinga/icinga-notifications/internal/config"
8+
"github.com/icinga/icinga-notifications/internal/daemon"
9+
"github.com/icinga/icinga-notifications/internal/event"
10+
"github.com/icinga/icinga-notifications/internal/incident"
11+
"github.com/icinga/icinga-notifications/internal/object"
12+
"github.com/icinga/icinga-notifications/internal/testutils"
13+
"github.com/icinga/icinga-notifications/internal/utils"
14+
"github.com/jmoiron/sqlx"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
"go.uber.org/zap/zapcore"
18+
"go.uber.org/zap/zaptest"
19+
"testing"
20+
"time"
21+
)
22+
23+
func TestProcess(t *testing.T) {
24+
ctx := context.Background()
25+
db := testutils.GetTestDB(ctx, t)
26+
27+
require.NoError(t, daemon.InitTestConfig(), "mocking daemon.Config should not fail")
28+
29+
// Insert a dummy source for our test cases!
30+
source := config.Source{Type: "notifications", Name: "Icinga Notifications", Icinga2InsecureTLS: types.Bool{Bool: false, Valid: true}}
31+
source.ChangedAt = types.UnixMilli(time.Now())
32+
source.Deleted = types.Bool{Bool: false, Valid: true}
33+
34+
err := utils.RunInTx(ctx, db, func(tx *sqlx.Tx) error {
35+
id, err := utils.InsertAndFetchId(ctx, tx, utils.BuildInsertStmtWithout(db, source, "id"), source)
36+
require.NoError(t, err, "populating source table should not fail")
37+
38+
source.ID = id
39+
return nil
40+
})
41+
require.NoError(t, err, "utils.RunInTx() should not fail")
42+
43+
logs, err := logging.NewLogging("events-router", zapcore.DebugLevel, "console", nil, time.Hour)
44+
require.NoError(t, err, "logging initialisation should not fail")
45+
46+
runtimeConfig := new(config.RuntimeConfig)
47+
48+
t.Run("InvalidEvents", func(t *testing.T) {
49+
assert.Nil(t, Process(ctx, db, logs, runtimeConfig, makeEvent(t, source.ID, event.TypeState, event.SeverityNone)))
50+
assert.ErrorIs(t, Process(ctx, db, logs, runtimeConfig, makeEvent(t, source.ID, event.TypeState, event.SeverityOK)), event.ErrSuperfluousStateChange)
51+
assert.ErrorIs(t, Process(ctx, db, logs, runtimeConfig, makeEvent(t, source.ID, event.TypeAcknowledgementSet, event.SeverityOK)), event.ErrSuperfluousStateChange)
52+
assert.ErrorIs(t, Process(ctx, db, logs, runtimeConfig, makeEvent(t, source.ID, event.TypeAcknowledgementCleared, event.SeverityOK)), event.ErrSuperfluousStateChange)
53+
})
54+
55+
t.Run("StateChangeEvents", func(t *testing.T) {
56+
states := map[string]*event.Event{
57+
"crit": makeEvent(t, source.ID, event.TypeState, event.SeverityCrit),
58+
"warn": makeEvent(t, source.ID, event.TypeState, event.SeverityWarning),
59+
"err": makeEvent(t, source.ID, event.TypeState, event.SeverityErr),
60+
"alert": makeEvent(t, source.ID, event.TypeState, event.SeverityAlert),
61+
}
62+
63+
for severity, ev := range states {
64+
assert.NoErrorf(t, Process(ctx, db, logs, runtimeConfig, ev), "state event with severity %q should open an incident", severity)
65+
assert.ErrorIsf(t, Process(ctx, db, logs, runtimeConfig, ev), event.ErrSuperfluousStateChange,
66+
"superfluous state event %q should be ignored", severity)
67+
68+
obj := object.GetFromCache(object.ID(source.ID, ev.Tags))
69+
require.NotNil(t, obj, "there should be a cached object")
70+
71+
i, err := incident.GetCurrent(ctx, db, obj, logs.GetLogger(), runtimeConfig, false)
72+
require.NoError(t, err, "retrieving current incident should not fail")
73+
require.NotNil(t, i, "there should be a cached incident")
74+
assert.Equal(t, ev.Severity, i.Severity, "severities should be equal")
75+
}
76+
77+
reloadIncidents := func(ctx context.Context) {
78+
object.ClearCache()
79+
80+
// Remove all existing incidents from the cache, as they are indexed with the
81+
// pointer of their object, which is going to change!
82+
for _, i := range incident.GetCurrentIncidents() {
83+
incident.RemoveCurrent(i.Object)
84+
}
85+
86+
// The incident loading process may hang due to unknown bugs or semaphore lock waits.
87+
// Therefore, give it maximum time of 10s to finish normally, otherwise give up and fail.
88+
ctx, cancelFunc := context.WithDeadline(ctx, time.Now().Add(10*time.Second))
89+
defer cancelFunc()
90+
91+
err := incident.LoadOpenIncidents(ctx, db, logging.NewLogger(zaptest.NewLogger(t).Sugar(), time.Hour), runtimeConfig)
92+
require.NoError(t, err, "loading active incidents should not fail")
93+
}
94+
reloadIncidents(ctx)
95+
96+
for severity, ev := range states {
97+
obj, err := object.FromEvent(ctx, db, ev)
98+
assert.NoError(t, err)
99+
100+
i, err := incident.GetCurrent(ctx, db, obj, logs.GetLogger(), runtimeConfig, false)
101+
assert.NoErrorf(t, err, "incident for event severity %q should be in cache", severity)
102+
103+
assert.Equal(t, obj, i.Object, "incident and event object should be the same")
104+
assert.Equal(t, i.Severity, ev.Severity, "incident and event severity should be the same")
105+
}
106+
107+
// Recover the incidents
108+
for _, ev := range states {
109+
ev.Time = time.Now()
110+
ev.Severity = event.SeverityOK
111+
112+
assert.NoErrorf(t, Process(ctx, db, logs, runtimeConfig, ev), "state event with severity %q should close an incident", "ok")
113+
}
114+
reloadIncidents(ctx)
115+
assert.Len(t, incident.GetCurrentIncidents(), 0, "there should be no cached incidents")
116+
})
117+
118+
t.Run("NonStateEvents", func(t *testing.T) {
119+
events := []*event.Event{
120+
makeEvent(t, source.ID, event.TypeDowntimeStart, event.SeverityNone),
121+
makeEvent(t, source.ID, event.TypeDowntimeEnd, event.SeverityNone),
122+
makeEvent(t, source.ID, event.TypeDowntimeRemoved, event.SeverityNone),
123+
makeEvent(t, source.ID, event.TypeCustom, event.SeverityNone),
124+
makeEvent(t, source.ID, event.TypeFlappingStart, event.SeverityNone),
125+
makeEvent(t, source.ID, event.TypeFlappingEnd, event.SeverityNone),
126+
}
127+
128+
for _, ev := range events {
129+
assert.NoErrorf(t, Process(ctx, db, logs, runtimeConfig, ev), "processing non-state event %q should not fail", ev.Type)
130+
assert.Lenf(t, incident.GetCurrentIncidents(), 0, "non-state event %q should not open an incident", ev.Type)
131+
require.NotNil(t, object.GetFromCache(object.ID(source.ID, ev.Tags)), "there should be a cached object")
132+
}
133+
})
134+
}
135+
136+
// makeEvent creates a fully initialised event.Event of the given type and severity.
137+
func makeEvent(t *testing.T, sourceID int64, typ string, severity event.Severity) *event.Event {
138+
return &event.Event{
139+
SourceId: sourceID,
140+
Name: testutils.MakeRandomString(t),
141+
URL: "https://localhost/icingaweb2/icingadb",
142+
Type: typ,
143+
Time: time.Now(),
144+
Severity: severity,
145+
Username: "icingaadmin",
146+
Message: "You will contract a rare disease :(",
147+
Tags: map[string]string{
148+
"Host": testutils.MakeRandomString(t),
149+
"Service": testutils.MakeRandomString(t),
150+
},
151+
ExtraTags: map[string]string{
152+
"hostgroup/database-server": "",
153+
"servicegroup/webserver": "",
154+
},
155+
}
156+
}

0 commit comments

Comments
 (0)