Skip to content
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

Preparation for sending non-state notifications #270

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
137 changes: 137 additions & 0 deletions internal/config/evaluable_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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.
type EvalOptions[T, U any] struct {
// OnPreEvaluate can be used to perform arbitrary actions before evaluating the current entry of type "T".
// An entry of type "T" for which this hook returns "false" will be excluded from evaluation.
OnPreEvaluate func(T) bool

// OnError can be used to perform arbitrary actions on filter evaluation errors.
// The original filter evaluation error is passed to this function as well as the current
// entry of type "T", whose filter evaluation triggered the error.
//
// By default, the filter evaluation doesn't get interrupted if any of them fail, instead it will continue
// evaluating all the remaining entries. 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 arbitrary actions after a successful filter evaluation of type "T".
// This callback obtains the current entry of type "T" as an argument, whose filter matched on the filterableTest.
//
// Note, any error returned by the OnFilterMatch hook causes the filter evaluation to be aborted
// immediately before even reaching the remaining ones.
OnFilterMatch func(T) error

// OnAllConfigEvaluated can be used to perform some post filter evaluation actions.
// This handler receives an arbitrary value, be it a result of any filter evaluation or a made-up one of type "U".
//
// 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