Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4918f08
Introduce notifications `Severity` type
yhabteab Aug 4, 2025
0e06246
notifications: introduce `Event` & `Type` types
yhabteab Aug 4, 2025
28bc87c
Add `types.MakeBool` with variadic transformer functions
yhabteab Aug 5, 2025
fdfb53f
Add a base Icinga Notifications `Client`
yhabteab Aug 5, 2025
b115cca
Add `Iter()` function to `RulesResult`
yhabteab Aug 5, 2025
fecb2d2
notifications: Move Rule Version and IDs to Event
oxzi Sep 9, 2025
b7ea78e
notifications: Change Rule IDs to Strings
oxzi Sep 29, 2025
2d45c20
notifications: Remove Client.JoinIcingaWeb2Path
oxzi Sep 29, 2025
577e857
notifications: Simplify and document API
oxzi Oct 2, 2025
bf3005c
Enhance channel
sukhwinder33445 Sep 27, 2023
59d7294
pkg/plugin/plugin.go: Introduce `Object` struct All object related va…
sukhwinder33445 Oct 4, 2023
089a47b
Handle unexpected termination of plugin
sukhwinder33445 Oct 10, 2023
31bcf6c
Enhance channel plugins
sukhwinder33445 Oct 10, 2023
bcf9f2e
Improve logs
sukhwinder33445 Nov 6, 2023
ac4e04d
channel/plugin.go: `UpsertPlugins()`: use filename as type
sukhwinder33445 Nov 10, 2023
4d7ed03
pkg/rpc/rpc.go: Remove unused variable from `RPC` struct
sukhwinder33445 Nov 14, 2023
f1aa8d7
pkg/plugin/plugin.go: Plugin shutdown should wait for pending resposes
sukhwinder33445 Nov 14, 2023
73c8515
pkg/rpc/rpc.go: `rpc.Close()` should not cause `rpc.Done()`
sukhwinder33445 Nov 14, 2023
06e242e
pkg/rpc/rpc.go: SetErr() calls rpc.Close()
sukhwinder33445 Nov 14, 2023
78635f9
pkg/rpc/rpc.go: Remove method SetErr as it is only used once
sukhwinder33445 Nov 15, 2023
4ff169b
pkg/rpc/rpc.go: Rename err variables
sukhwinder33445 Nov 16, 2023
43eda27
pkg/rpc/rpc.go: Add helper function to avoid multiple unlock calls
sukhwinder33445 Nov 20, 2023
143c22a
channels: Use `incident.severity` instead of `event.severity`
sukhwinder33445 Nov 23, 2023
a38fb77
Plugin#ConfigOptions: Remove placeholder and use default instead
sukhwinder33445 Dec 12, 2023
f4cb761
plugin: Document ConfigOption's Help field
oxzi Jan 12, 2024
ad3e679
utils.IterateOrderedMap for plugin.FormatMessage
oxzi Apr 24, 2024
c50bf27
Format notification subject and message depending on event type
sukhwinder33445 Jan 10, 2024
1281dc0
Switch to `icinga-go-library`
yhabteab May 24, 2024
13ac3d3
Channel default value by changing default logic
oxzi Jul 15, 2024
9e6a60b
Increase details of Channels documentation
oxzi Jul 16, 2024
8cb0753
Use utils.IterateOrderedMap() as intended, via for:=range (require Go…
Al2Klimov Apr 17, 2025
599879f
internal/utils: Cleanup and IGL unification
oxzi May 8, 2025
e1d39d3
Drop `object_extra_tags` & all its references
yhabteab Aug 4, 2025
2097b04
Use the newly introduced notifications event utils from `igl`
yhabteab Aug 4, 2025
6d45c75
notifications: Fix import after IGL import
oxzi Oct 9, 2025
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
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ require (
golang.org/x/sys v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

tool (
golang.org/x/tools/cmd/stringer
)
55 changes: 55 additions & 0 deletions notifications/event/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package event

import (
"github.com/icinga/icinga-go-library/types"
)

// Event represents an Icinga Notifications event that can be sent to the Icinga Notifications API.
//
// It contains all the necessary fields to fully describe an Icinga Notifications event and can be used to
// serialize the event to JSON for transmission over HTTP as well as to deserialize it from JSON requests.
type Event struct {
Name string `json:"name"` // Name is the name of the object this event is all about.

// URL represents a URL or a relative reference to the object in Icinga Web 2.
//
// If the URL field does not contain a URL, but only a reference relative to an Icinga Web URL, the Icinga
// Notifications daemon will create a URL. This allows a source to set this to something like
// "/icingadb/host?name=example.com" without having to know the Icinga Web 2 root URL by itself.
URL string `json:"url"`

// Tags contains additional metadata for the event that uniquely identifies the object it's referring to.
//
// It is a map of string keys to string values, allowing for flexible tagging of events if the event
// name alone is not sufficient to identify the object. In the case of using Icinga DB as a source, the
// tags will typically look like this:
// For hosts: {"host": "host_name"} and for services: {"host": "host_name", "service": "service_name"}.
Tags map[string]string `json:"tags"`

// Type indicates the type of the event.
Type Type `json:"type"`
// Severity of the event.
Severity Severity `json:"severity,omitempty"`
// Username is the name of the user who triggered the event.
Username string `json:"username"`
// Message is a human-readable message describing the event.
Message string `json:"message"`

// Mute indicates whether the object this event is referring to should be muted or not.
//
// If set to true, the object will be muted in Icinga Web 2, meaning that notifications for this object
// will not be sent out. The MuteReason field can be used to provide a reason for muting the object.
// If you don't set this field to anything, it will be omitted from the generated JSON.
Mute types.Bool `json:"mute,omitzero"`

// MuteReason provides a reason for muting the object if Mute is set to true.
//
// Setting this field to an empty string while Mute is true will cause the request to fail,
// as Icinga Notifications requires a reason for muting an object. Otherwise, it will be omitted
// from the encoded JSON.
MuteReason string `json:"mute_reason,omitempty"`

// RulesVersion and RuleIds are the source rules matching for this Event.
RulesVersion string `json:"rules_version"`
RuleIds []string `json:"rule_ids"`
}
72 changes: 72 additions & 0 deletions notifications/event/event_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package event

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEvent(t *testing.T) {
t.Parallel()

t.Run("JsonEncode", func(t *testing.T) {
t.Parallel()

t.Run("Valid Event", func(t *testing.T) {
t.Parallel()

event := &Event{
Name: "TestEvent",
URL: "/icingadb/service?name=https%20ssl%20v3.0%20compatibility%20IE%206.0&host.name=example%20host",
Tags: map[string]string{"tag1": "value1"},
Type: TypeState,
Severity: SeverityOK,
Username: "testuser",
Message: "Test",
RulesVersion: "0x1",
RuleIds: []string{"1", "2", "3", "6"},
}

data, err := json.Marshal(event)
require.NoError(t, err)

expected := `
{
"name":"TestEvent",
"url":"/icingadb/service?name=https%20ssl%20v3.0%20compatibility%20IE%206.0&host.name=example%20host",
"tags":{"tag1":"value1"},
"type":"state",
"severity":"ok",
"username":"testuser",
"message":"Test",
"rules_version": "0x1",
"rule_ids": ["1", "2", "3", "6"]
}`
assert.JSONEq(t, expected, string(data), "JSON encoding does not match expected output")
})

t.Run("Empty Severity", func(t *testing.T) {
t.Parallel()

event := &Event{
Name: "TestEvent",
URL: "https://example.com/icingaweb2/icingadb/service?name=https%20ssl%20v3.0%20compatibility%20IE%206.0&host.name=example%20host",
Tags: map[string]string{"tag1": "value1"},
Type: TypeMute,
Username: "testuser",
Message: "Test",
}

data, err := json.Marshal(event)
require.NoError(t, err)
assert.NotContains(t, string(data), "\"severity\":", "severity should be omitted when empty")

event.Severity = SeverityNone
data, err = json.Marshal(event)
require.NoError(t, err)
assert.NotContains(t, string(data), "\"severity\":", "severity should be omitted when set to none")
})
})
}
106 changes: 106 additions & 0 deletions notifications/event/severity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//go:generate go tool stringer -linecomment -type Severity -output severity_string.go

package event

import (
"database/sql/driver"
"encoding/json"
"fmt"
)

// Severity represents the severity level of an event in Icinga notifications.
// It is an integer type with predefined constants for different severity levels.
type Severity uint8

const (
SeverityNone Severity = iota // none

SeverityOK // ok
SeverityDebug // debug
SeverityInfo // info
SeverityNotice // notice
SeverityWarning // warning
SeverityErr // err
SeverityCrit // crit
SeverityAlert // alert
SeverityEmerg // emerg

severityMax // internal
)

// MarshalJSON implements the [json.Marshaler] interface for Severity.
func (s Severity) MarshalJSON() ([]byte, error) {
if s != SeverityNone {
return json.Marshal(s.String())
} else {
return json.Marshal(nil)
}
}

// UnmarshalJSON implements the [json.Unmarshaler] interface for Severity.
func (s *Severity) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
*s = SeverityNone
return nil
}

var severityStr string
if err := json.Unmarshal(data, &severityStr); err != nil {
return err
}

severity, err := ParseSeverity(severityStr)
if err != nil {
return err
}

*s = severity
return nil
}

