Skip to content
Open
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: 0 additions & 10 deletions doc/10-Channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,16 +256,6 @@ or if the channel is missing required configuration values.
"tags": {
"host": "dummy-816",
"service": "random fortune"
},
"extra_tags": {
"hostgroup/app-mobile": "",
"hostgroup/department-dev": "",
"hostgroup/env-prod": "",
"hostgroup/location-rome": "",
"servicegroup/app-storage": "",
"servicegroup/department-ps": "",
"servicegroup/env-prod": "",
"servicegroup/location-rome": ""
}
},
"incident": {
Expand Down
57 changes: 37 additions & 20 deletions doc/20-HTTP-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ The authentication is performed via HTTP Basic Authentication using the source's
When upgrading a setup from an earlier version, these usernames are still valid, but can be changed in Icinga Notifications Web.

Events sent to Icinga Notifications are expected to match rules that describe further event escalations.
These rules can be created in the web interface.
Next to an array of `rule_ids`, a `rules_version` must be provided to ensure that the source has no outdated state.
These rules can be configured in Icinga Notifications Web and should be designed to match the `relations` of the
submitted events. When submitting an event without the expected relations to evaluate the rules, Icinga Notifications
will reject the request with a `422 Unprocessable Entity` status code and a message describing the missing relations
when the `X-Icinga-Enable-Attributes-Negotiation` header is set to `true`. Otherwise, the request will be accepted
nonetheless, but some or even all the configured rules might not match, and thus the event will not be escalated.

When the submitted `rules_version` is either outdated or empty, the `/process-event` endpoint returns an HTTP 412 response.
The response's body is a JSON-encoded version of the
[`RulesInfo`](https://github.com/Icinga/icinga-go-library/blob/main/notifications/source/client.go),
containing the latest `rules_version` together with all rules for this source.
After reevaluating these rules, one can resubmit the event with the updated `rules_version`.
An example request to submit an event looks like this:

```
curl -v -u 'source-2:insecureinsecure' -d '@-' 'http://localhost:5680/process-event' <<EOF
Expand All @@ -37,22 +36,40 @@ curl -v -u 'source-2:insecureinsecure' -d '@-' 'http://localhost:5680/process-ev
"host": "dummy-809",
"service": "random fortune"
},
"extra_tags": {
"hostgroup/app-container": null,
"hostgroup/department-dev": null,
"hostgroup/env-qa": null,
"hostgroup/location-rome": null,
"servicegroup/app-mail": null,
"servicegroup/department-nms": null,
"servicegroup/env-prod": null,
"servicegroup/location-berlin": null
},
"type": "state",
"severity": "crit",
"username": "",
"message": "Something went somewhere very wrong.",
"rules_version": "23",
"rule_ids": ["0"]
"relations": {
"host": {
"name": "dummy-809",
"display_name": "My Dummy Host",
"vars": {
"os": "linux"
}
},
"services": [
{
"name": "random fortune",
"display_name": "Random Fortune Service",
"vars": {
"env": "production",
"team": "devops"
}
}
],
"hostgroups": [
{
"name": "linux-servers",
"display_name": "Linux Servers"
}
],
"servicegroups": [
{
"name": "production-services",
"display_name": "Production Services"
}
]
}
}
EOF
```
Expand Down
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,31 @@ require (
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.24.0
github.com/google/uuid v1.6.0
github.com/icinga/icinga-go-library v0.9.0
github.com/icinga/icinga-go-library v0.9.1-0.20260430073722-ab64a50d3fe9
github.com/jhillyerd/enmime v1.3.0
github.com/jmoiron/sqlx v1.4.0
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.11.1
github.com/teambition/rrule-go v1.8.2
github.com/theory/jsonpath v0.12.0
go.uber.org/zap v1.28.0
golang.org/x/crypto v0.50.0
golang.org/x/sync v0.20.0
)

require (
filippo.io/edwards25519 v1.1.1 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/caarlos0/env/v11 v11.4.0 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/goccy/go-yaml v1.13.0 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/lib/pq v1.11.2 // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
Expand Down
21 changes: 20 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc=
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
Expand All @@ -26,6 +28,8 @@ github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaC
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-yaml v1.13.0 h1:0Wtp0FZLd7Sm8gERmR9S6Iczzb3vItJj7NaHmFg8pTs=
Expand All @@ -38,6 +42,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/icinga/icinga-go-library v0.9.0 h1:U0zpgpRIjO2gEwlTkHCHGgvW+ZuZeb2W7R6OGcnkGTI=
github.com/icinga/icinga-go-library v0.9.0/go.mod h1:7vvur6e1MOsM50oeYBYLkxA7H1F1ZCS0anZfG11kYgY=
github.com/icinga/icinga-go-library v0.9.1-0.20260428141622-3dc9c05cb7a4 h1:bu8cqw2nQQlSxj+vU4LUzoo3d6Kl4RZv8K+naRQLUME=
github.com/icinga/icinga-go-library v0.9.1-0.20260428141622-3dc9c05cb7a4/go.mod h1:fuQx9hTs6EetUOThhX0p/nYLefZxSKl0TlPQHu1KG0I=
github.com/icinga/icinga-go-library v0.9.1-0.20260430073722-ab64a50d3fe9 h1:LElH2B7LK6oG7nQWds7rX0ALbvvw5l1rIi5TEhnFkp8=
github.com/icinga/icinga-go-library v0.9.1-0.20260430073722-ab64a50d3fe9/go.mod h1:L6zwhdk7XDWkeO/56QpTHHOyv700yflJdpcZzbckwQ8=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
Expand All @@ -46,11 +54,17 @@ github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99r
github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand All @@ -72,6 +86,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/ssgreg/journald v1.0.0 h1:0YmTDPJXxcWDPba12qNMdO6TxvfkFSYpFIJ31CwmLcU=
github.com/ssgreg/journald v1.0.0/go.mod h1:RUckwmTM8ghGWPslq2+ZBZzbb9/2KgjzYZ4JEP+oRt0=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
Expand All @@ -80,6 +96,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
github.com/theory/jsonpath v0.12.0 h1:NQeuE0ohHHhss0DoxU9Xu2IpTTrlx9x4mv4F3pcmDME=
github.com/theory/jsonpath v0.12.0/go.mod h1:vl8nfJyq9MKMbcAiKv+7N9W3jDCH8qPr0mZoZj8wRk8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
Expand All @@ -102,7 +120,8 @@ golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
7 changes: 3 additions & 4 deletions internal/channel/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,9 @@ func (c *Channel) Notify(contact *recipient.Contact, i contracts.Incident, ev *e
req := &plugin.NotificationRequest{
Contact: contactStruct,
Object: &plugin.Object{
Name: object.DisplayName(),
Url: ev.URL,
Tags: object.Tags,
ExtraTags: object.ExtraTags,
Name: object.DisplayName(),
Url: ev.URL,
Tags: object.Tags,
},
Incident: &plugin.Incident{
Id: i.ID(),
Expand Down
124 changes: 35 additions & 89 deletions internal/config/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,78 +4,33 @@ import (
"fmt"
"github.com/icinga/icinga-notifications/internal/rule"
"slices"
"time"
)

// SourceRuleVersion for SourceRulesInfo, consisting of two numbers, one static and one incrementable.
type SourceRuleVersion struct {
Major int64
Minor int64
}

// NewSourceRuleVersion creates a new source version based on the current timestamp and a zero counter.
func NewSourceRuleVersion() SourceRuleVersion {
return SourceRuleVersion{
Major: time.Now().UnixMilli(),
Minor: 0,
// GetRulesFilterColumnsForSource returns a set of all filter columns used in the rules of the given source.
//
// This can be used by sources to determine which columns they need to provide for the events to be
// able to evaluate the rules of this source.
func (r *RuntimeConfig) GetRulesFilterColumnsForSource(src *Source) []string {
r.RLock()
defer r.RUnlock()

var columns []string
for _, id := range src.RuleIDs() {
eventRule, ok := r.Rules[id]
if !ok {
continue
}
for column := range eventRule.FilterColumns {
if !slices.Contains(columns, column) {
columns = append(columns, column)
}
}
}
}

// Increment the version counter.
func (sourceVersion *SourceRuleVersion) Increment() {
sourceVersion.Minor++
}

// String implements fmt.Stringer and returns a pretty-printable representation.
func (sourceVersion *SourceRuleVersion) String() string {
return fmt.Sprintf("%x-%x", sourceVersion.Major, sourceVersion.Minor)
}

// SourceRulesInfo holds information about the rules associated with a specific source.
type SourceRulesInfo struct {
// Version is the version of the rules for the source.
//
// Multiple source's versions are independent of another.
Version SourceRuleVersion

// RuleIDs is a list of rule IDs associated with a specific source.
//
// It is used to quickly access the rules for a specific source without iterating over all rules.
RuleIDs []int64
return columns
}

// applyPendingRules synchronizes changed rules.
func (r *RuntimeConfig) applyPendingRules() {
// Keep track of sources the rules were updated for, so we can update their version later.
updatedSources := make(map[int64]struct{})

if r.RulesBySource == nil {
r.RulesBySource = make(map[int64]*SourceRulesInfo)
}

addToRulesBySource := func(elem *rule.Rule) {
if sourceInfo, ok := r.RulesBySource[elem.SourceID]; ok {
sourceInfo.RuleIDs = append(sourceInfo.RuleIDs, elem.ID)
} else {
r.RulesBySource[elem.SourceID] = &SourceRulesInfo{
Version: NewSourceRuleVersion(),
RuleIDs: []int64{elem.ID},
}
}

updatedSources[elem.SourceID] = struct{}{}
}

delFromRulesBySource := func(elem *rule.Rule) {
if sourceInfo, ok := r.RulesBySource[elem.SourceID]; ok {
sourceInfo.RuleIDs = slices.DeleteFunc(sourceInfo.RuleIDs, func(id int64) bool {
return id == elem.ID
})
}

updatedSources[elem.SourceID] = struct{}{}
}

incrementalApplyPending(
r,
&r.Rules, &r.configChange.Rules,
Expand All @@ -89,9 +44,11 @@ func (r *RuntimeConfig) applyPendingRules() {
}

newElement.Escalations = make(map[int64]*rule.Escalation)

addToRulesBySource(newElement)

// If the source this rule belongs to is already known, add this rule to the source's rule list.
// Otherwise, the rule will be added to that list when its source is being loaded.
if src, ok := r.Sources[newElement.SourceID]; ok {
src.ruleIDs = append(src.ruleIDs, newElement.ID)
}
return nil
},
func(curElement, update *rule.Rule) error {
Expand All @@ -109,36 +66,25 @@ func (r *RuntimeConfig) applyPendingRules() {
curElement.TimePeriod = nil
}

if curElement.SourceID != update.SourceID {
delFromRulesBySource(curElement)
curElement.SourceID = update.SourceID
addToRulesBySource(curElement)
}

// ObjectFilterExpr is being initialized by config.IncrementalConfigurableInitAndValidatable.
// ObjectFilter{,Expr} are being initialized by config.IncrementalConfigurableInitAndValidatable.
curElement.ObjectFilter = update.ObjectFilter
curElement.ObjectFilterExpr = update.ObjectFilterExpr

updatedSources[curElement.SourceID] = struct{}{}
curElement.FilterColumns = update.FilterColumns

return nil
},
func(delElement *rule.Rule) error {
delFromRulesBySource(delElement)

// If the source this rule belongs to is already known, remove this rule from the source's rule list.
// Otherwise, there's nothing more to do!
if src, ok := r.Sources[delElement.SourceID]; ok {
src.ruleIDs = slices.DeleteFunc(src.ruleIDs, func(id int64) bool {
return id == delElement.ID
})
}
return nil
},
)

// After applying the rules, we need to update the version of the sources that were modified.
// This is done to ensure that the version is incremented whenever a rule is added, modified,
// or deleted only once per applyPendingRules call, even if multiple rules from the same source
// were changed.
for sourceID := range updatedSources {
if sourceInfo, ok := r.RulesBySource[sourceID]; ok {
sourceInfo.Version.Increment()
}
}

incrementalApplyPending(
r,
&r.ruleEscalations, &r.configChange.ruleEscalations,
Expand Down
Loading
Loading