Skip to content

Commit 1f5f18e

Browse files
authored
Merge pull request #145 from Icinga/notifications
Import notifications packages
2 parents da2e71b + 6600889 commit 1f5f18e

File tree

17 files changed

+1436
-0
lines changed

17 files changed

+1436
-0
lines changed

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ require (
3333
golang.org/x/sys v0.26.0 // indirect
3434
gopkg.in/yaml.v3 v3.0.1 // indirect
3535
)
36+
37+
tool (
38+
golang.org/x/tools/cmd/stringer
39+
)

notifications/event/event.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package event
2+
3+
import (
4+
"github.com/icinga/icinga-go-library/types"
5+
)
6+
7+
// Event represents an Icinga Notifications event that can be sent to the Icinga Notifications API.
8+
//
9+
// It contains all the necessary fields to fully describe an Icinga Notifications event and can be used to
10+
// serialize the event to JSON for transmission over HTTP as well as to deserialize it from JSON requests.
11+
type Event struct {
12+
Name string `json:"name"` // Name is the name of the object this event is all about.
13+
14+
// URL represents a URL or a relative reference to the object in Icinga Web 2.
15+
//
16+
// If the URL field does not contain a URL, but only a reference relative to an Icinga Web URL, the Icinga
17+
// Notifications daemon will create a URL. This allows a source to set this to something like
18+
// "/icingadb/host?name=example.com" without having to know the Icinga Web 2 root URL by itself.
19+
URL string `json:"url"`
20+
21+
// Tags contains additional metadata for the event that uniquely identifies the object it's referring to.
22+
//
23+
// It is a map of string keys to string values, allowing for flexible tagging of events if the event
24+
// name alone is not sufficient to identify the object. In the case of using Icinga DB as a source, the
25+
// tags will typically look like this:
26+
// For hosts: {"host": "host_name"} and for services: {"host": "host_name", "service": "service_name"}.
27+
Tags map[string]string `json:"tags"`
28+
29+
// ExtraTags supplement Tags, for example with host or service groups for an Icinga DB source.
30+
ExtraTags map[string]string `json:"extra_tags"`
31+
32+
// Type indicates the type of the event.
33+
Type Type `json:"type"`
34+
// Severity of the event.
35+
Severity Severity `json:"severity,omitempty"`
36+
// Username is the name of the user who triggered the event.
37+
Username string `json:"username"`
38+
// Message is a human-readable message describing the event.
39+
Message string `json:"message"`
40+
41+
// Mute indicates whether the object this event is referring to should be muted or not.
42+
//
43+
// If set to true, the object will be muted in Icinga Web 2, meaning that notifications for this object
44+
// will not be sent out. The MuteReason field can be used to provide a reason for muting the object.
45+
// If you don't set this field to anything, it will be omitted from the generated JSON.
46+
Mute types.Bool `json:"mute,omitzero"`
47+
48+
// MuteReason provides a reason for muting the object if Mute is set to true.
49+
//
50+
// Setting this field to an empty string while Mute is true will cause the request to fail,
51+
// as Icinga Notifications requires a reason for muting an object. Otherwise, it will be omitted
52+
// from the encoded JSON.
53+
MuteReason string `json:"mute_reason,omitempty"`
54+
55+
// RulesVersion and RuleIds are the source rules matching for this Event.
56+
RulesVersion string `json:"rules_version"`
57+
RuleIds []string `json:"rule_ids"`
58+
}

