Skip to content

Preparation for sending non-state notifications #270

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
61 changes: 24 additions & 37 deletions internal/channel/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@ package channel
import (
"context"
"errors"
"fmt"
"github.com/icinga/icinga-notifications/internal/config/baseconf"
"github.com/icinga/icinga-notifications/internal/contracts"
"github.com/icinga/icinga-notifications/internal/event"
"github.com/icinga/icinga-notifications/internal/recipient"
"github.com/icinga/icinga-notifications/pkg/plugin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"net/url"
)

type Channel struct {
Expand Down Expand Up @@ -158,42 +154,33 @@ func (c *Channel) Restart() {
c.restartCh <- newConfig{c.Type, c.Config}
}

// Notify prepares and sends the notification request, returns a non-error on fails, nil on success
func (c *Channel) Notify(contact *recipient.Contact, i contracts.Incident, ev *event.Event, icingaweb2Url string) error {
p := c.getPlugin()
if p == nil {
return errors.New("plugin could not be started")
// Notify sends the provided notification request to the given *recipient.Contact.
// If the *plugin.Contact field of the specified *plugin.NotificationRequest is not set, it
// automatically determines the contact addresses and sets the notification request contact accordingly.
//
// Returns an error in all the following cases:
// - if the *plugin.Event of the provided notification request is not set,
// - the *plugin.Object of the provided notification request is not set,
// - trying to send a state change event without an associated *plugin.Incident,
// - the corresponding plugin of this channel cannot be started successfully,
// - or fails to successfully deliver the request to the corresponding recipient address(es).
func (c *Channel) Notify(req *plugin.NotificationRequest) error {
if req.Event == nil {
return errors.New("invalid notification request: Event is nil")
}

contactStruct := &plugin.Contact{FullName: contact.FullName}
for _, addr := range contact.Addresses {
contactStruct.Addresses = append(contactStruct.Addresses, &plugin.Address{Type: addr.Type, Address: addr.Address})
if req.Object == nil {
return errors.New("invalid notification request: Object is nil")
}
if req.Contact == nil {
return errors.New("invalid notification request: Contact is nil")
}
if req.Incident == nil && req.Event.Type == event.TypeState {
return errors.New("invalid notification request: cannot send state notification without an incident")
}

baseUrl, _ := url.Parse(icingaweb2Url)
incidentUrl := baseUrl.JoinPath("/notifications/incident")
incidentUrl.RawQuery = fmt.Sprintf("id=%d", i.ID())
object := i.IncidentObject()

req := &plugin.NotificationRequest{
Contact: contactStruct,
Object: &plugin.Object{
Name: object.DisplayName(),
Url: ev.URL,
Tags: object.Tags,
ExtraTags: object.ExtraTags,
},
Incident: &plugin.Incident{
Id: i.ID(),
Url: incidentUrl.String(),
Severity: i.SeverityString(),
},
Event: &plugin.Event{
Time: ev.Time,
Type: ev.Type,
Username: ev.Username,
Message: ev.Message,
},
p := c.getPlugin()
if p == nil {
return errors.New("plugin could not be started")
}

return p.SendNotification(req)
Expand Down
147 changes: 147 additions & 0 deletions internal/config/evaluable_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package config

import (
"github.com/icinga/icinga-notifications/internal/filter"
"github.com/icinga/icinga-notifications/internal/rule"
)

// EvalOptions specifies optional callbacks that are executed upon certain filter evaluation events.
//
// The EvalOptions type is generic and can be used with any filterable type, such as [rule.Rule] or [rule.Escalation].
// The type "T" is the type of the filterable object that is being evaluated, e.g. [rule.Rule] or [rule.Escalation].
// The type "U" is an arbitrary type that can be used to pass any value to the OnAllConfigEvaluated callback.
type EvalOptions[T, U any] struct {
// OnPreEvaluate can be used to perform arbitrary actions before evaluating the current entry of type "T".
//
// This callback receives the current entry of type "T" as an argument, whose filter is about to be evaluated.
// If this callback returns "false", the filter evaluation for the current entry is skipped. This can be useful
// to apply some pre-filtering logic, and skip certain entries based on whatever criteria you want.
OnPreEvaluate func(T) bool

// OnError can be used to handle errors that occur during the filter evaluation of type "T".
//
// This callback receives the current entry of type "T", whose filter evaluation triggered the error,
// and the error itself as arguments.
//
// By default, the filter evaluation will continue evaluating all the remaining entries even if some of them fail.
// However, you can override this behaviour by returning "false" in your handler, in which case the filter
// evaluation is aborted prematurely.
OnError func(T, error) bool

// OnFilterMatch can be used to perform some actions when the filter for the current entry of type "T" matches.
//
// This callback receives the current entry of type "T" as an argument, whose filter has just matched.
// If this callback returns an error, the filter evaluation is aborted prematurely, and the error is returned.
// If this callback returns nil, the filter evaluation continues evaluating the remaining entries until all of
// them are evaluated or a non-recoverable error occurs.
OnFilterMatch func(T) error

// OnAllConfigEvaluated can be used to perform some actions after all the configured entries of type "T" have
// been evaluated.
//
// This callback receives a value of type "U" as an argument, which can be used to pass any value you want.
// OnAllConfigEvaluated will only be called once all the entries of type "T" are evaluated, though it doesn't
// necessarily depend on the result of the individual entry filter evaluation. If the individual Eval* receivers
// don't return prematurely with an error, this hook is guaranteed to be called in any other cases. However, you
// should be aware, that this hook may not be supported by all Eval* methods.
OnAllConfigEvaluated func(U)
}

// 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:"-"`
}

// 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),
}
}

// EvaluateRules evaluates all the configured event rule.Rule(s) for the given filter.Filterable object.
//
// Please note that this function may not always evaluate *all* configured rules from the specified RuntimeConfig,
// as it internally caches all previously matched rules based on their ID.
//
// EvaluateRules allows you to specify EvalOptions and hook up certain filter evaluation steps.
// This function does not support the EvalOptions.OnAllConfigEvaluated callback and will never trigger
// it (if provided). Please refer to the description of the individual EvalOptions to find out more about
// when the hooks get triggered and possible special cases.
//
// Returns an error if any of the provided callbacks return an error, otherwise always nil.
func (e *Evaluable) EvaluateRules(r *RuntimeConfig, filterable filter.Filterable, options EvalOptions[*rule.Rule, any]) error {
for _, ru := range r.Rules {
if !e.Rules[ru.ID] && (options.OnPreEvaluate == nil || options.OnPreEvaluate(ru)) {
matched, err := ru.Eval(filterable)
if err != nil && options.OnError != nil && !options.OnError(ru, err) {
return err
}
if err != nil || !matched {
continue
}

if options.OnFilterMatch != nil {
if err := options.OnFilterMatch(ru); err != nil {
return err
}
}

e.Rules[ru.ID] = true
}
}

return nil
}

// EvaluateRuleEntries evaluates all the configured rule.Entry for the provided filter.Filterable object.
//
// This function allows you to specify EvalOptions and hook up certain filter evaluation steps.
// Currently, EvaluateRuleEntries fully support all the available EvalOptions. Please refer to the
// description of the individual EvalOptions to find out more about when the hooks get triggered and
// 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 {
retryAfter := rule.RetryNever

for ruleID := range e.Rules {
ru := r.Rules[ruleID]
if ru == nil {
// It would be appropriate to have a debug log here, but unfortunately we don't have access to a logger.
continue
}

for _, entry := range ru.Escalations {
if options.OnPreEvaluate != nil && !options.OnPreEvaluate(entry) {
continue
}

if matched, err := entry.Eval(filterable); err != nil {
if options.OnError != nil && !options.OnError(entry, err) {
return err
}
} else if cond, ok := filterable.(*rule.EscalationFilter); !matched && ok {
incidentAgeFilter := cond.ReevaluateAfter(entry.Condition)
retryAfter = min(retryAfter, incidentAgeFilter)
} else if matched {
if options.OnFilterMatch != nil {
if err := options.OnFilterMatch(entry); err != nil {
return err
}
}

e.RuleEntries[entry.ID] = entry
}
}
}

if options.OnAllConfigEvaluated != nil {
options.OnAllConfigEvaluated(retryAfter)
}

return nil
}
Loading
Loading