// Scan implements the [sql.Scanner] interface for Severity.
// Supports SQL NULL values.
func (s *Severity) Scan(src any) error {
if src == nil {
*s = SeverityNone
return nil
}

var severityStr string
switch val := src.(type) {
case string:
severityStr = val
case []byte:
severityStr = string(val)
default:
return fmt.Errorf("cannot scan severity from type %T", src)
}

severity, err := ParseSeverity(severityStr)
if err != nil {
return err
}

*s = severity
return nil
}

// Value implements the [driver.Valuer] interface for Severity.
func (s Severity) Value() (driver.Value, error) {
if s != SeverityNone {
return s.String(), nil
}
return nil, nil // Return nil for SeverityNone or invalid values
}

// ParseSeverity parses a string representation of a severity level and returns the corresponding Severity value.
// If the string does not match any known severity, it returns an error indicating the unknown severity.
func ParseSeverity(name string) (Severity, error) {
for s := range severityMax {
if s.String() == name {
return s, nil
}
}

return SeverityNone, fmt.Errorf("unknown severity %q", name)
}
33 changes: 33 additions & 0 deletions notifications/event/severity_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 109 additions & 0 deletions notifications/event/severity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package event

import (
"database/sql/driver"
"testing"

"github.com/icinga/icinga-go-library/testutils"
)