notifications/event/event_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package event
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestEvent(t *testing.T) {
12+
t.Parallel()
13+
14+
t.Run("JsonEncode", func(t *testing.T) {
15+
t.Parallel()
16+
17+
t.Run("Valid Event", func(t *testing.T) {
18+
t.Parallel()
19+
20+
event := &Event{
21+
Name: "TestEvent",
22+
URL: "/icingadb/service?name=https%20ssl%20v3.0%20compatibility%20IE%206.0&host.name=example%20host",
23+
Tags: map[string]string{"tag1": "value1"},
24+
ExtraTags: map[string]string{},
25+
Type: TypeState,
26+
Severity: SeverityOK,
27+
Username: "testuser",
28+
Message: "Test",
29+
RulesVersion: "0x1",
30+
RuleIds: []string{"1", "2", "3", "6"},
31+
}
32+
33+
data, err := json.Marshal(event)
34+
require.NoError(t, err)
35+
36+
expected := `
37+
{
38+
"name":"TestEvent",
39+
"url":"/icingadb/service?name=https%20ssl%20v3.0%20compatibility%20IE%206.0&host.name=example%20host",
40+
"tags":{"tag1":"value1"},
41+
"extra_tags":{},
42+
"type":"state",
43+
"severity":"ok",
44+
"username":"testuser",
45+
"message":"Test",
46+
"rules_version": "0x1",
47+
"rule_ids": ["1", "2", "3", "6"]
48+
}`
49+
assert.JSONEq(t, expected, string(data), "JSON encoding does not match expected output")
50+
})
51+
52+
t.Run("Empty Severity", func(t *testing.T) {
53+
t.Parallel()
54+
55+
event := &Event{
56+
Name: "TestEvent",
57+
URL: "https://example.com/icingaweb2/icingadb/service?name=https%20ssl%20v3.0%20compatibility%20IE%206.0&host.name=example%20host",
58+
Tags: map[string]string{"tag1": "value1"},
59+
Type: TypeMute,
60+
Username: "testuser",
61+
Message: "Test",
62+
}
63+
64+
data, err := json.Marshal(event)
65+
require.NoError(t, err)
66+
assert.NotContains(t, string(data), "\"severity\":", "severity should be omitted when empty")
67+
68+
event.Severity = SeverityNone
69+
data, err = json.Marshal(event)
70+
require.NoError(t, err)
71+
assert.NotContains(t, string(data), "\"severity\":", "severity should be omitted when set to none")
72+
})
73+
})
74+
}

notifications/event/severity.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//go:generate go tool stringer -linecomment -type Severity -output severity_string.go
2+
3+
package event
4+
5+
import (
6+
"database/sql/driver"
7+
"encoding/json"
8+
"fmt"
9+
)
10+
11+
// Severity represents the severity level of an event in Icinga notifications.
12+
// It is an integer type with predefined constants for different severity levels.
13+
type Severity uint8
14+
15+
const (
16+
SeverityNone Severity = iota // none
17+
18+
SeverityOK // ok
19+
SeverityDebug // debug
20+
SeverityInfo // info
21+
SeverityNotice // notice
22+
SeverityWarning // warning
23+
SeverityErr // err
24+
SeverityCrit // crit
25+
SeverityAlert // alert
26+
SeverityEmerg // emerg
27+
28+
severityMax // internal
29+
)
30+
31+
// MarshalJSON implements the [json.Marshaler] interface for Severity.
32+
func (s Severity) MarshalJSON() ([]byte, error) {
33+
if s != SeverityNone {
34+
return json.Marshal(s.String())
35+
} else {
36+
return json.Marshal(nil)
37+
}
38+
}
39+
40+
// UnmarshalJSON implements the [json.Unmarshaler] interface for Severity.
41+
func (s *Severity) UnmarshalJSON(data []byte) error {
42+
if string(data) == "null" {
43+
*s = SeverityNone
44+
return nil
45+
}
46+
47+
var severityStr string
48+
if err := json.Unmarshal(data, &severityStr); err != nil {
49+
return err
50+
}
51+
52+
severity, err := ParseSeverity(severityStr)
53+
if err != nil {
54+
return err
55+
}
56+
57+
*s = severity
58+
return nil
59+
}
60+
61+
// Scan implements the [sql.Scanner] interface for Severity.
62+
// Supports SQL NULL values.
63+
func (s *Severity) Scan(src any) error {
64+
if src == nil {
65+
*s = SeverityNone
66+
return nil
67+
}
68+
69+
var severityStr string
70+
switch val := src.(type) {
71+
case string:
72+
severityStr = val
73+
case []byte:
74+
severityStr = string(val)
75+
default:
76+
return fmt.Errorf("cannot scan severity from type %T", src)
77+
}
78+
79+
severity, err := ParseSeverity(severityStr)
80+
if err != nil {
81+
return err
82+
}
83+
84+
*s = severity
85+
return nil
86+
}
87+
88+
// Value implements the [driver.Valuer] interface for Severity.
89+
func (s Severity) Value() (driver.Value, error) {
90+
if s != SeverityNone {
91+
return s.String(), nil
92+
}
93+
return nil, nil // Return nil for SeverityNone or invalid values
94+
}
95+
96+
// ParseSeverity parses a string representation of a severity level and returns the corresponding Severity value.
97+
// If the string does not match any known severity, it returns an error indicating the unknown severity.
98+
func ParseSeverity(name string) (Severity, error) {
99+
for s := range severityMax {
100+
if s.String() == name {
101+
return s, nil
102+
}
103+
}
104+
105+
return SeverityNone, fmt.Errorf("unknown severity %q", name)
106+
}

notifications/event/severity_string.go

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package event
2+
3+
import (
4+
"database/sql/driver"
5+
"testing"
6+
7+
"github.com/icinga/icinga-go-library/testutils"
8+
)
9+
10+
func TestSeverity(t *testing.T) {
11+
t.Parallel()
12+
13+
t.Run("MarshalJson", func(t *testing.T) {
14+
t.Parallel()
15+
16+
testdata := []testutils.TestCase[string, Severity]{
17+
{Name: "None", Expected: "null", Data: SeverityNone, Error: nil},
18+
{Name: "Ok", Expected: `"ok"`, Data: SeverityOK, Error: nil},
19+
{Name: "Debug", Expected: `"debug"`, Data: SeverityDebug, Error: nil},
20+
{Name: "Info", Expected: `"info"`, Data: SeverityInfo, Error: nil},
21+
{Name: "Notice", Expected: `"notice"`, Data: SeverityNotice, Error: nil},
22+
{Name: "Warning", Expected: `"warning"`, Data: SeverityWarning, Error: nil},
23+
{Name: "Err", Expected: `"err"`, Data: SeverityErr, Error: nil},
24+
{Name: "Crit", Expected: `"crit"`, Data: SeverityCrit, Error: nil},
25+
{Name: "Alert", Expected: `"alert"`, Data: SeverityAlert, Error: nil},
26+
{Name: "Emerg", Expected: `"emerg"`, Data: SeverityEmerg, Error: nil},
27+
}
28+
29+
for _, tt := range testdata {
30+
t.Run(tt.Name, tt.F(func(s Severity) (string, error) {
31+
data, err := s.MarshalJSON()
32+
return string(data), err
33+
}))
34+
}
35+
})
36+
37+
t.Run("UnmarshalJson", func(t *testing.T) {
38+
t.Parallel()
39+
40+
testData := []testutils.TestCase[Severity, string]{
41+
{Name: "None", Expected: SeverityNone, Data: `null`, Error: nil},
42+
{Name: "Ok", Expected: SeverityOK, Data: `"ok"`, Error: nil},
43+
{Name: "Debug", Expected: SeverityDebug, Data: `"debug"`, Error: nil},
44+
{Name: "Info", Expected: SeverityInfo, Data: `"info"`, Error: nil},
45+
{Name: "Notice", Expected: SeverityNotice, Data: `"notice"`, Error: nil},
46+
{Name: "Warning", Expected: SeverityWarning, Data: `"warning"`, Error: nil},
47+
{Name: "Err", Expected: SeverityErr, Data: `"err"`, Error: nil},
48+
{Name: "Crit", Expected: SeverityCrit, Data: `"crit"`, Error: nil},
49+
{Name: "Alert", Expected: SeverityAlert, Data: `"alert"`, Error: nil},
50+
{Name: "Emerg", Expected: SeverityEmerg, Data: `"emerg"`, Error: nil},
51+
{Name: "Invalid", Expected: SeverityNone, Data: `"invalid"`, Error: testutils.ErrorContains(`unknown severity "invalid"`)},
52+
}
53+
54+
for _, tt := range testData {
55+
t.Run(tt.Name, tt.F(func(input string) (Severity, error) {
56+
var s Severity
57+
return s, s.UnmarshalJSON([]byte(input))
58+
}))
59+
}
60+
})
61+
62+
t.Run("Scan", func(t *testing.T) {
63+
t.Parallel()
64+
65+
testData := []testutils.TestCase[Severity, any]{
66+
{Name: "None", Expected: SeverityNone, Data: nil, Error: nil},
67+
{Name: "Ok", Expected: SeverityOK, Data: `ok`, Error: nil},
68+
{Name: "Debug", Expected: SeverityDebug, Data: `debug`, Error: nil},
69+
{Name: "Info", Expected: SeverityInfo, Data: `info`, Error: nil},
70+
{Name: "Notice", Expected: SeverityNotice, Data: `notice`, Error: nil},
71+
{Name: "Warning", Expected: SeverityWarning, Data: `warning`, Error: nil},
72+
{Name: "Err", Expected: SeverityErr, Data: `err`, Error: nil},
73+
{Name: "Crit", Expected: SeverityCrit, Data: `crit`, Error: nil},
74+
{Name: "Alert", Expected: SeverityAlert, Data: `alert`, Error: nil},
75+
{Name: "Alert Bytes", Expected: SeverityAlert, Data: []byte("alert"), Error: nil},
76+
{Name: "Emerg", Expected: SeverityEmerg, Data: `emerg`, Error: nil},
77+
{Name: "Invalid Number", Expected: SeverityNone, Data: 150, Error: testutils.ErrorContains(`cannot scan severity from type int`)},
78+
{Name: "Invalid String", Expected: SeverityNone, Data: `invalid`, Error: testutils.ErrorContains(`unknown severity "invalid"`)},
79+
}
80+
81+
for _, tt := range testData {
82+
t.Run(tt.Name, tt.F(func(input any) (Severity, error) {
83+
var s Severity
84+
return s, s.Scan(input)
85+
}))
86+
}
87+
})
88+
89+
t.Run("Value", func(t *testing.T) {
90+
t.Parallel()
91+
92+
testdata := []testutils.TestCase[driver.Value, Severity]{
93+
{Name: "None", Expected: nil, Data: SeverityNone, Error: nil},
94+
{Name: "Ok", Expected: `ok`, Data: SeverityOK, Error: nil},
95+
{Name: "Debug", Expected: `debug`, Data: SeverityDebug, Error: nil},
96+
{Name: "Info", Expected: `info`, Data: SeverityInfo, Error: nil},
97+
{Name: "Notice", Expected: `notice`, Data: SeverityNotice, Error: nil},
98+
{Name: "Warning", Expected: `warning`, Data: SeverityWarning, Error: nil},
99+
{Name: "Err", Expected: `err`, Data: SeverityErr, Error: nil},
100+
{Name: "Crit", Expected: `crit`, Data: SeverityCrit, Error: nil},
101+
{Name: "Alert", Expected: `alert`, Data: SeverityAlert, Error: nil},
102+
{Name: "Emerg", Expected: `emerg`, Data: SeverityEmerg, Error: nil},
103+
}
104+
105+
for _, tt := range testdata {
106+
t.Run(tt.Name, tt.F(func(s Severity) (driver.Value, error) { return s.Value() }))
107+
}
108+
})
109+
}

0 commit comments

Comments
 (0)