func TestSeverity(t *testing.T) {
t.Parallel()

t.Run("MarshalJson", func(t *testing.T) {
t.Parallel()

testdata := []testutils.TestCase[string, Severity]{
{Name: "None", Expected: "null", Data: SeverityNone, Error: nil},
{Name: "Ok", Expected: `"ok"`, Data: SeverityOK, Error: nil},
{Name: "Debug", Expected: `"debug"`, Data: SeverityDebug, Error: nil},
{Name: "Info", Expected: `"info"`, Data: SeverityInfo, Error: nil},
{Name: "Notice", Expected: `"notice"`, Data: SeverityNotice, Error: nil},
{Name: "Warning", Expected: `"warning"`, Data: SeverityWarning, Error: nil},
{Name: "Err", Expected: `"err"`, Data: SeverityErr, Error: nil},
{Name: "Crit", Expected: `"crit"`, Data: SeverityCrit, Error: nil},
{Name: "Alert", Expected: `"alert"`, Data: SeverityAlert, Error: nil},
{Name: "Emerg", Expected: `"emerg"`, Data: SeverityEmerg, Error: nil},
}

for _, tt := range testdata {
t.Run(tt.Name, tt.F(func(s Severity) (string, error) {
data, err := s.MarshalJSON()
return string(data), err
}))
}
})

t.Run("UnmarshalJson", func(t *testing.T) {
t.Parallel()

testData := []testutils.TestCase[Severity, string]{
{Name: "None", Expected: SeverityNone, Data: `null`, Error: nil},
{Name: "Ok", Expected: SeverityOK, Data: `"ok"`, Error: nil},
{Name: "Debug", Expected: SeverityDebug, Data: `"debug"`, Error: nil},
{Name: "Info", Expected: SeverityInfo, Data: `"info"`, Error: nil},
{Name: "Notice", Expected: SeverityNotice, Data: `"notice"`, Error: nil},
{Name: "Warning", Expected: SeverityWarning, Data: `"warning"`, Error: nil},
{Name: "Err", Expected: SeverityErr, Data: `"err"`, Error: nil},
{Name: "Crit", Expected: SeverityCrit, Data: `"crit"`, Error: nil},
{Name: "Alert", Expected: SeverityAlert, Data: `"alert"`, Error: nil},
{Name: "Emerg", Expected: SeverityEmerg, Data: `"emerg"`, Error: nil},
{Name: "Invalid", Expected: SeverityNone, Data: `"invalid"`, Error: testutils.ErrorContains(`unknown severity "invalid"`)},
}

for _, tt := range testData {
t.Run(tt.Name, tt.F(func(input string) (Severity, error) {
var s Severity
return s, s.UnmarshalJSON([]byte(input))
}))
}
})

t.Run("Scan", func(t *testing.T) {
t.Parallel()

testData := []testutils.TestCase[Severity, any]{
{Name: "None", Expected: SeverityNone, Data: nil, Error: nil},
{Name: "Ok", Expected: SeverityOK, Data: `ok`, Error: nil},
{Name: "Debug", Expected: SeverityDebug, Data: `debug`, Error: nil},
{Name: "Info", Expected: SeverityInfo, Data: `info`, Error: nil},
{Name: "Notice", Expected: SeverityNotice, Data: `notice`, Error: nil},
{Name: "Warning", Expected: SeverityWarning, Data: `warning`, Error: nil},
{Name: "Err", Expected: SeverityErr, Data: `err`, Error: nil},
{Name: "Crit", Expected: SeverityCrit, Data: `crit`, Error: nil},
{Name: "Alert", Expected: SeverityAlert, Data: `alert`, Error: nil},
{Name: "Alert Bytes", Expected: SeverityAlert, Data: []byte("alert"), Error: nil},
{Name: "Emerg", Expected: SeverityEmerg, Data: `emerg`, Error: nil},
{Name: "Invalid Number", Expected: SeverityNone, Data: 150, Error: testutils.ErrorContains(`cannot scan severity from type int`)},
{Name: "Invalid String", Expected: SeverityNone, Data: `invalid`, Error: testutils.ErrorContains(`unknown severity "invalid"`)},
}

for _, tt := range testData {
t.Run(tt.Name, tt.F(func(input any) (Severity, error) {
var s Severity
return s, s.Scan(input)
}))
}
})

t.Run("Value", func(t *testing.T) {
t.Parallel()

testdata := []testutils.TestCase[driver.Value, Severity]{
{Name: "None", Expected: nil, Data: SeverityNone, Error: nil},
{Name: "Ok", Expected: `ok`, Data: SeverityOK, Error: nil},
{Name: "Debug", Expected: `debug`, Data: SeverityDebug, Error: nil},
{Name: "Info", Expected: `info`, Data: SeverityInfo, Error: nil},
{Name: "Notice", Expected: `notice`, Data: SeverityNotice, Error: nil},
{Name: "Warning", Expected: `warning`, Data: SeverityWarning, Error: nil},
{Name: "Err", Expected: `err`, Data: SeverityErr, Error: nil},
{Name: "Crit", Expected: `crit`, Data: SeverityCrit, Error: nil},
{Name: "Alert", Expected: `alert`, Data: SeverityAlert, Error: nil},
{Name: "Emerg", Expected: `emerg`, Data: SeverityEmerg, Error: nil},
}

for _, tt := range testdata {
t.Run(tt.Name, tt.F(func(s Severity) (driver.Value, error) { return s.Value() }))
}
})
}
Loading
Loading